diff --git a/CMakeLists.txt b/CMakeLists.txt
index 17a56c85..949e329a 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -53,11 +53,9 @@ set(SACNVIEW_HEADER_PATHS
set(SACNVIEW_HEADERS
src/ui/mdimainwindow.h
src/sacn/securesacn.h
- src/widgets/monitorspinbox.h
src/widgets/qpushbutton_rightclick.h
src/widgets/qspinbox_resizetocontent.h
src/ui/newversiondialog.h
- src/ui/scopewindow.h
src/ui/universeview.h
src/sacn/sacnsynchronization.h
src/models/sacnsynclistmodel.h
@@ -78,7 +76,6 @@ set(SACNVIEW_HEADERS
src/sacn/sacnsender.h
src/ui/configureperchanpriodlg.h
src/widgets/gridwidget.h
- src/widgets/scopewidget.h
src/ui/aboutdialog.h
src/sacn/sacneffectengine.h
src/models/sacnuniverselistmodel.h
@@ -102,13 +99,15 @@ set(SACNVIEW_HEADERS
src/ui/multiview.h
src/models/sacnsourcetablemodel.h
src/models/csvmodelexport.h
+ src/widgets/glscopewidget.h
+ src/widgets/steppedspinbox.h
)
set(SACNVIEW_SOURCES
+ sacnview.natvis
src/commandline.cpp
src/firewallcheck.cpp
src/ipc.cpp
- src/main.cpp
src/preferences.cpp
src/models/sacnsynclistmodel.cpp
src/models/sacndiscoveredsourcelistmodel.cpp
@@ -133,7 +132,6 @@ set(SACNVIEW_SOURCES
src/ui/configureperchanpriodlg.cpp
src/ui/newversiondialog.cpp
src/ui/mdimainwindow.cpp
- src/ui/scopewindow.cpp
src/ui/universeview.cpp
src/ui/nicselectdialog.cpp
src/ui/preferencesdialog.cpp
@@ -145,21 +143,24 @@ set(SACNVIEW_SOURCES
src/ui/bigdisplay.cpp
src/ui/addmultidialog.cpp
src/ui/multiview.cpp
- src/widgets/monitorspinbox.cpp
+ src/ui/glscopewindow.cpp
src/widgets/qpushbutton_rightclick.cpp
src/widgets/qspinbox_resizetocontent.cpp
+ src/widgets/steppedspinbox.cpp
src/widgets/universedisplay.cpp
src/widgets/gridwidget.cpp
- src/widgets/scopewidget.cpp
src/widgets/clssnapshot.cpp
src/widgets/grideditwidget.cpp
+ src/widgets/glscopewidget.cpp
src/models/sacnsourcetablemodel.cpp
src/models/csvmodelexport.cpp
)
+# Prepend the path to all sources
+LIST(TRANSFORM SACNVIEW_SOURCES PREPEND ${CMAKE_CURRENT_LIST_DIR}/)
+
set(SACNVIEW_FORMS
ui/mdimainwindow.ui
- ui/scopewindow.ui
ui/universeview.ui
ui/nicselectdialog.ui
ui/preferencesdialog.ui
@@ -199,8 +200,18 @@ set(CMAKE_AUTORCC ON)
# Find Qt 5.15 or Qt 6
find_package(QT NAMES Qt6 Qt5 REQUIRED COMPONENTS Core)
find_package(Qt${QT_VERSION_MAJOR} COMPONENTS Core Gui Network Multimedia Widgets REQUIRED)
+set(SACNVIEW_QT_LIBRARIES
+ Qt::Core Qt::Gui Qt::Network Qt::Multimedia Qt::Widgets
+)
+
+# Qt 6 moved some components
+if (Qt6_FOUND)
+ find_package(Qt6 COMPONENTS OpenGLWidgets REQUIRED)
+ list(APPEND SACNVIEW_QT_LIBRARIES Qt::OpenGLWidgets)
+endif()
add_executable(sACNView WIN32
+ src/main.cpp
${SACNVIEW_HEADERS}
${SACNVIEW_SOURCES}
${SACNVIEW_FORMS}
@@ -219,7 +230,7 @@ set_target_properties(sACNView PROPERTIES
)
# Link Qt libraries
-target_link_libraries(sACNView PRIVATE Qt::Core Qt::Gui Qt::Network Qt::Multimedia Qt::Widgets)
+target_link_libraries(sACNView PRIVATE ${SACNVIEW_QT_LIBRARIES})
# Link PCap/WinPCap libraries
target_link_libraries(sACNView PRIVATE ${PCAP_LIBS})
diff --git a/install/win/install.nsi b/install/win/install.nsi
index 96c64b71..a78319b9 100644
--- a/install/win/install.nsi
+++ b/install/win/install.nsi
@@ -15,6 +15,7 @@
; limitations under the License.
SetCompressor /SOLID lzma
+RequestExecutionLevel admin
!define PRODUCT_NAME "sACNView"
!define PRODUCT_PUBLISHER "Tom Steer"
@@ -55,27 +56,17 @@ ShowUninstDetails show
InstallDir "$PROGRAMFILES\${PRODUCT_NAME}"
InstallDirRegKey ${INSTDIR_REG_ROOT} "${INSTDIR_REG_KEY}" "InstallDir"
+; Create this file if you wish to sign the installer and uninstaller
+; Example content (NSIS 3.08):
+; !finalize 'C:\sign.bat "%1" "${PRODUCT_NAME} Installer" http://example.com' = 0
+; !uninstfinalize 'C:\sign.bat "%1" "${PRODUCT_NAME} Installer" http://example.com' = 0
+
+!include /NONFATAL ..\signing\sign_installer.nsh
+
!insertmacro INTERACTIVE_UNINSTALL
!insertmacro MUI_PAGE_WELCOME
-; Check for Admin rights
-Section CheckAdmin
- DetailPrint "Checking Admin Rights"
- System::Call "kernel32::GetModuleHandle(t 'shell32.dll') i .s"
- System::Call "kernel32::GetProcAddress(i s, i 680) i .r0"
- System::Call "::$0() i .r0"
-
- IntCmp $0 0 isNotAdmin isNotAdmin isAdmin
-isNotAdmin:
- DetailPrint "Missing Administrator Rights !!!"
- messageBox MB_OK "You do not have Administrator rights on this computer.$\r$\r\
-Please log in as an administrator to install sACNView."
- quit
-isAdmin:
- DetailPrint "Administrator Rights granted"
-SectionEnd
-
!insertmacro MUI_PAGE_DIRECTORY
!insertmacro MUI_PAGE_INSTFILES
!define MUI_FINISHPAGE_RUN $INSTDIR\${PRODUCT_NAME}.exe
@@ -121,7 +112,13 @@ Section "Main Application" sec01
;Same as create shortcut you need to use ${UNINST_EXE} instead of anything else.
WriteRegStr ${INSTDIR_REG_ROOT} "${INSTDIR_REG_KEY}" "UninstallString" "${UNINST_EXE}"
- SimpleFC::AddApplication "sACNView" "$INSTDIR\sACNView.exe" 0 2 "" 1
+ DetailPrint "Adding Firewall Exception"
+ ; rule_name, description, protocol, direction,
+ ; status, profiles, action, application,
+ ; service_name, icmp_types_and_codes, group, local_ports, remote_ports, local_address, remote_address
+ SimpleFC::AdvAddRule ${PRODUCT_NAME} "${PRODUCT_NAME} UDP Multicast Receive" "17" \
+ "1" "1" "2147483647" "1" "$INSTDIR\sACNView.exe" \
+ "" "" "@$INSTDIR\sACNView.exe,-10000" "" "" "" ""
Pop $0
IntCmp $0 0 fw_ok
@@ -158,21 +155,23 @@ FunctionEnd
#######################################################################################
Section UnInstall
+ ;uninstall from path, must be repeated for every install logged path individual
+ !insertmacro UNINSTALL.LOG_UNINSTALL "$INSTDIR"
- ;begin uninstall, especially for MUI could be added in UN.onInit function instead
- ;!insertmacro UNINSTALL.LOG_BEGIN_UNINSTALL
-
- ;uninstall from path, must be repeated for every install logged path individual
- !insertmacro UNINSTALL.LOG_UNINSTALL "$INSTDIR"
+ ;end uninstall, after uninstall from all logged paths has been performed
+ !insertmacro UNINSTALL.LOG_END_UNINSTALL
- ;end uninstall, after uninstall from all logged paths has been performed
- !insertmacro UNINSTALL.LOG_END_UNINSTALL
+ ; Remove firewall exception
+ SimpleFC::AdvRemoveRule ${PRODUCT_NAME}
- Delete "$SMPROGRAMS\${PRODUCT_NAME}\${PRODUCT_NAME}.lnk"
- Delete "$SMPROGRAMS\${PRODUCT_NAME}\Uninstall.lnk"
- RmDir "$SMPROGRAMS\${PRODUCT_NAME}"
+ DeleteRegValue ${INSTDIR_REG_ROOT} "${INSTDIR_REG_KEY}" "InstallDir"
+ DeleteRegValue ${INSTDIR_REG_ROOT} "${INSTDIR_REG_KEY}" "DisplayIcon"
+ DeleteRegValue ${INSTDIR_REG_ROOT} "${INSTDIR_REG_KEY}" "DisplayName"
+ DeleteRegValue ${INSTDIR_REG_ROOT} "${INSTDIR_REG_KEY}" "DisplayVersion"
+ DeleteRegValue ${INSTDIR_REG_ROOT} "${INSTDIR_REG_KEY}" "Publisher"
+ DeleteRegValue ${INSTDIR_REG_ROOT} "${INSTDIR_REG_KEY}" "UninstallString"
- DeleteRegKey /ifempty ${INSTDIR_REG_ROOT} "${INSTDIR_REG_KEY}"
+ DeleteRegKey /ifempty ${INSTDIR_REG_ROOT} "${INSTDIR_REG_KEY}"
SectionEnd
diff --git a/install/win64/install.nsi b/install/win64/install.nsi
index a230bb52..f6c4cac1 100644
--- a/install/win64/install.nsi
+++ b/install/win64/install.nsi
@@ -17,6 +17,7 @@
Unicode true
SetCompressor /SOLID lzma
+RequestExecutionLevel admin
!define PRODUCT_NAME "sACNView64"
!define PRODUCT_PUBLISHER "Tom Steer"
@@ -58,27 +59,17 @@ ShowUninstDetails show
InstallDir "$PROGRAMFILES64\${PRODUCT_NAME}"
InstallDirRegKey ${INSTDIR_REG_ROOT} "${INSTDIR_REG_KEY}" "InstallDir"
+; Create this file if you wish to sign the installer and uninstaller
+; Example content (NSIS 3.08):
+; !finalize 'C:\sign.bat "%1" "${PRODUCT_NAME} Installer" http://example.com' = 0
+; !uninstfinalize 'C:\sign.bat "%1" "${PRODUCT_NAME} Installer" http://example.com' = 0
+
+!include /NONFATAL ..\signing\sign_installer.nsh
+
!insertmacro INTERACTIVE_UNINSTALL
!insertmacro MUI_PAGE_WELCOME
-; Check for Admin rights
-Section CheckAdmin
- DetailPrint "Checking Admin Rights"
- System::Call "kernel32::GetModuleHandle(t 'shell32.dll') i .s"
- System::Call "kernel32::GetProcAddress(i s, i 680) i .r0"
- System::Call "::$0() i .r0"
-
- IntCmp $0 0 isNotAdmin isNotAdmin isAdmin
-isNotAdmin:
- DetailPrint "Missing Administrator Rights !!!"
- messageBox MB_OK "You do not have Administrator rights on this computer.$\r$\r\
-Please log in as an administrator to install sACNView."
- quit
-isAdmin:
- DetailPrint "Administrator Rights granted"
-SectionEnd
-
!insertmacro MUI_PAGE_DIRECTORY
!insertmacro MUI_PAGE_INSTFILES
!define MUI_FINISHPAGE_RUN $INSTDIR\sACNView.exe
@@ -125,8 +116,13 @@ Section "Main Application" sec01
;Same as create shortcut you need to use ${UNINST_EXE} instead of anything else.
WriteRegStr ${INSTDIR_REG_ROOT} "${INSTDIR_REG_KEY}" "UninstallString" "${UNINST_EXE}"
- ; Add firewall exception
- SimpleFC::AddApplication ${PRODUCT_NAME} "$INSTDIR\sACNView.exe" 0 2 "" 1
+ DetailPrint "Adding Firewall Exception"
+ ; rule_name, description, protocol, direction,
+ ; status, profiles, action, application,
+ ; service_name, icmp_types_and_codes, group, local_ports, remote_ports, local_address, remote_address
+ SimpleFC::AdvAddRule ${PRODUCT_NAME} "${PRODUCT_NAME} UDP Multicast Receive" "17" \
+ "1" "1" "2147483647" "1" "$INSTDIR\sACNView.exe" \
+ "" "" "@$INSTDIR\sACNView.exe,-10000" "" "" "" ""
Pop $0
IntCmp $0 0 fw_ok
@@ -169,22 +165,26 @@ Section UnInstall
; Use the 64bit registry
SetRegView 64
- ;begin uninstall, especially for MUI could be added in UN.onInit function instead
- ;!insertmacro UNINSTALL.LOG_BEGIN_UNINSTALL
+ ;uninstall from path, must be repeated for every install logged path individual
+ !insertmacro UNINSTALL.LOG_UNINSTALL "$INSTDIR"
+
+ ;end uninstall, after uninstall from all logged paths has been performed
+ !insertmacro UNINSTALL.LOG_END_UNINSTALL
- ;uninstall from path, must be repeated for every install logged path individual
- !insertmacro UNINSTALL.LOG_UNINSTALL "$INSTDIR"
+ Delete "$SMPROGRAMS\${PRODUCT_NAME}\${PRODUCT_NAME}.lnk"
+ RmDir "$SMPROGRAMS\${PRODUCT_NAME}"
- ;end uninstall, after uninstall from all logged paths has been performed
- !insertmacro UNINSTALL.LOG_END_UNINSTALL
+ ; Remove firewall exception
+ SimpleFC::AdvRemoveRule ${PRODUCT_NAME}
- Delete "$SMPROGRAMS\${PRODUCT_NAME}\${PRODUCT_NAME}.lnk"
- RmDir "$SMPROGRAMS\${PRODUCT_NAME}"
-
- ; Remove firewall exception
- SimpleFC::RemoveApplication "$INSTDIR\sACNView.exe"
+ DeleteRegValue ${INSTDIR_REG_ROOT} "${INSTDIR_REG_KEY}" "InstallDir"
+ DeleteRegValue ${INSTDIR_REG_ROOT} "${INSTDIR_REG_KEY}" "DisplayIcon"
+ DeleteRegValue ${INSTDIR_REG_ROOT} "${INSTDIR_REG_KEY}" "DisplayName"
+ DeleteRegValue ${INSTDIR_REG_ROOT} "${INSTDIR_REG_KEY}" "DisplayVersion"
+ DeleteRegValue ${INSTDIR_REG_ROOT} "${INSTDIR_REG_KEY}" "Publisher"
+ DeleteRegValue ${INSTDIR_REG_ROOT} "${INSTDIR_REG_KEY}" "UninstallString"
- DeleteRegKey /ifempty ${INSTDIR_REG_ROOT} "${INSTDIR_REG_KEY}"
+ DeleteRegKey /ifempty ${INSTDIR_REG_ROOT} "${INSTDIR_REG_KEY}"
SectionEnd
@@ -193,7 +193,7 @@ Function UN.onInit
; Use the 64bit registry
SetRegView 64
- ;begin uninstall, could be added on top of uninstall section instead
- !insertmacro UNINSTALL.LOG_BEGIN_UNINSTALL
+ ;begin uninstall, could be added on top of uninstall section instead
+ !insertmacro UNINSTALL.LOG_BEGIN_UNINSTALL
FunctionEnd
diff --git a/sACNView.pro b/sACNView.pro
index 8f28b548..1ed5a347 100644
--- a/sACNView.pro
+++ b/sACNView.pro
@@ -15,6 +15,10 @@
QT += core gui network multimedia widgets
+greaterThan(QT_MAJOR_VERSION, 5) {
+ QT += openglwidgets
+}
+
TARGET = sACNView
TEMPLATE = app
DESCRIPTION = $$shell_quote("A tool for sending and receiving the Streaming ACN control protocol")
@@ -89,12 +93,12 @@ INCLUDEPATH += src \
SOURCES += src/main.cpp\
src/sacn/securesacn.cpp \
- src/widgets/monitorspinbox.cpp \
+ src/widgets/steppedspinbox.cpp \
src/widgets/qpushbutton_rightclick.cpp \
src/widgets/qspinbox_resizetocontent.cpp \
src/ui/newversiondialog.cpp \
src/ui/mdimainwindow.cpp \
- src/ui/scopewindow.cpp \
+ src/ui/glscopewindow.cpp \
src/ui/universeview.cpp \
src/ui/multiview.cpp \
src/sacn/sacnsynchronization.cpp \
@@ -114,7 +118,7 @@ SOURCES += src/main.cpp\
src/sacn/sacnsender.cpp \
src/ui/configureperchanpriodlg.cpp \
src/widgets/gridwidget.cpp \
- src/widgets/scopewidget.cpp \
+ src/widgets/glscopewidget.cpp \
src/ui/aboutdialog.cpp \
src/sacn/sacneffectengine.cpp \
src/models/sacnuniverselistmodel.cpp \
@@ -138,11 +142,11 @@ SOURCES += src/main.cpp\
HEADERS += src/ui/mdimainwindow.h \
src/sacn/securesacn.h \
- src/widgets/monitorspinbox.h \
+ src/widgets/steppedspinbox.h \
src/widgets/qpushbutton_rightclick.h \
src/widgets/qspinbox_resizetocontent.h \
src/ui/newversiondialog.h \
- src/ui/scopewindow.h \
+ src/ui/glscopewindow.h \
src/ui/universeview.h \
src/ui/multiview.h \
src/sacn/sacnsynchronization.h \
@@ -164,7 +168,7 @@ HEADERS += src/ui/mdimainwindow.h \
src/sacn/sacnsender.h \
src/ui/configureperchanpriodlg.h \
src/widgets/gridwidget.h \
- src/widgets/scopewidget.h \
+ src/widgets/glscopewidget.h \
src/ui/aboutdialog.h \
src/sacn/sacneffectengine.h \
src/models/sacnuniverselistmodel.h \
@@ -189,7 +193,6 @@ HEADERS += src/ui/mdimainwindow.h \
src/widgets/grideditwidget.h
FORMS += ui/mdimainwindow.ui \
- ui/scopewindow.ui \
ui/universeview.ui \
ui/multiview.ui \
ui/nicselectdialog.ui \
diff --git a/sacnview.natvis b/sacnview.natvis
new file mode 100644
index 00000000..15611501
--- /dev/null
+++ b/sacnview.natvis
@@ -0,0 +1,12 @@
+
+
+
+ {{{(m_cid[0]),nvoXb}{(m_cid[1]),nvoXb}{(m_cid[2]),nvoXb}{(m_cid[3]),nvoXb}-{(m_cid[4]),nvoXb}{(m_cid[5]),nvoXb}-{(m_cid[6]),nvoXb}{(m_cid[7]),nvoXb}-{(m_cid[8]),nvoXb}{(m_cid[9]),nvoXb}-{(m_cid[10]),nvoXb}{(m_cid[11]),nvoXb}{(m_cid[12]),nvoXb}{(m_cid[13]),nvoXb}{(m_cid[14]),nvoXb}{(m_cid[15]),nvoXb}}}
+
+
+ {m_cid}
+
+
+ {{ IPv4Addr = {(((uint32_t*)d.d)[8]>>24)&0xff,d}.{(((uint32_t*)d.d)[8]>>16)&0xff,d}.{(((uint32_t*)d.d)[8]>>8)&0xff,d}.{(((uint32_t*)d.d)[8]>>0)&0xff,d} }}
+
+
\ No newline at end of file
diff --git a/src/main.cpp b/src/main.cpp
index eaa4e289..c7f13b89 100644
--- a/src/main.cpp
+++ b/src/main.cpp
@@ -26,12 +26,15 @@
#include
#include
#include
+#include
+
#include "themes.h"
#include "sacnsender.h"
#include "newversiondialog.h"
#include "firewallcheck.h"
#include "ipc.h"
#include "translations/translationdialog.h"
+
#ifdef USE_BREAKPAD
#include "crash_handler.h"
#include "crash_test.h"
@@ -46,6 +49,13 @@ int main(int argc, char *argv[])
QCoreApplication::setAttribute(Qt::AA_UseHighDpiPixmaps);
QGuiApplication::setHighDpiScaleFactorRoundingPolicy(Qt::HighDpiScaleFactorRoundingPolicy::PassThrough);
+ // Share the OpenGL Contexts
+ QCoreApplication::setAttribute(Qt::AA_ShareOpenGLContexts);
+ {
+ QSurfaceFormat format;
+ QSurfaceFormat::setDefaultFormat(format);
+ }
+
QApplication a(argc, argv);
a.setApplicationName(APP_NAME);
@@ -165,6 +175,7 @@ int main(int argc, char *argv[])
int result = a.exec();
+ w->saveSubWindows();
delete w;
Preferences::Instance().savePreferences();
diff --git a/src/models/sacnsourcetablemodel.cpp b/src/models/sacnsourcetablemodel.cpp
index 1960d9c9..dc66b678 100644
--- a/src/models/sacnsourcetablemodel.cpp
+++ b/src/models/sacnsourcetablemodel.cpp
@@ -65,8 +65,10 @@ QVariant SACNSourceTableModel::data(const QModelIndex& index, int role) const
// Text
case COL_NAME:
case COL_CID:
+ case COL_TIME_SUMMARY:
return static_cast(Qt::AlignVCenter | Qt::AlignLeft);
// Numeric
+ case COL_UNIVERSE:
case COL_PRIO:
case COL_SYNC:
case COL_FPS:
@@ -106,6 +108,7 @@ QVariant SACNSourceTableModel::getDisplayData(const RowData& rowData, int column
return QStringLiteral("??");
}
case COL_CID: return rowData.cid.operator QString();
+ case COL_UNIVERSE: return rowData.universe;
case COL_PRIO: return rowData.per_address ? QStringLiteral("(*) ") + QString::number(rowData.priority) : QString::number(rowData.priority);
case COL_SYNC:
if (rowData.protocol_version == sACNProtocolDraft)
@@ -117,6 +120,7 @@ QVariant SACNSourceTableModel::getDisplayData(const RowData& rowData, int column
case COL_PREVIEW: return (rowData.preview ? tr("Yes") : tr("No"));
case COL_IP: return rowData.ip.toString();
case COL_FPS: return QStringLiteral("%1Hz").arg(QString::number(rowData.fps, 'f', 2));
+ case COL_TIME_SUMMARY: return getTimingSummary(rowData);
case COL_SEQ_ERR: return rowData.seq_err;
case COL_JUMPS: return rowData.jumps;
case COL_VER: return GetProtocolVersionString(rowData.protocol_version);
@@ -162,6 +166,7 @@ QVariant SACNSourceTableModel::getBackgroundData(const RowData& rowData, int col
case SourceSecure::Yes: return Preferences::Instance().colorForStatus(Preferences::Status::Good);
}
case COL_CID:
+ case COL_UNIVERSE:
case COL_PRIO:
case COL_SYNC:
case COL_PREVIEW:
@@ -178,6 +183,33 @@ QVariant SACNSourceTableModel::getBackgroundData(const RowData& rowData, int col
return QVariant();
}
+QVariant SACNSourceTableModel::getTimingSummary(const RowData& rowData) const
+{
+ const FpsCounter::Histogram& histogram = rowData.histogram;
+ if (histogram.empty())
+ return tr("N/A");
+
+ // Min - max
+ QString result = QStringLiteral("%1-%2ms").arg(std::chrono::milliseconds(histogram.begin()->first).count()).arg(std::chrono::milliseconds(histogram.rbegin()->first).count());
+ // And count of interesting ranges
+ size_t shortCount = 0;
+ size_t longCount = 0;
+ size_t staticCount = 0;
+ for (const auto& item : histogram)
+ {
+ // Times are rounded up, don't count anything twice
+ if (item.first <= m_shortInterval)
+ shortCount += item.second;
+ else if (item.first > m_staticInterval)
+ staticCount += item.second;
+ else if (item.first > m_longInterval)
+ longCount += item.second;
+ }
+
+ result.append(QStringLiteral(" (%1 %2 %3)").arg(shortCount).arg(longCount).arg(staticCount));
+ return result;
+}
+
QVariant SACNSourceTableModel::headerData(int section, Qt::Orientation orientation, int role) const
{
@@ -192,11 +224,16 @@ QVariant SACNSourceTableModel::headerData(int section, Qt::Orientation orientati
case COL_NAME: return tr("Name");
case COL_ONLINE: return tr("Online");
case COL_CID: return tr("CID");
+ case COL_UNIVERSE: return tr("Universe");
case COL_PRIO: return tr("Priority");
case COL_SYNC: return tr("Sync");
case COL_PREVIEW: return tr("Preview");
case COL_IP: return tr("IP Address");
case COL_FPS: return tr("FPS");
+ case COL_TIME_SUMMARY: return tr("Times (<%1 >%2 >%3)")
+ .arg(std::chrono::milliseconds(m_shortInterval).count())
+ .arg(std::chrono::milliseconds(m_longInterval).count())
+ .arg(std::chrono::milliseconds(m_staticInterval).count());
case COL_SEQ_ERR: return tr("SeqErr");
case COL_JUMPS: return tr("Jumps");
case COL_VER: return tr("Ver");
@@ -211,7 +248,8 @@ QVariant SACNSourceTableModel::headerData(int section, Qt::Orientation orientati
case COL_NAME: return tr("The human readable name the source has been given");
case COL_ONLINE: return tr("Online status of the source");
case COL_CID: return tr("The Component IDentifier of the source, a Universally Unique Identifier");
- case COL_PRIO: return tr("Source priority 0 (ignore) to 200 (most important)");
+ case COL_UNIVERSE: return tr("sACN Universe number (%1-%1)").arg(MIN_SACN_UNIVERSE).arg(MAX_SACN_UNIVERSE);
+ case COL_PRIO: return tr("Source priority %1 (ignore) to %2 (most important). Default %1").arg(MIN_SACN_PRIORITY).arg(MAX_SACN_PRIORITY).arg(DEFAULT_SACN_PRIORITY);
case COL_SYNC: return tr("Does the source support Universe Synchronization?");
case COL_PREVIEW: return tr("Indicates that the data in this packet is intended for use in visualization or media server preview applications and shall not be used to generate live output.");
case COL_IP: return tr("The IP address of the source");
@@ -229,6 +267,39 @@ QVariant SACNSourceTableModel::headerData(int section, Qt::Orientation orientati
return QVariant();
}
+void SACNSourceTableModel::setShortInterval(int millisec)
+{
+ if (millisec == shortInterval())
+ return;
+
+ m_shortInterval = std::chrono::milliseconds(millisec);
+
+ emit headerDataChanged(Qt::Horizontal, COL_TIME_SUMMARY, COL_TIME_SUMMARY);
+ emit dataChanged(index(0, COL_TIME_SUMMARY), index(rowCount() - 1, COL_TIME_SUMMARY));
+}
+
+void SACNSourceTableModel::setLongInterval(int millisec)
+{
+ if (millisec == longInterval())
+ return;
+
+ m_longInterval = std::chrono::milliseconds(millisec);
+
+ emit headerDataChanged(Qt::Horizontal, COL_TIME_SUMMARY, COL_TIME_SUMMARY);
+ emit dataChanged(index(0, COL_TIME_SUMMARY), index(rowCount() - 1, COL_TIME_SUMMARY));
+}
+
+void SACNSourceTableModel::setStaticInterval(int millisec)
+{
+ if (millisec == longInterval())
+ return;
+
+ m_staticInterval = std::chrono::milliseconds(millisec);
+
+ emit headerDataChanged(Qt::Horizontal, COL_TIME_SUMMARY, COL_TIME_SUMMARY);
+ emit dataChanged(index(0, COL_TIME_SUMMARY), index(rowCount() - 1, COL_TIME_SUMMARY));
+}
+
void SACNSourceTableModel::addListener(const sACNManager::tListener& listener)
{
if (!listener)
@@ -247,35 +318,69 @@ void SACNSourceTableModel::addListener(const sACNManager::tListener& listener)
m_listeners.push_back(listener);
}
-void SACNSourceTableModel::removeListener(const sACNManager::tListener& listener)
+void SACNSourceTableModel::pause()
{
- if (!listener)
- return;
- disconnect(listener.data(), nullptr, this, nullptr);
-
- // TODO: Remove sources from this listener
+ // Stop listening (queued events will still happen)
+ for (size_t i = 0; i < m_listeners.size(); ++i)
+ {
+ sACNManager::tListener listener(m_listeners[i]);
+ if (listener)
+ disconnect(listener.data(), nullptr, this, nullptr);
+ }
}
-void SACNSourceTableModel::clear()
+void SACNSourceTableModel::restart()
{
- // Stop listening for new sources
- for (size_t i = 0; i < m_listeners.size(); ++i)
+ // Restart listening on all still extant universes
+ for (auto it = m_listeners.begin(); it != m_listeners.end(); /**/)
{
- disconnect(m_listeners[i].data(), nullptr, this, nullptr);
+ sACNManager::tListener listener(*it);
+ if (listener)
+ {
+ connect(listener.data(), &sACNListener::sourceFound, this, &SACNSourceTableModel::sourceOnline);
+ connect(listener.data(), &sACNListener::sourceLost, this, &SACNSourceTableModel::sourceChanged);
+ connect(listener.data(), &sACNListener::sourceChanged, this, &SACNSourceTableModel::sourceChanged);
+ ++it;
+ }
+ else
+ {
+ // Remove nulled weak pointers
+ it = m_listeners.erase(it);
+ }
}
+}
+
+void SACNSourceTableModel::clear()
+{
+ pause();
m_listeners.clear();
+ // Clear the model
beginResetModel();
m_rows.clear();
+ m_sourceToTableRow.clear();
endResetModel();
}
+void SACNSourceTableModel::resetTimeSummaryCounters()
+{
+ for (auto it = m_sourceToTableRow.begin(); it != m_sourceToTableRow.end(); ++it)
+ {
+ it.key()->fpscounter.ClearHistogram();
+ }
+ emit dataChanged(index(0, COL_TIME_SUMMARY), index(rowCount() - 1, COL_TIME_SUMMARY));
+}
+
void SACNSourceTableModel::resetSequenceCounters()
{
for (auto it = m_sourceToTableRow.begin(); it != m_sourceToTableRow.end(); ++it)
{
it.key()->resetSeqErr();
}
+ for (auto& row : m_rows)
+ {
+ row.seq_err = 0;
+ }
emit dataChanged(index(0, COL_SEQ_ERR), index(rowCount() - 1, COL_SEQ_ERR));
}
@@ -285,6 +390,10 @@ void SACNSourceTableModel::resetJumpsCounters()
{
it.key()->resetJumps();
}
+ for (auto& row : m_rows)
+ {
+ row.jumps = 0;
+ }
emit dataChanged(index(0, COL_JUMPS), index(rowCount() - 1, COL_JUMPS));
}
@@ -294,8 +403,14 @@ void SACNSourceTableModel::resetCounters()
{
it.key()->resetSeqErr();
it.key()->resetJumps();
+ it.key()->fpscounter.ClearHistogram();
+ }
+ for (auto& row : m_rows)
+ {
+ row.seq_err = 0;
+ row.jumps = 0;
}
- emit dataChanged(index(0, COL_SEQ_ERR), index(rowCount() - 1, COL_JUMPS));
+ emit dataChanged(index(0, COL_TIME_SUMMARY), index(rowCount() - 1, COL_JUMPS));
}
void SACNSourceTableModel::sourceChanged(sACNSource* source)
@@ -312,7 +427,7 @@ void SACNSourceTableModel::sourceChanged(sACNSource* source)
// Update and signal
m_rows[row_num].Update(source);
- emit dataChanged(index(row_num, 0), index(row_num, COL_END));
+ emit dataChanged(index(row_num, 0), index(row_num, COL_END - 1));
}
void SACNSourceTableModel::sourceOnline(sACNSource* source)
@@ -339,6 +454,7 @@ void SACNSourceTableModel::RowData::Update(const sACNSource* source)
name = source->name;
cid = source->src_cid;
+ universe = source->universe;
protocol_version = source->protocol_version;
ip = source->ip;
fps = source->fpscounter.FPS();
@@ -378,4 +494,5 @@ void SACNSourceTableModel::RowData::Update(const sACNSource* source)
priority = source->priority;
preview = source->isPreview;
per_address = source->doing_per_channel;
+ histogram = source->fpscounter.GetHistogram();
}
diff --git a/src/models/sacnsourcetablemodel.h b/src/models/sacnsourcetablemodel.h
index d52494ae..0454782a 100644
--- a/src/models/sacnsourcetablemodel.h
+++ b/src/models/sacnsourcetablemodel.h
@@ -29,11 +29,13 @@ class SACNSourceTableModel : public QAbstractTableModel
COL_NAME,
COL_ONLINE,
COL_CID,
+ COL_UNIVERSE,
COL_PRIO,
COL_SYNC,
COL_PREVIEW,
COL_IP,
COL_FPS,
+ COL_TIME_SUMMARY,
COL_SEQ_ERR,
COL_JUMPS,
COL_VER,
@@ -54,12 +56,25 @@ class SACNSourceTableModel : public QAbstractTableModel
QVariant data(const QModelIndex& index, int role = Qt::DisplayRole) const override;
QVariant headerData(int section, Qt::Orientation orientation, int role = Qt::DisplayRole) const override;
+ // Time interval summary
+ Q_SLOT void setShortInterval(int millisec);
+ int shortInterval() const { return m_shortInterval.count(); }
+ Q_SLOT void setLongInterval(int millisec);
+ int longInterval() const { return m_longInterval.count(); }
+ Q_SLOT void setStaticInterval(int millisec);
+ int staticInterval() const { return m_staticInterval.count(); }
+
+ // Add a listener. Does not take ownership
void addListener(const sACNManager::tListener& listener);
- void removeListener(const sACNManager::tListener& listener);
- // Remove all listeners and sources
+ // Stop updating
+ void pause();
+ // Restart updates of the existing list of sources and listeners
+ void restart();
+ // Clear all data and remove all listeners
void clear();
// Convenience
+ void resetTimeSummaryCounters();
void resetSequenceCounters();
void resetJumpsCounters();
void resetCounters();
@@ -98,23 +113,33 @@ private Q_SLOTS:
StreamingACNProtocolVersion protocol_version = sACNProtocolUnknown;
QHostAddress ip;
float fps = 0;
- int seq_err = 0;
- int jumps = 0;
+ unsigned int seq_err = 0;
+ unsigned int jumps = 0;
SourceState online = SourceState::Offline;
SourceSecure security = SourceSecure::None;
+ uint16_t universe = 0;
uint16_t sync_universe = 0;
uint16_t slot_count = 0;
uint8_t priority = 0;
bool preview = false;
bool per_address = false;
+ FpsCounter::Histogram histogram;
void Update(const sACNSource* source);
};
std::vector m_rows;
- std::vector m_listeners;
+ std::vector m_listeners;
+
+ // Interval shorter than expected
+ FpsCounter::HistogramBucket m_shortInterval = std::chrono::milliseconds(19);
+ // Interval longer than expected
+ FpsCounter::HistogramBucket m_longInterval = std::chrono::milliseconds(25);
+ // Is either a static level or something has gone very wrong
+ FpsCounter::HistogramBucket m_staticInterval = std::chrono::milliseconds(500);
// Data
QVariant getDisplayData(const RowData& rowData, int column) const;
QVariant getBackgroundData(const RowData& rowData, int column) const;
+ QVariant getTimingSummary(const RowData& rowData) const;
};
diff --git a/src/sacn/ACNShare/tock.cpp b/src/sacn/ACNShare/tock.cpp
index 43f76cce..7278623f 100644
--- a/src/sacn/ACNShare/tock.cpp
+++ b/src/sacn/ACNShare/tock.cpp
@@ -58,12 +58,6 @@ void Tock_StopLib()
tock::tock():v(0) {}
-template
-tock::tock(std::chrono::duration duration)
-{
- v = duration;
-}
-
tock::resolution_t tock::Get() const
{
return v;
diff --git a/src/sacn/ACNShare/tock.h b/src/sacn/ACNShare/tock.h
index aa474980..fe85cb1a 100644
--- a/src/sacn/ACNShare/tock.h
+++ b/src/sacn/ACNShare/tock.h
@@ -68,7 +68,7 @@ class tock
tock& operator=(tock&&) = default;
template
- tock(std::chrono::duration duration);
+ tock(std::chrono::duration duration) { v = duration; }
//Returns the number of nanoseconds that this tock represents
resolution_t Get() const;
diff --git a/src/sacn/fpscounter.cpp b/src/sacn/fpscounter.cpp
index ea13b9ed..3feea1ff 100644
--- a/src/sacn/fpscounter.cpp
+++ b/src/sacn/fpscounter.cpp
@@ -1,85 +1,105 @@
#include "fpscounter.h"
-#define updateInterval 1000
+#include "streamingacn.h"
-FpsCounter::FpsCounter(QObject *parent) : QObject(parent)
+constexpr std::chrono::milliseconds updateInterval(1000);
+
+FpsCounter::FpsCounter(QObject* parent) : QObject(parent)
{
- // Maximum fps permitted by standard is 44, so this should never reallocate
- frameTimes.reserve(50);
- elapsedTimer.start();
- timerId = startTimer(updateInterval);
+ // Maximum fps permitted by standard is 44, so this should never reallocate
+ m_frameTimes.reserve(127);
+ m_timerId = startTimer(updateInterval.count());
}
FpsCounter::~FpsCounter()
{
- if (timerId != 0)
- killTimer(timerId);
+ if (m_timerId != 0)
+ killTimer(m_timerId);
}
-void FpsCounter::timerEvent(QTimerEvent * /*e*/)
+void FpsCounter::timerEvent(QTimerEvent* /*ev*/)
{
- QMutexLocker queueLocker(&queueMutex);
+ QMutexLocker queueLocker(&m_queueMutex);
- // We need at least two frames to calculate interval
- if (Q_UNLIKELY(frameTimes.count() < 2))
+ // We need at least two frames to calculate interval
+ if (Q_UNLIKELY(m_frameTimes.size() < 2))
+ {
+ // No frames, or very old single frame
+ if (
+ m_frameTimes.empty() ||
+ ((m_frameTimes.size() == 1) && (sACNManager::GetTock().Get() > (m_frameTimes.back() + (updateInterval * 2))))
+ )
{
- // No frames, or very old single frame
- if (
- (frameTimes.count() == 0) ||
- ((frameTimes.count() == 1) && (elapsedTimer.elapsed() > (frameTimes.back() + (updateInterval * 2))))
- )
- {
- previousFps = currentFps;
- currentFps = 0;
- }
- } else {
- // Calculate average of the intervals
- qint64 intervalTotal = 0;
- qint64 intervalCount = 0;
+ m_previousFps = m_currentFps;
+ m_currentFps = 0;
+ }
+ }
+ else
+ {
+ // Calculate average of the intervals
+ tock::resolution_t intervalTotal{0};
+ int64_t intervalCount = 0;
- int frameIndex = 0;
- if (lastFrameTime == 0)
- {
- lastFrameTime = frameTimes[0];
- frameIndex = 1;
- }
+ tock::resolution_t lastFrameTime = m_frameTimes.front();
+
+ for (size_t frameIndex = 1; frameIndex < m_frameTimes.size(); ++frameIndex)
+ {
+ const auto& time = m_frameTimes[frameIndex];
- for (/* init above */ ; frameIndex < frameTimes.count() ; ++frameIndex)
+ if (time > lastFrameTime)
+ {
+ const auto interval = time - lastFrameTime;
+ lastFrameTime = time;
+ // Ignore very long intervals
+ if (interval < (updateInterval * 2))
{
- auto time = frameTimes[frameIndex];
-
- if (time > lastFrameTime)
- {
- auto interval = time - lastFrameTime;
- lastFrameTime = time;
- intervalTotal += interval;
- ++intervalCount;
- }
+ // Add to histogram
+ ++m_frameDeltaHistogram[std::chrono::ceil(interval)];
+ // Add to total
+ intervalTotal += interval;
+ ++intervalCount;
}
+ }
+ }
- frameTimes.clear();
+ if (intervalCount == 0)
+ return;
- if (intervalCount == 0)
- return;
+ m_frameTimes.clear();
+ m_frameTimes.push_back(lastFrameTime);
- auto intervalAvg = intervalTotal / intervalCount;
+ const auto intervalAvg = intervalTotal / intervalCount;
- // Calculate the current FPS
- previousFps = currentFps;
- currentFps = 1000 / static_cast(intervalAvg);
- }
+ // Calculate the current FPS
+ m_previousFps = m_currentFps;
+ m_currentFps = 1.0f / std::chrono::duration(intervalAvg).count();
+ }
+
+ // Flag if the FPS has changed
+ m_newFps = (m_previousFps < m_currentFps) || (m_previousFps > m_currentFps);
+
+ queueLocker.unlock();
+
+ if (m_newFps)
+ {
+ emit updatedFPS();
+ }
+}
- // Flag if the FPS has changed
- newFps = ((previousFps < currentFps) | (previousFps > currentFps));
+FpsCounter::Histogram FpsCounter::GetHistogram() const
+{
+ QMutexLocker queueLocker(&m_queueMutex);
+ return m_frameDeltaHistogram;
+}
- if (newFps)
- emit updatedFPS();
+void FpsCounter::ClearHistogram()
+{
+ QMutexLocker queueLocker(&m_queueMutex);
+ m_frameDeltaHistogram.clear();
}
-void FpsCounter::newFrame()
+void FpsCounter::newFrame(tock timePoint)
{
- qint64 elapsedTime = elapsedTimer.elapsed();
- QMutexLocker queueLocker(&queueMutex);
- frameTimes.append(elapsedTime);
- // TODO: Consider using an IIR filter to produce a rolling average
+ QMutexLocker queueLocker(&m_queueMutex);
+ m_frameTimes.push_back(timePoint.Get());
}
diff --git a/src/sacn/fpscounter.h b/src/sacn/fpscounter.h
index a5eb1827..f336b090 100644
--- a/src/sacn/fpscounter.h
+++ b/src/sacn/fpscounter.h
@@ -2,43 +2,55 @@
#define FPSCounter_H
#include
-#include
-#include
#include
+#include "tock.h"
+
+#include
+
class FpsCounter : public QObject
{
- Q_OBJECT
+ Q_OBJECT
public:
- explicit FpsCounter(QObject *parent = nullptr);
- ~FpsCounter();
+ // Histogram bucket size
+ using HistogramBucket = std::chrono::milliseconds;
+ using Histogram = std::map;
+
+public:
+ explicit FpsCounter(QObject* parent = nullptr);
+ ~FpsCounter();
+
+ // Return current FPS
+ float FPS() const { m_newFps = false; return m_currentFps; }
- // Return current FPS
- float FPS() const { newFps = false; return currentFps;}
+ // Returns true if FPS has changed since last checked
+ bool isNewFPS() const { return m_newFps; }
- // Returns true if FPS has changed since last checked
- bool isNewFPS() const { return newFps; }
+ // Get a copy of the frame time histogram
+ Histogram GetHistogram() const;
+
+ // Clear the histogram
+ void ClearHistogram();
- // Log new frame
- void newFrame();
+ // Log new frame
+ void newFrame(tock timePoint);
signals:
- void updatedFPS();
+ void updatedFPS();
protected:
- void timerEvent(QTimerEvent *e) final;
+ void timerEvent(QTimerEvent* ev) final;
private:
- QElapsedTimer elapsedTimer;
- int timerId = 0;
+ int m_timerId = 0;
- float currentFps = 0.0f;
- float previousFps = 0.0f;
- mutable bool newFps = false;
+ float m_currentFps = 0.0f;
+ float m_previousFps = 0.0f;
+ mutable bool m_newFps = false;
- QMutex queueMutex;
- QVector frameTimes;
- qint64 lastFrameTime = 0;
+ mutable QMutex m_queueMutex;
+ std::vector m_frameTimes;
+ Histogram m_frameDeltaHistogram;
};
#endif // FPSCounter_H
diff --git a/src/sacn/sacndiscovery.cpp b/src/sacn/sacndiscovery.cpp
index c5e50257..33bc6140 100644
--- a/src/sacn/sacndiscovery.cpp
+++ b/src/sacn/sacndiscovery.cpp
@@ -221,7 +221,7 @@ void sACNDiscoveryRX::timeoutUniverses()
}
}
-void sACNDiscoveryRX::processPacket(quint8* pbuf, uint buflen)
+void sACNDiscoveryRX::processPacket(const quint8* pbuf, uint buflen)
{
bool flag1, flag2, flag3;
quint32 length;
diff --git a/src/sacn/sacndiscovery.h b/src/sacn/sacndiscovery.h
index 284ab594..53656bad 100644
--- a/src/sacn/sacndiscovery.h
+++ b/src/sacn/sacndiscovery.h
@@ -68,7 +68,7 @@ class sACNDiscoveryRX : public QObject
typedef QHash tDiscoveryList;
- void processPacket(quint8* pbuf, uint buflen);
+ void processPacket(const quint8* pbuf, uint buflen);
const tDiscoveryList &getDiscoveryList() const { return m_discoveryList; }
signals:
diff --git a/src/sacn/sacnlistener.cpp b/src/sacn/sacnlistener.cpp
index 62b6bfd0..b7d0ad3e 100644
--- a/src/sacn/sacnlistener.cpp
+++ b/src/sacn/sacnlistener.cpp
@@ -40,776 +40,777 @@
//Background merge interval
#define BACKGROUND_MERGE 500
-sACNListener::sACNListener(int universe, QObject *parent)
- : QObject(parent)
- , m_merged_levels(DMX_SLOT_MAX, sACNMergedAddress())
- , m_universe(universe)
+sACNListener::sACNListener(int universe, QObject* parent)
+ : QObject(parent)
+ , m_merged_levels(DMX_SLOT_MAX, sACNMergedAddress())
+ , m_universe(universe)
{
- qRegisterMetaType("QHostAddress");
+ qRegisterMetaType("QHostAddress");
+ m_current_levels.fill(-1);
+ m_current_priorities.fill(-1);
}
sACNListener::~sACNListener()
{
- m_initalSampleTimer->deleteLater();
- m_mergeTimer->deleteLater();
- qDeleteAll(m_sockets);
- qDebug() << "sACNListener" << QThread::currentThreadId() << ": stopping";
+ m_initalSampleTimer->deleteLater();
+ m_mergeTimer->deleteLater();
+ qDeleteAll(m_sockets);
+ qDebug() << this << ": stopping";
}
void sACNListener::startReception()
{
- qDebug() << "sACNListener" << QThread::currentThreadId() << ": Starting universe" << m_universe;
-
- // Clear the levels array
- std::fill(std::begin(m_last_levels), std::end(m_last_levels), -1);
-
- if (Preferences::Instance().GetNetworkListenAll() && !Preferences::Instance().networkInterface().flags().testFlag(QNetworkInterface::IsLoopBack)) {
- // Listen on ALL interfaces and not working offline
- for (const auto &interface : QNetworkInterface::allInterfaces())
- {
- // If the interface is ok for use...
- if(Preferences::Instance().interfaceSuitable(interface))
- {
- startInterface(interface);
- }
- }
- } else {
- // Listen only to selected interface
- startInterface(Preferences::Instance().networkInterface());
- if (!m_sockets.empty()) {
- m_bindStatus.multicast = (m_sockets.back()->multicastInterface().isValid()) ? sACNRxSocket::BIND_OK : sACNRxSocket::BIND_FAILED;
- m_bindStatus.unicast = (m_sockets.back()->state() == QAbstractSocket::BoundState) ? sACNRxSocket::BIND_OK : sACNRxSocket::BIND_FAILED;
- }
+ qDebug() << this << ": Starting universe" << m_universe;
+ m_last_levels.fill(-1);
+ m_last_priorities.fill(-1);
+ m_current_levels.fill(-1);
+ m_current_priorities.fill(-1);
+
+ if (Preferences::Instance().GetNetworkListenAll() && !Preferences::Instance().networkInterface().flags().testFlag(QNetworkInterface::IsLoopBack)) {
+ // Listen on ALL interfaces and not working offline
+ for (const auto& interface : QNetworkInterface::allInterfaces())
+ {
+ // If the interface is ok for use...
+ if (Preferences::Instance().interfaceSuitable(interface))
+ {
+ startInterface(interface);
+ }
}
-
- // Start intial sampling
- m_initalSampleTimer = new QTimer(this);
- m_initalSampleTimer->setSingleShot(true);
- m_initalSampleTimer->setInterval(SAMPLE_TIME);
- connect(m_initalSampleTimer, &QTimer::timeout, this, &sACNListener::sampleExpiration, Qt::DirectConnection);
- m_initalSampleTimer->start();
-
- // Merge is performed whenever a packet arrives and every BACKGROUND_MERGE interval
- m_elapsedTime.start();
- m_mergesPerSecondTimer.start();
- m_mergeTimer = new QTimer(this);
- m_mergeTimer->setInterval(BACKGROUND_MERGE);
- connect(m_mergeTimer, &QTimer::timeout, this, &sACNListener::performMerge, Qt::DirectConnection);
- connect(m_mergeTimer, &QTimer::timeout, this, &sACNListener::checkSourceExpiration, Qt::DirectConnection);
- m_mergeTimer->start();
-
- // Everything is set
- emit listenerStarted(m_universe);
+ }
+ else {
+ // Listen only to selected interface
+ startInterface(Preferences::Instance().networkInterface());
+ if (!m_sockets.empty()) {
+ m_bindStatus.multicast = (m_sockets.back()->multicastInterface().isValid()) ? sACNRxSocket::BIND_OK : sACNRxSocket::BIND_FAILED;
+ m_bindStatus.unicast = (m_sockets.back()->state() == QAbstractSocket::BoundState) ? sACNRxSocket::BIND_OK : sACNRxSocket::BIND_FAILED;
+ }
+ }
+
+ // Start intial sampling
+ m_initalSampleTimer = new QTimer(this);
+ m_initalSampleTimer->setSingleShot(true);
+ m_initalSampleTimer->setInterval(SAMPLE_TIME);
+ connect(m_initalSampleTimer, &QTimer::timeout, this, &sACNListener::sampleExpiration, Qt::DirectConnection);
+ m_initalSampleTimer->start();
+
+ // Merge is performed whenever a packet arrives and every BACKGROUND_MERGE interval
+ m_mergesPerSecondTimer.start();
+ m_mergeTimer = new QTimer(this);
+ m_mergeTimer->setInterval(BACKGROUND_MERGE);
+ connect(m_mergeTimer, &QTimer::timeout, this, &sACNListener::performMerge, Qt::DirectConnection);
+ connect(m_mergeTimer, &QTimer::timeout, this, &sACNListener::checkSourceExpiration, Qt::DirectConnection);
+ m_mergeTimer->start();
+
+ // Everything is set
+ emit listenerStarted(m_universe);
}
-void sACNListener::startInterface(const QNetworkInterface &iface)
+void sACNListener::startInterface(const QNetworkInterface& iface)
{
- m_sockets.push_back(new sACNRxSocket(iface));
- sACNRxSocket::sBindStatus status = m_sockets.back()->bind(m_universe);
- if (status.unicast == sACNRxSocket::BIND_OK || status.multicast == sACNRxSocket::BIND_OK) {
- connect(m_sockets.back(), &QUdpSocket::readyRead, this, &sACNListener::readPendingDatagrams, Qt::DirectConnection);
- } else {
- // Failed to bind
- m_sockets.pop_back();
- }
-
- if ((m_bindStatus.unicast == sACNRxSocket::BIND_UNKNOWN) || (m_bindStatus.unicast == sACNRxSocket::BIND_OK))
- m_bindStatus.unicast = status.unicast;
- if ((m_bindStatus.multicast == sACNRxSocket::BIND_UNKNOWN) || (m_bindStatus.multicast == sACNRxSocket::BIND_OK))
- m_bindStatus.multicast = status.multicast;
+ m_sockets.push_back(new sACNRxSocket(iface));
+ sACNRxSocket::sBindStatus status = m_sockets.back()->bind(m_universe);
+ if (status.unicast == sACNRxSocket::BIND_OK || status.multicast == sACNRxSocket::BIND_OK) {
+ connect(m_sockets.back(), &QUdpSocket::readyRead, this, &sACNListener::readPendingDatagrams, Qt::DirectConnection);
+ }
+ else {
+ // Failed to bind
+ m_sockets.pop_back();
+ }
+
+ if ((m_bindStatus.unicast == sACNRxSocket::BIND_UNKNOWN) || (m_bindStatus.unicast == sACNRxSocket::BIND_OK))
+ m_bindStatus.unicast = status.unicast;
+ if ((m_bindStatus.multicast == sACNRxSocket::BIND_UNKNOWN) || (m_bindStatus.multicast == sACNRxSocket::BIND_OK))
+ m_bindStatus.multicast = status.multicast;
}
void sACNListener::sampleExpiration()
{
- m_isSampling = false;
- qDebug() << "sACNListener" << QThread::currentThreadId() << ": Sampling has ended";
+ m_isSampling = false;
+ qDebug() << this << ": Sampling has ended";
}
void sACNListener::checkSourceExpiration()
{
- char cidstr [CID::CIDSTRINGBYTES];
- for(std::vector::iterator it = m_sources.begin(); it != m_sources.end(); ++it)
+ char cidstr[CID::CIDSTRINGBYTES];
+ for (std::vector::iterator it = m_sources.begin(); it != m_sources.end(); ++it)
+ {
+ if ((*it)->src_valid)
{
- if((*it)->src_valid)
- {
- if((*it)->active.Expired() && (*it)->priority_wait.Expired())
- {
- (*it)->src_valid = false;
- (*it)->src_stable = false;
- CID::CIDIntoString((*it)->src_cid, cidstr);
- emit sourceLost(*it);
- m_mergeAll = true;
- qDebug() << "Lost source " << cidstr;
- }
- else if ((*it)->doing_per_channel && (*it)->priority_wait.Expired())
- {
- CID::CIDIntoString((*it)->src_cid, cidstr);
- (*it)->doing_per_channel = false;
- emit sourceChanged(*it);
- m_mergeAll = true;
- qDebug() << "sACNListener" << QThread::currentThreadId() << ": Source stopped sending per-channel priority" << cidstr;
- }
- }
+ if ((*it)->active.Expired() && (*it)->priority_wait.Expired())
+ {
+ (*it)->src_valid = false;
+ (*it)->src_stable = false;
+ CID::CIDIntoString((*it)->src_cid, cidstr);
+ emit sourceLost(*it);
+ m_mergeAll = true;
+ qDebug() << "Lost source " << cidstr;
+ }
+ else if ((*it)->doing_per_channel && (*it)->priority_wait.Expired())
+ {
+ CID::CIDIntoString((*it)->src_cid, cidstr);
+ (*it)->doing_per_channel = false;
+ emit sourceChanged(*it);
+ m_mergeAll = true;
+ qDebug() << this << ": Source stopped sending per-channel priority" << cidstr;
+ }
}
+ }
}
void sACNListener::readPendingDatagrams()
{
- #if (QT_VERSION == QT_VERSION_CHECK(5, 9, 3))
- #error "QT5.9.3 QUdpSocket::readDatagram Returns incorrect infomation: https://bugreports.qt.io/browse/QTBUG-64784"
- #endif
- #if (QT_VERSION == QT_VERSION_CHECK(5, 10, 0))
- #error "QT5.10.0 QUdpSocket::readDatagram Returns incorrect infomation: https://bugreports.qt.io/browse/QTBUG-65099"
- #endif
-
- // Check all sockets
- for (auto m_socket : m_sockets)
+#if (QT_VERSION == QT_VERSION_CHECK(5, 9, 3))
+#error "QT5.9.3 QUdpSocket::readDatagram Returns incorrect infomation: https://bugreports.qt.io/browse/QTBUG-64784"
+#endif
+#if (QT_VERSION == QT_VERSION_CHECK(5, 10, 0))
+#error "QT5.10.0 QUdpSocket::readDatagram Returns incorrect infomation: https://bugreports.qt.io/browse/QTBUG-65099"
+#endif
+
+ // Check all sockets
+ for (auto m_socket : m_sockets)
+ {
+ while (m_socket->hasPendingDatagrams())
{
- while(m_socket->hasPendingDatagrams())
- {
- QNetworkDatagram datagram = m_socket->receiveDatagram();
-
- if (datagram.data().isEmpty())
- break;
-
- /* Localhost - Allowed
- * Relevant Multicast - Allowed
- * Unicast for this interface - Allowed (Universe checked later)
- * Broadcast - Rejected
- */
- if (datagram.destinationAddress().isBroadcast())
- break;
-
- QList interfaceAddress;
- for (const auto &address : m_socket->getBoundInterface().addressEntries())
- interfaceAddress << address.ip();
-
- if (
- // Relevant Multicast
- (datagram.destinationAddress().isMulticast() && datagram.destinationAddress() == m_socket->getMulticastAddr())
- ||
- // Unicast for this interface
- interfaceAddress.contains(QHostAddress(datagram.destinationAddress()))
- )
- {
- processDatagram(
- datagram.data(),
- datagram.destinationAddress(),
- datagram.senderAddress());
- }
- }
+ QNetworkDatagram datagram = m_socket->receiveDatagram();
+
+ if (datagram.data().isEmpty())
+ break;
+
+ /* Localhost - Allowed
+ * Relevant Multicast - Allowed
+ * Unicast for this interface - Allowed (Universe checked later)
+ * Broadcast - Rejected
+ */
+ if (datagram.destinationAddress().isBroadcast())
+ break;
+
+ QList interfaceAddress;
+ for (const auto& address : m_socket->getBoundInterface().addressEntries())
+ interfaceAddress << address.ip();
+
+ if (
+ // Relevant Multicast
+ (datagram.destinationAddress().isMulticast() && datagram.destinationAddress() == m_socket->getMulticastAddr())
+ ||
+ // Unicast for this interface
+ interfaceAddress.contains(QHostAddress(datagram.destinationAddress()))
+ )
+ {
+ processDatagram(
+ datagram.data(),
+ datagram.destinationAddress(),
+ datagram.senderAddress());
+ }
}
+ }
}
-void sACNListener::processDatagram(const QByteArray &data, const QHostAddress &destination, const QHostAddress &sender)
+void sACNListener::processDatagram(const QByteArray& data, const QHostAddress& destination, const QHostAddress& sender)
{
- if(QThread::currentThread()!=this->thread())
+ if (QThread::currentThread() != this->thread())
+ {
+ QMetaObject::invokeMethod(
+ this,
+ "processDatagram",
+ Q_ARG(QByteArray, data),
+ Q_ARG(QHostAddress, destination),
+ Q_ARG(QHostAddress, sender));
+ return;
+ };
+
+ const tock packet_tock = sACNManager::GetTock();
+
+ QMutexLocker locker(&m_processMutex);
+
+ // Process packet
+ quint32 root_vector = 0;
+ CID source_cid;
+ quint8 start_code = 0;
+ quint8 sequence = 0;
+ quint16 universe = 0;
+ quint16 slot_count = 0;
+ const quint8* pdata = nullptr;
+ char source_name[SOURCE_NAME_SIZE] = {};
+ quint8 priority = 0;
+ /*
+ * These only apply to the ratified version of the spec, so we will hardwire
+ * them to defaults just in case they never get set.
+ */
+ quint16 synchronization = NOT_SYNCHRONIZED_VALUE; // E1.31:2018
+ quint8 options = NO_OPTIONS_VALUE;
+
+ const e_ValidateStreamHeader streamHeaderVersion = ValidateStreamHeader(reinterpret_cast(data.data()), data.length(), root_vector, source_cid, source_name, priority,
+ start_code, synchronization, sequence, options, universe, slot_count, pdata);
+
+ switch (streamHeaderVersion)
+ {
+ case e_ValidateStreamHeader::StreamHeader_Invalid:
+ // Recieved a packet but not valid. Log and discard
+ qDebug() << this << ": Invalid Packet";
+ return;
+
+ case e_ValidateStreamHeader::StreamHeader_Draft:
+ case e_ValidateStreamHeader::StreamHeader_Ratified:
+ break;
+
+ case e_ValidateStreamHeader::StreamHeader_Extended:
+ {
+ quint32 vector;
+ if (static_cast(data.length()) > ROOT_VECTOR_ADDR + sizeof(vector))
{
- QMetaObject::invokeMethod(
- this,
- "processDatagram",
- Q_ARG(QByteArray, data),
- Q_ARG(QHostAddress, destination),
- Q_ARG(QHostAddress, sender));
- return;
- };
-
- QMutexLocker locker(&m_processMutex);
-
- // Process packet
- quint32 root_vector;
- CID source_cid;
- quint8 start_code;
- quint8 sequence;
- quint16 universe;
- quint16 slot_count;
- quint8* pdata;
- char source_name [SOURCE_NAME_SIZE];
- quint8 priority;
- /*
- * These only apply to the ratified version of the spec, so we will hardwire
- * them to be 0 just in case they never get set.
- */
- quint16 synchronization = NOT_SYNCHRONIZED_VALUE;
- quint8 options = NO_OPTIONS_VALUE;
- bool preview = false;
-
- switch (ValidateStreamHeader((quint8*)data.data(), data.length(), root_vector, source_cid, source_name, priority,
- start_code, synchronization, sequence, options, universe, slot_count, pdata))
- {
- case e_ValidateStreamHeader::StreamHeader_Invalid:
- // Recieved a packet but not valid. Log and discard
- qDebug() << "sACNListener" << QThread::currentThreadId() << ": Invalid Packet";
- return;
-
- case e_ValidateStreamHeader::StreamHeader_Unknown:
- qDebug() << "sACNListener" << QThread::currentThreadId() << ": Unkown Root Vector";
- return;
-
- case e_ValidateStreamHeader::StreamHeader_Extended:
- quint32 vector;
- if (static_cast(data.length()) > ROOT_VECTOR_ADDR + sizeof(vector))
- {
- vector = UpackBUint32((quint8*)data.data() + FRAMING_VECTOR_ADDR);
- switch (vector)
- {
- case VECTOR_E131_EXTENDED_DISCOVERY:
- sACNDiscoveryRX::getInstance()->processPacket((quint8*)data.data(), data.length());
- break;
-
- case VECTOR_E131_EXTENDED_SYNCHRONIZATION:
- sACNSynchronizationRX::getInstance()->processPacket((quint8*)data.data(), data.length(), destination, sender);
- break;
-
- default:
- qDebug() << "sACNListener" << QThread::currentThreadId() << ": Unknown Extended Packet";
- }
- }
- return;
-
- case e_ValidateStreamHeader::StreamHeader_Pathway_Secure:
- if (!Preferences::Instance().GetPathwaySecureRx())
- {
- qDebug() << "sACNListener" << QThread::currentThreadId() << ": Ignore Pathway secure";
- return;
- }
+ vector = UpackBUint32(reinterpret_cast(data.data()) + FRAMING_VECTOR_ADDR);
+ switch (vector)
+ {
+ case VECTOR_E131_EXTENDED_DISCOVERY:
+ sACNDiscoveryRX::getInstance()->processPacket(reinterpret_cast(data.data()), data.length());
+ break;
- default:
+ case VECTOR_E131_EXTENDED_SYNCHRONIZATION:
+ sACNSynchronizationRX::getInstance()->processPacket(reinterpret_cast(data.data()), data.length(), destination, sender);
break;
- }
- // Wrong universe
- if(m_universe != universe)
- {
- // Was it unicast? Send to correct listener (if listening)
- if (!destination.isMulticast() && !destination.isBroadcast())
- {
- // Unicast, send to correct listener!
- decltype(sACNManager::Instance().getListenerList()) listenerList
- = sACNManager::Instance().getListenerList();
- if (listenerList.contains(universe))
- listenerList[universe].toStrongRef()->processDatagram(data, destination, sender);
- return;
- } else {
- // Log and discard
- qDebug() << "sACNListener" << QThread::currentThreadId()
- << ": Rejecting universe" << universe << "sent to" << destination;
- return;
- }
+ default:
+ qDebug() << this << ": Unknown Extended Packet";
+ }
}
+ } return;
- // Listen to preview?
- preview = (PREVIEW_DATA_OPTION == (options & PREVIEW_DATA_OPTION));
- if ((preview) && !Preferences::Instance().GetBlindVisualizer())
+ case e_ValidateStreamHeader::StreamHeader_Pathway_Secure:
+ if (!Preferences::Instance().GetPathwaySecureRx())
{
- qDebug() << "sACNListener" << QThread::currentThreadId() << ": Ignore preview";
- return;
+ qDebug() << this << ": Ignore Pathway secure";
+ return;
}
+ break;
- sACNSource *ps = NULL; // Pointer to the source
- bool foundsource = false;
- bool newsourcenotify = false;
- bool validpacket = true; //whether or not we will actually process the packet
+ default:
+ case e_ValidateStreamHeader::StreamHeader_Unknown:
+ qDebug() << this << ": Unkown Root Vector";
+ return;
+ }
- for(std::vector::iterator it = m_sources.begin(); it != m_sources.end(); ++it)
+ // Packet is now know to contain DMX Level Data
+
+ // Wrong universe
+ if (m_universe != universe)
+ {
+ // Was it unicast? Send to correct listener (if listening)
+ if (!destination.isMulticast() && !destination.isBroadcast())
{
- if((*it)->src_cid == source_cid)
- {
- foundsource = true;
- ps = *it;
-
- // Verify Pathway Secure DMX security features, do this before updating the active flag
- if (root_vector == VECTOR_ROOT_E131_DATA_PATHWAY_SECURE) {
- PathwaySecure::VerifyStreamSecurity(
- (quint8*)data.data(), data.size(),
- Preferences::Instance().GetPathwaySecureRxPassword(),
- *ps);
- } else {
- ps->pathway_secure.passwordOk = false;
- ps->pathway_secure.sequenceOk = false;
- ps->pathway_secure.digestOk = false;
- }
-
- if(!ps->src_valid)
- {
- // This is a source which is coming back online, so we need to repeat the steps
- // for initial source aquisition
- ps->active.SetInterval(std::chrono::milliseconds(E131_NETWORK_DATA_LOSS_TIMEOUT + m_ssHLL));
- ps->lastseq = sequence;
- ps->src_cid = source_cid;
- ps->src_valid = true;
- ps->doing_dmx = (start_code == STARTCODE_DMX);
- ps->doing_per_channel = ps->waited_for_dd = false;
- newsourcenotify = false;
- ps->priority_wait.SetInterval(std::chrono::milliseconds(WAIT_PRIORITY));
- }
-
- if(
- ((root_vector == VECTOR_ROOT_E131_DATA) || root_vector == VECTOR_ROOT_E131_DATA_PATHWAY_SECURE)
- && ((options & STREAM_TERMINATED_OPTION) == STREAM_TERMINATED_OPTION))
- {
- //by setting this flag to false, 0xdd packets that may come in while the terminated data
- //packets come in won't reset the priority_wait timer
- (*it)->waited_for_dd = false;
- if(start_code == STARTCODE_DMX)
- (*it)->doing_dmx = false;
-
- //"Upon receipt of a packet containing this bit set
- //to a value of 1, a receiver shall enter network
- //data loss condition. Any property values in
- //these packets shall be ignored"
- (*it)->active.SetInterval(std::chrono::milliseconds(m_ssHLL)); //We factor in the hold last look time here, rather than 0
-
- if((*it)->doing_per_channel)
- (*it)->priority_wait.SetInterval(std::chrono::milliseconds(m_ssHLL)); //We factor in the hold last look time here, rather than 0
-
- validpacket = false;
- break;
- }
-
- //Based on the start code, update the timers
- if(start_code == STARTCODE_DMX)
- {
- //No matter how valid, we got something -- but we'll tweak the interval for any hll change
- (*it)->doing_dmx = true;
- (*it)->active.SetInterval(std::chrono::milliseconds(E131_NETWORK_DATA_LOSS_TIMEOUT + m_ssHLL));
- }
- else if(start_code == STARTCODE_PRIORITY && (*it)->waited_for_dd)
- {
- (*it)->doing_per_channel = true; //The source could have stopped sending dd for a while.
- (*it)->priority_wait.Reset();
- }
-
- //Validate the sequence number, updating the stored one
- //The two's complement math is to handle rollover, and we're explicitly
- //doing assignment to force the type sizes. A negative number means
- //we got an "old" one, but we assume that anything really old is possibly
- //due the device having rebooted and starting the sequence over.
- qint8 result = ((qint8)sequence) - ((qint8)((*it)->lastseq));
- if(result!=1)
- (*it)->jumps++;
- if((result <= 0) && (result > -20))
- {
- validpacket = false;
- (*it)->seqErr++;
- }
- else
- (*it)->lastseq = sequence;
-
- //This next bit is a little tricky. We want to wait for dd packets (sampling period
- //tweaks aside) and notify them with the dd packet first, but we don't want to do that
- //if we've never seen a dmx packet from the source.
- if(!(*it)->doing_dmx)
- {
- /* For fault finding an installation which only sends 0xdd for a universe
- * (For example: ETC Cobalt does this for, currenlty, unpatched universes with in it's network map)
- * We do want to say that having not sent dimmer data is a valid source/packet
- */
- // validpacket = false;
- (*it)->priority_wait.Reset(); //We don't want to let the priority timer run out
- }
- else if(!(*it)->waited_for_dd && validpacket)
- {
- if(start_code == STARTCODE_PRIORITY)
- {
- (*it)->waited_for_dd = true;
- (*it)->doing_per_channel = true;
- (*it)->priority_wait.SetInterval(std::chrono::milliseconds(E131_NETWORK_DATA_LOSS_TIMEOUT + m_ssHLL));
- newsourcenotify = true;
- }
- else if((*it)->priority_wait.Expired())
- {
- (*it)->waited_for_dd = true;
- (*it)->doing_per_channel = false;
- (*it)->priority_wait.SetInterval(std::chrono::milliseconds(E131_NETWORK_DATA_LOSS_TIMEOUT + m_ssHLL)); //In case the source later decides to sent 0xdd packets
- newsourcenotify = true;
- }
- else
- newsourcenotify = validpacket = false;
- }
-
- //Found the source, and we're ready to process the packet
- }
+ // Unicast, send to correct listener!
+ decltype(sACNManager::Instance().getListenerList()) listenerList
+ = sACNManager::Instance().getListenerList();
+ auto it = listenerList.find(universe);
+ if (it != listenerList.end())
+ it.value().toStrongRef()->processDatagram(data, destination, sender);
+ }
+ else {
+ // Log and discard
+ qDebug() << this << ": Rejecting universe" << universe << "sent to" << destination;
+ }
+ return;
+ }
+
+ // Listen to preview?
+ const bool preview = (PREVIEW_DATA_OPTION == (options & PREVIEW_DATA_OPTION));
+ if (preview && !Preferences::Instance().GetBlindVisualizer())
+ {
+ qDebug() << this << ": Ignore preview";
+ return;
+ }
+
+ sACNSource* ps = nullptr; // Pointer to the source
+ bool newsourcenotify = false;
+ bool validpacket = true; //whether or not we will actually process the packet
+
+ // Find existing known source
+ auto it = std::find_if(m_sources.begin(), m_sources.end(), [source_cid](sACNSource* source) { return source && source->src_cid == source_cid; });
+ if (it != m_sources.end())
+ {
+ ps = (*it);
+
+ // Verify Pathway Secure DMX security features, do this before updating the active flag
+ if (streamHeaderVersion == e_ValidateStreamHeader::StreamHeader_Pathway_Secure) {
+ PathwaySecure::VerifyStreamSecurity(reinterpret_cast(data.data()), data.size(),
+ Preferences::Instance().GetPathwaySecureRxPassword(), *ps);
+ }
+ else {
+ ps->pathway_secure.passwordOk = false;
+ ps->pathway_secure.sequenceOk = false;
+ ps->pathway_secure.digestOk = false;
}
- if(!validpacket)
+ if (!ps->src_valid)
{
- qDebug() << "sACNListener" << QThread::currentThreadId() << ": Source coming up, not processing packet";
- return;
+ // This is a source which is coming back online, so we need to repeat the steps
+ // for initial source aquisition
+ ps->active.SetInterval(std::chrono::milliseconds(E131_NETWORK_DATA_LOSS_TIMEOUT + m_ssHLL));
+ ps->lastseq = sequence;
+ ps->src_valid = true;
+ ps->doing_dmx = (start_code == STARTCODE_DMX);
+ ps->doing_per_channel = ps->waited_for_dd = false;
+ newsourcenotify = false;
+ ps->priority_wait.SetInterval(std::chrono::milliseconds(WAIT_PRIORITY));
}
- if(!foundsource) //Add a new source to the list
+ if ((options & STREAM_TERMINATED_OPTION) == STREAM_TERMINATED_OPTION)
{
- ps = new sACNSource();
+ // Source is terminating
- m_sources.push_back(ps);
+ //by setting this flag to false, 0xdd packets that may come in while the terminated data
+ //packets come in won't reset the priority_wait timer
+ ps->waited_for_dd = false;
+ if (start_code == STARTCODE_DMX)
+ ps->doing_dmx = false;
- ps->name = QString::fromUtf8(source_name);
- ps->ip = sender;
- ps->universe = universe;
- ps->synchronization = synchronization;
+ //"Upon receipt of a packet containing this bit set
+ //to a value of 1, a receiver shall enter network
+ //data loss condition. Any property values in
+ //these packets shall be ignored"
+ ps->active.SetInterval(std::chrono::milliseconds(m_ssHLL)); //We factor in the hold last look time here, rather than 0
+
+ if (ps->doing_per_channel)
+ ps->priority_wait.SetInterval(std::chrono::milliseconds(m_ssHLL)); //We factor in the hold last look time here, rather than 0
+
+ validpacket = false;
+ }
+ else
+ {
+ //Based on the start code, update the timers
+ switch (start_code)
+ {
+ case STARTCODE_DMX:
+ {
+ //No matter how valid, we got something -- but we'll tweak the interval for any hll change
+ ps->doing_dmx = true;
ps->active.SetInterval(std::chrono::milliseconds(E131_NETWORK_DATA_LOSS_TIMEOUT + m_ssHLL));
- ps->lastseq = sequence;
- ps->src_cid = source_cid;
- ps->src_valid = true;
- ps->src_stable = true;
- ps->doing_dmx = (start_code == STARTCODE_DMX);
- //If we are in the sampling period, let all packets through
- if(m_isSampling)
+ } break;
+
+ case STARTCODE_PRIORITY: if (ps->waited_for_dd)
+ {
+ ps->doing_per_channel = true; //The source could have stopped sending dd for a while.
+ ps->priority_wait.Reset();
+ } break;
+ }
+
+ //Validate the sequence number, updating the stored one
+ //The two's complement math is to handle rollover, and we're explicitly
+ //doing assignment to force the type sizes. A negative number means
+ //we got an "old" one, but we assume that anything really old is possibly
+ //due the device having rebooted and starting the sequence over.
+ const qint16 result = reinterpret_cast(sequence) - reinterpret_cast(ps->lastseq);
+ if (result != 1)
+ {
+ ps->jumps++;
+ if ((result <= 0) && (result > -20))
{
- ps->waited_for_dd = true;
- ps->doing_per_channel = (start_code == STARTCODE_PRIORITY);
- newsourcenotify = true;
- ps->priority_wait.SetInterval(std::chrono::milliseconds(E131_NETWORK_DATA_LOSS_TIMEOUT + m_ssHLL));
+ validpacket = false;
+ ps->seqErr++;
}
- else
+ }
+
+ ps->lastseq = sequence;
+
+ //This next bit is a little tricky. We want to wait for dd packets (sampling period
+ //tweaks aside) and notify them with the dd packet first, but we don't want to do that
+ //if we've never seen a dmx packet from the source.
+ if (!ps->doing_dmx)
+ {
+ /* For fault finding an installation which only sends 0xdd for a universe
+ * (For example: ETC Cobalt does this for, currenlty, unpatched universes with in it's network map)
+ * We do want to say that having not sent dimmer data is a valid source/packet
+ */
+ // validpacket = false;
+ ps->priority_wait.Reset(); //We don't want to let the priority timer run out
+ }
+ else if (!ps->waited_for_dd && validpacket)
+ {
+ if (start_code == STARTCODE_PRIORITY)
+ {
+ ps->waited_for_dd = true;
+ ps->doing_per_channel = true;
+ ps->priority_wait.SetInterval(std::chrono::milliseconds(E131_NETWORK_DATA_LOSS_TIMEOUT + m_ssHLL));
+ newsourcenotify = true;
+ }
+ else if (ps->priority_wait.Expired())
{
- //If we aren't sampling, we want the earlier logic to set the state
- ps->doing_per_channel = ps->waited_for_dd = false;
- newsourcenotify = false;
- ps->priority_wait.SetInterval(std::chrono::milliseconds(WAIT_PRIORITY));
+ ps->waited_for_dd = true;
+ ps->doing_per_channel = false;
+ ps->priority_wait.SetInterval(std::chrono::milliseconds(E131_NETWORK_DATA_LOSS_TIMEOUT + m_ssHLL)); //In case the source later decides to sent 0xdd packets
+ newsourcenotify = true;
}
+ else
+ newsourcenotify = validpacket = false;
+ }
+ }
+ // Found the source, and we're ready to process the packet
+ }
+
+
+ if (!validpacket)
+ {
+ qDebug() << this << ": Source coming up, not processing packet";
+ return;
+ }
+
+ if (ps == nullptr) // Add a new source to the list
+ {
+ ps = new sACNSource(source_cid, universe);
+
+ m_sources.push_back(ps);
+
+ ps->name = QString::fromUtf8(source_name);
+ ps->ip = sender;
+ ps->synchronization = synchronization;
+ ps->active.SetInterval(std::chrono::milliseconds(E131_NETWORK_DATA_LOSS_TIMEOUT + m_ssHLL));
+ ps->lastseq = sequence;
+ ps->src_valid = true;
+ ps->src_stable = true;
+ ps->doing_dmx = (start_code == STARTCODE_DMX);
+ //If we are in the sampling period, let all packets through
+ if (m_isSampling)
+ {
+ ps->waited_for_dd = true;
+ ps->doing_per_channel = (start_code == STARTCODE_PRIORITY);
+ newsourcenotify = true;
+ ps->priority_wait.SetInterval(std::chrono::milliseconds(E131_NETWORK_DATA_LOSS_TIMEOUT + m_ssHLL));
+ }
+ else
+ {
+ //If we aren't sampling, we want the earlier logic to set the state
+ ps->doing_per_channel = ps->waited_for_dd = false;
+ newsourcenotify = false;
+ ps->priority_wait.SetInterval(std::chrono::milliseconds(WAIT_PRIORITY));
+ }
- validpacket = newsourcenotify;
+ validpacket = newsourcenotify;
- // This is a brand new source
- qDebug() << "sACNListener" << QThread::currentThreadId() << ": Found new source name " << source_name;
- m_mergeAll = true;
- emit sourceFound(ps);
- }
+ // This is a brand new source
+ qDebug() << this << ": Found new source name " << source_name;
+ m_mergeAll = true;
+ emit sourceFound(ps);
+ }
+
+ if (newsourcenotify)
+ {
+ // This is a source that came back online
+ qDebug() << this << ": Source came back name " << source_name;
+ m_mergeAll = true;
+ emit sourceResumed(ps);
+ emit sourceChanged(ps);
+ }
- if (newsourcenotify)
- {
- // This is a source that came back online
- qDebug() << "sACNListener" << QThread::currentThreadId() << ": Source came back name " << source_name;
- m_mergeAll = true;
- emit sourceResumed(ps);
- emit sourceChanged(ps);
- }
+ //Finally, Process the buffer
+ if (validpacket)
+ {
+ ps->source_params_change = false;
- //Finally, Process the buffer
- if(validpacket)
+ QString name = QString::fromUtf8(source_name);
+
+ if (ps->ip != sender)
{
- ps->source_params_change = false;
+ ps->ip = sender;
+ ps->source_params_change = true;
+ }
- QString name = QString::fromUtf8(source_name);
+ StreamingACNProtocolVersion protocolVersion = sACNProtocolUnknown;
+ switch (root_vector) {
+ case VECTOR_ROOT_E131_DATA:
+ protocolVersion = sACNProtocolRelease;
+ break;
- if(ps->ip != sender)
- {
- ps->ip = sender;
- ps->source_params_change = true;
- }
+ case VECTOR_ROOT_E131_DATA_DRAFT:
+ protocolVersion = sACNProtocolDraft;
+ break;
- StreamingACNProtocolVersion protocolVersion = sACNProtocolUnknown;
- switch (root_vector) {
- case VECTOR_ROOT_E131_DATA:
- protocolVersion = sACNProtocolRelease;
- break;
+ case VECTOR_ROOT_E131_DATA_PATHWAY_SECURE:
+ protocolVersion = sACNProtocolPathwaySecure;
+ break;
- case VECTOR_ROOT_E131_DATA_DRAFT:
- protocolVersion = sACNProtocolDraft;
- break;
+ default:
+ protocolVersion = sACNProtocolUnknown;
+ break;
+ }
- case VECTOR_ROOT_E131_DATA_PATHWAY_SECURE:
- protocolVersion = sACNProtocolPathwaySecure;
- break;
+ if (ps->protocol_version != protocolVersion)
+ {
+ ps->protocol_version = protocolVersion;
+ ps->source_params_change = true;
+ }
- default:
- protocolVersion = sACNProtocolUnknown;
- break;
- }
+ if (start_code == STARTCODE_DMX)
+ {
+ if (ps->name != name)
+ {
+ ps->name = name;
+ ps->source_params_change = true;
+ }
+ if (ps->isPreview != preview)
+ {
+ ps->isPreview = preview;
+ ps->source_params_change = true;
+ }
+ if (ps->priority != priority)
+ {
+ ps->priority = priority;
+ ps->source_params_change = true;
+ }
+ if (ps->synchronization != synchronization)
+ {
+ ps->synchronization = synchronization;
+ ps->source_params_change = true;
+ }
- if(ps->protocol_version!=protocolVersion)
- {
- ps->protocol_version = protocolVersion;
- ps->source_params_change = true;
- }
+ ps->storeReceivedLevels(pdata, slot_count);
- if(start_code == STARTCODE_DMX)
- {
- if(ps->name!=name)
- {
- ps->name = name;
- ps->source_params_change = true;
- }
- if(ps->isPreview != preview)
- {
- ps->isPreview = preview;
- ps->source_params_change = true;
- }
- if(ps->priority != priority)
- {
- ps->priority = priority;
- ps->source_params_change = true;
- }
- if(ps->synchronization != synchronization)
- {
- ps->synchronization = synchronization;
- ps->source_params_change = true;
- }
- // This is DMX
- // Copy the last array back
- memcpy(ps->last_level_array, ps->level_array, DMX_SLOT_MAX);
- // Fill in the new array
- memset(ps->level_array, 0, DMX_SLOT_MAX);
- memcpy(ps->level_array, pdata, slot_count);
-
- // Slot count change, re-merge all slots
- if(ps->slot_count != slot_count)
- {
- ps->slot_count = slot_count;
- ps->source_params_change = true;
- ps->source_levels_change = true;
- for(int i=0; idirty_array[i] |= true;
- }
-
- // Compare the two
- for(int i=0; ilevel_array[i]!=ps->last_level_array[i])
- {
- ps->dirty_array[i] |= true;
- ps->source_levels_change = true;
- }
- }
-
- // FPS Counter - we count only DMX frames
- ps->fpscounter.newFrame();
- if (ps->fpscounter.isNewFPS())
- ps->source_params_change = true;
- }
- else if(start_code == STARTCODE_PRIORITY)
- {
- // Copy the last array back
- memcpy(ps->last_priority_array, ps->priority_array, DMX_SLOT_MAX);
- // Fill in the new array
- memset(ps->priority_array, 0, DMX_SLOT_MAX);
- if (!Preferences::Instance().GetETCDD())
- { // DD is disabled, fill with universe priority
- std::fill(std::begin(ps->priority_array), std::end(ps->priority_array), ps->priority);
- } else {
- memcpy(ps->priority_array, pdata, slot_count);
- }
- // Compare the two
- for(int i=0; ipriority_array[i]!=ps->last_priority_array[i])
- {
- ps->dirty_array[i] |= true;
- ps->source_levels_change = true;
- }
- }
- }
+ // FPS Counter - we count only DMX frames
+ ps->fpscounter.newFrame(packet_tock);
+ if (ps->fpscounter.isNewFPS())
+ ps->source_params_change = true;
+ }
+ else if (start_code == STARTCODE_PRIORITY)
+ {
+ if (!Preferences::Instance().GetETCDD())
+ { // DD is disabled, fill with universe priority.
+ // TODO: Does this actually ever do anything?
+ std::array univ_priority;
+ univ_priority.fill(ps->priority);
+ ps->storeReceivedPriorities(univ_priority.data(), slot_count);
+ }
+ else
+ {
+ ps->storeReceivedPriorities(pdata, slot_count);
+ }
+ }
- if(ps->source_params_change)
- {
- emit sourceChanged(ps);
- ps->source_params_change = false;
- }
+ if (ps->source_params_change)
+ {
+ emit sourceChanged(ps);
+ ps->source_params_change = false;
+ }
- // Listen to synchronization source
- if (ps->synchronization &&
- (!ps->sync_listener || ps->sync_listener->universe() != ps->synchronization)) {
- ps->sync_listener = sACNManager::Instance().getListener(ps->synchronization);
- }
+ // Listen to synchronization source
+ if (ps->synchronization &&
+ (!ps->sync_listener || ps->sync_listener->universe() != ps->synchronization)) {
+ ps->sync_listener = sACNManager::Instance().getListener(ps->synchronization);
+ }
- // Merge
- performMerge();
+ // Merge
+ performMerge();
+ // Inform everyone who cares that a DMX packet has been received
+ if (start_code == STARTCODE_DMX)
+ {
+ QMutexLocker locker(&m_directCallbacksMutex);
+ for (IDmxReceivedCallback* callback : m_dmxReceivedCallbacks)
+ {
+ callback->sACNListenerDmxReceived(packet_tock, m_universe, m_current_levels);
+ }
}
+ }
}
-inline bool isPatched(const sACNSource &source, uint16_t address)
+void sACNListener::addDirectCallback(IDmxReceivedCallback* callback)
{
- // Can only be unpatched if we have DD packets
- if (!source.doing_per_channel)
- return true;
-
- // Priority not 0
- return source.priority_array[address] != 0;
+ QMutexLocker locker(&m_directCallbacksMutex);
+ for (const IDmxReceivedCallback* existing : m_dmxReceivedCallbacks)
+ {
+ if (existing == callback)
+ return;
+ }
+ m_dmxReceivedCallbacks.push_back(callback);
}
-void sACNListener::performMerge()
+void sACNListener::removeDirectCallback(IDmxReceivedCallback* callback)
{
- //array of addresses to merge. to prevent duplicates and because you can have
- //an odd collection of addresses, addresses[n] would be 'n' for the value in question
- // and -1 if not required
- int addresses_to_merge[DMX_SLOT_MAX];
- int number_of_addresses_to_merge = 0;
+ QMutexLocker locker(&m_directCallbacksMutex);
+ for (auto it = m_dmxReceivedCallbacks.begin(); it != m_dmxReceivedCallbacks.end(); /**/)
+ {
+ if (callback == (*it))
+ it = m_dmxReceivedCallbacks.erase(it);
+ else
+ ++it;
+ }
+}
- memset(addresses_to_merge, -1, sizeof(int) * DMX_SLOT_MAX);
+inline bool isPatched(const sACNSource& source, uint16_t address)
+{
+ // Can only be unpatched if we have DD packets
+ if (!source.doing_per_channel)
+ return true;
- {
- QMutexLocker locker(&m_monitoredChannelsMutex);
- for(auto const &chan: qAsConst(m_monitoredChannels))
- {
- QPointF data;
- data.setX(m_elapsedTime.nsecsElapsed()/1000000.0);
- data.setY(mergedLevels().at(chan).level);
- emit dataReady(chan, data);
- }
- }
+ // Priority not 0
+ return source.priority_array[address] != 0;
+}
- if(m_mergesPerSecondTimer.hasExpired(1000))
+void sACNListener::performMerge()
+{
+ if (m_mergesPerSecondTimer.hasExpired(1000))
+ {
+ m_mergesPerSecond = m_mergeCounter;
+ m_mergeCounter = 0;
+ m_mergesPerSecondTimer.restart();
+ }
+
+ ++m_mergeCounter;
+
+ //array of addresses to merge. to prevent duplicates and because you can have
+ //an odd collection of addresses, addresses[n] would be 'n' for the value in question
+ // and -1 if not required
+ std::array addresses_to_merge;
+ int number_of_addresses_to_merge = 0;
+
+ // Step one : find any addresses which have changed
+ if (m_mergeAll) // Act like all addresses changed
+ {
+ number_of_addresses_to_merge = addresses_to_merge.size();
+ for (int i = 0; i < addresses_to_merge.size(); i++)
{
- m_mergesPerSecond = m_mergeCounter;
- m_mergeCounter = 0;
- m_mergesPerSecondTimer.restart();
+ addresses_to_merge[i] = i;
}
- m_mergeCounter++;
-
- // Step one : find any addresses which have changed
- if(m_mergeAll) // Act like all addresses changed
- {
- number_of_addresses_to_merge = DMX_SLOT_MAX;
- for(int i=0; i::iterator it = m_sources.begin(); it != m_sources.end(); ++it)
+ if (!ps->src_valid)
+ continue; // Inactive source, ignore it
+ if (!ps->source_levels_change)
+ continue; // We don't need to consider this one, no change
+ for (int i = 0; i < DMX_SLOT_MAX; i++)
+ {
+ if (ps->dirty_array[i])
{
- sACNSource *ps = *it;
- if(!ps->src_valid)
- continue; // Inactive source, ignore it
- if(!ps->source_levels_change)
- continue; // We don't need to consider this one, no change
- for(int i=0; idirty_array[i])
- {
- addresses_to_merge[i] = i;
- number_of_addresses_to_merge++;
- }
- }
- // Clear the flags
- memset(ps->dirty_array, 0 , DMX_SLOT_MAX);
- ps->source_levels_change = false;
+ addresses_to_merge[i] = i;
+ number_of_addresses_to_merge++;
}
+ }
+ // Clear the flags
+ ps->dirty_array.fill(false);
+ ps->source_levels_change = false;
}
+ }
- if(number_of_addresses_to_merge == 0) return; // Nothing to do
+ if (number_of_addresses_to_merge == 0) return; // Nothing to do
- // Clear out the sources list for all the affected channels, we'll be refreshing it
+ // Clear out the sources list for all the affected channels, we'll be refreshing it
+ QMutexLocker mergeLocker(&m_merged_levelsMutex);
- int skipCounter = 0;
- for(int i=0; i < DMX_SLOT_MAX && i<(number_of_addresses_to_merge + skipCounter); i++)
- {
- QMutexLocker mergeLocker(&m_merged_levelsMutex);
- m_merged_levels[i].changedSinceLastMerge = false;
- if(addresses_to_merge[i] == -1) {
- ++skipCounter;
- continue;
- }
- sACNMergedAddress *pAddr = &m_merged_levels[addresses_to_merge[i]];
- pAddr->otherSources.clear();
+ int skipCounter = 0;
+ for (int i = 0; i < DMX_SLOT_MAX && i < (number_of_addresses_to_merge + skipCounter); i++)
+ {
+ m_merged_levels[i].changedSinceLastMerge = false;
+ if (addresses_to_merge[i] == -1) {
+ ++skipCounter;
+ continue;
}
+ sACNMergedAddress* pAddr = &m_merged_levels[addresses_to_merge[i]];
+ pAddr->otherSources.clear();
+ }
- // Find the highest priority source for each address we need to work on
-
- int levels[DMX_SLOT_MAX];
- memset(&levels, -1, sizeof(levels));
+ // Find the highest priority source for each address we need to work on
+ m_last_levels.fill(-1);
+ m_last_priorities.fill(-1);
- int priorities[DMX_SLOT_MAX];
- memset(&priorities, -1, sizeof(priorities));
+ QMultiMap addressToSourceMap;
- QMultiMap addressToSourceMap;
+ // Find the highest priority for the address
+ bool secureDataOnly = false;
+ if (Preferences::Instance().GetPathwaySecureRx())
+ secureDataOnly = Preferences::Instance().GetPathwaySecureRxDataOnly();
+ for (std::vector::iterator it = m_sources.begin(); it != m_sources.end(); ++it)
+ {
+ sACNSource* ps = *it;
- // Find the highest priority for the address
- bool secureDataOnly = false;
- if (Preferences::Instance().GetPathwaySecureRx())
- secureDataOnly = Preferences::Instance().GetPathwaySecureRxDataOnly();
- for(std::vector::iterator it = m_sources.begin(); it != m_sources.end(); ++it)
+ if (ps->src_valid && !ps->active.Expired() && !ps->doing_per_channel)
{
- sACNSource *ps = *it;
-
- if(ps->src_valid && !ps->active.Expired() && !ps->doing_per_channel)
- {
- // Set the priority array for sources which are not doing per-channel
- memset(ps->priority_array, ps->priority, sizeof(ps->priority_array));
- }
-
- skipCounter = 0;
- for(int i=0; i < DMX_SLOT_MAX && i<(number_of_addresses_to_merge + skipCounter); i++)
- {
- QMutexLocker mergeLocker(&m_merged_levelsMutex);
- if(addresses_to_merge[i] == -1) {
- ++skipCounter;
- continue;
- }
- int address = addresses_to_merge[i];
-
- if (
- ps->src_valid // Valid Source
- && !ps->active.Expired() // Not expired
- && !(ps->priority_array[address] < priorities[address]) // Not lesser priority
- && isPatched(*ps, address) // Priority > 0 if DD
- && (address < ps->slot_count) // Sending the required slot
- && ((!secureDataOnly) || (secureDataOnly && ps->pathway_secure.isSecure())) // Is secure, if only displaying secure sources
- )
- {
- if (ps->priority_array[address] > priorities[address])
- {
- // Source of higher priority
- priorities[address] = ps->priority_array[address];
- addressToSourceMap.remove(address);
- }
- addressToSourceMap.insert(address, ps);
- }
-
- if(
- ps->src_valid // Valid Source
- && !ps->active.Expired() // Not Expired
- && isPatched(*ps, address) // Priority > 0 if DD
- && (address < ps->slot_count) // Sending the required slot
- )
- m_merged_levels[addresses_to_merge[i]].otherSources.insert(ps);
- }
+ // Set the priority array for sources which are not doing per-channel
+ ps->priority_array.fill(ps->priority);
}
- // Next, find highest level for the highest prioritized sources
skipCounter = 0;
- for(int i=0; i < DMX_SLOT_MAX && i<(number_of_addresses_to_merge + skipCounter); i++)
+ for (int i = 0; i < DMX_SLOT_MAX && i < (number_of_addresses_to_merge + skipCounter); i++)
{
- QMutexLocker mergeLocker(&m_merged_levelsMutex);
- if(addresses_to_merge[i] == -1) {
- ++skipCounter;
- continue;
- }
- int address = addresses_to_merge[i];
- QList sourceList = addressToSourceMap.values(address);
-
- if(sourceList.count() == 0)
- {
- m_merged_levels[address].level = -1;
- m_merged_levels[address].winningSource = NULL;
- m_merged_levels[address].otherSources.clear();
- m_merged_levels[address].winningPriority = priorities[address];
- }
- for (auto s : sourceList)
+ if (addresses_to_merge[i] == -1) {
+ ++skipCounter;
+ continue;
+ }
+ int address = addresses_to_merge[i];
+
+ if (
+ ps->src_valid // Valid Source
+ && !ps->active.Expired() // Not expired
+ && !(ps->priority_array[address] < m_last_priorities[address]) // Not lesser priority
+ && isPatched(*ps, address) // Priority > 0 if DD
+ && (address < ps->slot_count) // Sending the required slot
+ && ((!secureDataOnly) || (secureDataOnly && ps->pathway_secure.isSecure())) // Is secure, if only displaying secure sources
+ )
+ {
+ if (ps->priority_array[address] > m_last_priorities[address])
{
- if(s->level_array[address] > levels[address])
- {
- levels[address] = s->level_array[address];
- m_merged_levels[address].changedSinceLastMerge = (m_merged_levels[address].level != levels[address]);
- m_merged_levels[address].level = levels[address];
- m_merged_levels[address].winningSource = s;
- m_merged_levels[address].winningPriority = priorities[address];
- }
+ // Source of higher priority
+ m_last_priorities[address] = ps->priority_array[address];
+ addressToSourceMap.remove(address);
}
- // Remove the winning source from the list of others
- if(m_merged_levels[address].winningSource)
- m_merged_levels[address].otherSources.remove(m_merged_levels[address].winningSource);
+ addressToSourceMap.insert(address, ps);
+ }
+
+ if (
+ ps->src_valid // Valid Source
+ && !ps->active.Expired() // Not Expired
+ && isPatched(*ps, address) // Priority > 0 if DD
+ && (address < ps->slot_count) // Sending the required slot
+ )
+ m_merged_levels[addresses_to_merge[i]].otherSources.insert(ps);
+ }
+ }
+
+ // Next, find highest level for the highest prioritized sources
+ skipCounter = 0;
+ for (int i = 0; i < DMX_SLOT_MAX && i < (number_of_addresses_to_merge + skipCounter); i++)
+ {
+ if (addresses_to_merge[i] == -1) {
+ ++skipCounter;
+ continue;
}
+ int address = addresses_to_merge[i];
+ QList sourceList = addressToSourceMap.values(address);
- // Tell people..
- emit levelsChanged();
-}
+ if (sourceList.empty())
+ {
+ m_merged_levels[address].level = -1;
+ m_merged_levels[address].winningSource = nullptr;
+ m_merged_levels[address].otherSources.clear();
+ m_merged_levels[address].winningPriority = m_last_priorities[address];
+ }
+ for (sACNSource* s : sourceList)
+ {
+ if (s->level_array[address] > m_last_levels[address])
+ {
+ m_last_levels[address] = s->level_array[address];
+ m_merged_levels[address].changedSinceLastMerge = (m_merged_levels[address].level != m_last_levels[address]);
+ m_merged_levels[address].level = m_last_levels[address];
+ m_merged_levels[address].winningSource = s;
+ m_merged_levels[address].winningPriority = m_last_priorities[address];
+ }
+ }
+ // Remove the winning source from the list of others
+ if (m_merged_levels[address].winningSource)
+ m_merged_levels[address].otherSources.remove(m_merged_levels[address].winningSource);
+
+ // Update current final merge
+ m_current_levels[address] = m_last_levels[address];
+ m_current_priorities[address] = m_last_priorities[address];
+ }
+ mergeLocker.unlock();
+
+ // Tell people..
+ emit levelsChanged();
+}
diff --git a/src/sacn/sacnlistener.h b/src/sacn/sacnlistener.h
index 962db035..40a7f49a 100644
--- a/src/sacn/sacnlistener.h
+++ b/src/sacn/sacnlistener.h
@@ -27,21 +27,18 @@
#include "streamingacn.h"
#include "sacnsocket.h"
+#include
+
Q_DECLARE_METATYPE(QHostAddress)
struct sACNMergedAddress
{
- sACNMergedAddress() {
- level = -1;
- winningSource = NULL;
- changedSinceLastMerge = false;
- winningPriority = 0;
- }
- int level;
- sACNSource *winningSource;
- QSet otherSources;
- bool changedSinceLastMerge;
- int winningPriority;
+ sACNMergedAddress() = default;
+ sACNSource* winningSource = nullptr;
+ QSet otherSources;
+ int level = -1;
+ int winningPriority = 0;
+ bool changedSinceLastMerge = false;
};
typedef QVector sACNMergedSourceList;
@@ -53,106 +50,145 @@ typedef QVector sACNMergedSourceList;
*/
class sACNListener : public QObject
{
- Q_OBJECT
-public:
- sACNListener(int universe, QObject *parent = 0);
- virtual ~sACNListener();
+ Q_OBJECT
+public:
+ class IDmxReceivedCallback
+ {
+ public:
+ virtual ~IDmxReceivedCallback() {}
/**
- * @brief universe
- * @return the universe which this listener is listening for
- */
- int universe() const { return m_universe; }
- /**
- * @brief mergedLevels
- * @return an sACNMergerdSourceList, a list of merged address structures, allowing you to see
- * the result of the merge algorithm together with all the sub-sources, by address
- */
- sACNMergedSourceList mergedLevels() {
- QMutexLocker mergeLocker(&m_merged_levelsMutex);
- return m_merged_levels;
- }
-
- int sourceCount() const { return static_cast(m_sources.size()); }
- sACNSource *source(int index) { return m_sources[index];}
- const std::vector getSourceList() const { return m_sources; }
+ * @brief Called in the listener thread after a DMX packet has been received and merged
+ * @param packet_tock global tock of this packet
+ * @param universe number
+ * @param mergedLevels array of merged levels
+ */
+ virtual void sACNListenerDmxReceived(tock packet_tock, int universe, const std::array &mergedLevels) = 0;
+ };
- /**
- * @brief processDatagram Process a suspected sACN datagram.
- * This allows other listeners to pass on unicast datagrams for other universes
- *
- */
- Q_INVOKABLE void processDatagram(const QByteArray &data, const QHostAddress &destination, const QHostAddress &sender);
+public:
+ sACNListener(int universe, QObject* parent = 0);
+ virtual ~sACNListener();
+
+ /**
+ * @brief universe
+ * @return the universe which this listener is listening for
+ */
+ int universe() const { return m_universe; }
+
+ /**
+ * @brief mergedLevels
+ * @return an sACNMergedSourceList, a list of merged address structures, allowing you to see
+ * the result of the merge algorithm together with all the sub-sources, by address
+ */
+ sACNMergedSourceList mergedLevels() {
+ QMutexLocker mergeLocker(&m_merged_levelsMutex);
+ return m_merged_levels;
+ }
+
+ /**
+ * @brief mergedLevels
+ * @param address The address to return
+ * @return an sACNMergedAddress containing the result of the merge algorithm together with all the sub-sources
+ */
+ sACNMergedAddress mergedLevel(int address) {
+ if (address < 0 || address >= m_merged_levels.size()) return sACNMergedAddress();
+ QMutexLocker mergeLocker(&m_merged_levelsMutex);
+ return m_merged_levels[address];
+ }
+
+ /**
+ * @brief mergedLevelsOnly
+ * @return an array of merged levels. -1 means no source at all
+ */
+ std::array mergedLevelsOnly() {
+ QMutexLocker mergeLocker(&m_merged_levelsMutex);
+ return m_current_levels;
+ }
+
+ /**
+ * @brief mergedPrioritiesOnly
+ * @return an array of final priorities. -1 means no source at all
+ */
+ std::array mergedPrioritiesOnly() {
+ QMutexLocker mergeLocker(&m_merged_levelsMutex);
+ return m_current_priorities;
+ }
- // Diagnostic - the number of merge operations per second
+ int sourceCount() const { return static_cast(m_sources.size()); }
+ sACNSource* source(int index) { return m_sources[index]; }
+ const std::vector getSourceList() const { return m_sources; }
- int mergesPerSecond() const { return (m_mergesPerSecond > 0) ? m_mergesPerSecond : 0;}
+ /**
+ * @brief processDatagram Process a suspected sACN datagram.
+ * This allows other listeners to pass on unicast datagrams for other universes
+ *
+ */
+ Q_INVOKABLE void processDatagram(const QByteArray& data, const QHostAddress& destination, const QHostAddress& sender);
- /**
- * @brief getBindStatus Get interface bind status of listener
- * @return A struct of bind types and status
- */
- sACNRxSocket::sBindStatus getBindStatus() const { return m_bindStatus; }
+ // Diagnostic - the number of merge operations per second
- /**
- * @brief Force the listener to performa a full merge
- */
- void doFullMerge() { m_mergeAll = true; }
+ int mergesPerSecond() const { return (m_mergesPerSecond > 0) ? m_mergesPerSecond : 0; }
+
+ /**
+ * @brief getBindStatus Get interface bind status of listener
+ * @return A struct of bind types and status
+ */
+ sACNRxSocket::sBindStatus getBindStatus() const { return m_bindStatus; }
+
+ /**
+ * @brief Force the listener to perform a full merge
+ */
+ void doFullMerge() { m_mergeAll = true; }
+
+ // Objects that want a direct callback for levels in the listener thread
+ void addDirectCallback(IDmxReceivedCallback* callback);
+ void removeDirectCallback(IDmxReceivedCallback* callback);
public slots:
- void startReception();
- void monitorAddress(int address, const QObject *owner) {
- QMutexLocker locker(&m_monitoredChannelsMutex);
- m_monitoredChannels.insert(owner, address);
- connect(owner, &QObject::destroyed, this, [this](const QObject *owner) {
- QMutexLocker locker(&m_monitoredChannelsMutex);
- m_monitoredChannels.remove(owner);
- });
- }
- void unMonitorAddress(int address, const QObject *owner) {
- QMutexLocker locker(&m_monitoredChannelsMutex);
- m_monitoredChannels.remove(owner, address);
- }
+ void startReception();
+
signals:
- void listenerStarted(int universe);
- void sourceFound(sACNSource *source);
- void sourceLost(sACNSource *source);
- void sourceResumed(sACNSource *source);
- void sourceChanged(sACNSource *source);
- void levelsChanged();
- void dataReady(int address, QPointF data); // Used by ScopeWidget
+ void listenerStarted(int universe);
+ void sourceFound(sACNSource* source);
+ void sourceLost(sACNSource* source);
+ void sourceResumed(sACNSource* source);
+ void sourceChanged(sACNSource* source);
+ void levelsChanged();
private slots:
- void readPendingDatagrams();
- void performMerge();
- void checkSourceExpiration();
- void sampleExpiration();
+ void readPendingDatagrams();
+ void performMerge();
+ void checkSourceExpiration();
+ void sampleExpiration();
private:
- QMutex m_processMutex;
- void startInterface(const QNetworkInterface &iface);
- std::list m_sockets;
- std::vector m_sources;
- int m_last_levels[MAX_DMX_ADDRESS];
- sACNMergedSourceList m_merged_levels;
- QMutex m_merged_levelsMutex;
- int m_universe;
- // The per-source hold last look time
- int m_ssHLL = 1000;
- // Are we in the initial sampling state
- bool m_isSampling = true;
- QTimer *m_initalSampleTimer = nullptr;
- QTimer *m_mergeTimer = nullptr;
- QElapsedTimer m_elapsedTime;
- int m_predictableTimerValue;
- QMutex m_monitoredChannelsMutex;
- QMultiMap m_monitoredChannels;
- bool m_mergeAll; // A flag to initiate a complete remerge of everything
- unsigned int m_mergesPerSecond = 0;
- int m_mergeCounter;
- QElapsedTimer m_mergesPerSecondTimer;
-
- sACNRxSocket::sBindStatus m_bindStatus;
+ QMutex m_processMutex;
+ void startInterface(const QNetworkInterface& iface);
+ std::list m_sockets;
+ std::vector m_sources;
+ std::array m_last_levels = {};
+ std::array m_last_priorities = {};
+ std::array m_current_levels = {};
+ std::array m_current_priorities = {};
+ sACNMergedSourceList m_merged_levels;
+ QMutex m_merged_levelsMutex;
+ const int m_universe = 0;
+ // The per-source hold last look time
+ int m_ssHLL = 1000;
+ // Are we in the initial sampling state
+ bool m_isSampling = true;
+ QTimer* m_initalSampleTimer = nullptr;
+ QTimer* m_mergeTimer = nullptr;
+ int m_predictableTimerValue;
+ QMutex m_directCallbacksMutex;
+ std::vector m_dmxReceivedCallbacks;
+ bool m_mergeAll = true; // A flag to initiate a complete remerge of everything
+ unsigned int m_mergesPerSecond = 0;
+ int m_mergeCounter = 0;
+ QElapsedTimer m_mergesPerSecondTimer;
+
+ sACNRxSocket::sBindStatus m_bindStatus;
};
diff --git a/src/sacn/sacnsynchronization.cpp b/src/sacn/sacnsynchronization.cpp
index eeb367d3..217d7e11 100644
--- a/src/sacn/sacnsynchronization.cpp
+++ b/src/sacn/sacnsynchronization.cpp
@@ -107,12 +107,14 @@ void sACNSynchronizationRX::timeoutSyncAddresses() {
}
}
-void sACNSynchronizationRX::processPacket(quint8* pbuf, uint buflen, QHostAddress destination, QHostAddress sender)
+void sACNSynchronizationRX::processPacket(const quint8* pbuf, uint buflen, QHostAddress destination, QHostAddress sender)
{
bool flag1, flag2, flag3;
quint32 length;
CID cid;
+ const tock packet_tock = sACNManager::GetTock();
+
QMutexLocker locker(&m_mutex);
// Check length
@@ -157,7 +159,7 @@ void sACNSynchronizationRX::processPacket(quint8* pbuf, uint buflen, QHostAddres
m_synchronizationSources[syncAddress][cid].sender = sender;
m_synchronizationSources[syncAddress][cid].dataLoss.SetInterval(std::chrono::milliseconds(E131_NETWORK_DATA_LOSS_TIMEOUT));
- m_synchronizationSources[syncAddress][cid].fps->newFrame();
+ m_synchronizationSources[syncAddress][cid].fps->newFrame(packet_tock);
emit synchronize(syncAddress);
}
diff --git a/src/sacn/sacnsynchronization.h b/src/sacn/sacnsynchronization.h
index 2af26695..67d1dc19 100644
--- a/src/sacn/sacnsynchronization.h
+++ b/src/sacn/sacnsynchronization.h
@@ -59,8 +59,8 @@ class sACNSynchronizationRX : public QObject
sourceSequence(quint8 sequence) : lastNum(sequence) {}
quint8 lastNum = 0;
- quint16 jumps = 0;
- quint16 seqErr = 0;
+ unsigned int jumps = 0;
+ unsigned int seqErr = 0;
// Check sequence number
bool checkSeq(quint8 newNum) {
@@ -103,7 +103,7 @@ class sACNSynchronizationRX : public QObject
tCIDDetails getSynchronizationSources(tsyncAddress syncAddress) const { return m_synchronizationSources.value(syncAddress); }
QList getSynchronizationAddresses() const { return m_synchronizationSources.keys(); }
- void processPacket(quint8* pbuf, uint buflen, QHostAddress destination, QHostAddress sender);
+ void processPacket(const quint8* pbuf, uint buflen, QHostAddress destination, QHostAddress sender);
signals:
void newSyncAddress(tsyncAddress syncAddress);
diff --git a/src/sacn/securesacn.cpp b/src/sacn/securesacn.cpp
index 25130ad9..b7837ec8 100644
--- a/src/sacn/securesacn.cpp
+++ b/src/sacn/securesacn.cpp
@@ -181,12 +181,12 @@ PathwaySecure::Sequence::value_t PathwaySecure::Sequence::next(const CID &cid, t
}
bool PathwaySecure::VerifyStreamHeader(
- quint8* pbuf, uint buflen,
+ const quint8* pbuf, uint buflen,
CID &source_cid, char* source_name,
quint8 &priority, quint8 &start_code,
quint16 &synchronization, quint8 &sequence,
quint8 &options, quint16 &universe,
- quint16 &slot_count, quint8* &pdata)
+ quint16 &slot_count, const quint8* &pdata)
{
if(!pbuf)
return false;
@@ -201,22 +201,14 @@ bool PathwaySecure::VerifyStreamHeader(
if (UpackBUint32(pbuf + ROOT_VECTOR_ADDR) != RootLayer::VECTOR)
return false;
- /* Verify header as if release version
- * Temporarily replacing root vector to fake release version, so that the stock VerifyStreamHeader works ok
- */
- PackBUint32(pbuf + ROOT_VECTOR_ADDR, VECTOR_ROOT_E131_DATA);
- bool verify = ::VerifyStreamHeader(
+ /* Verify header as if release version. The stock VerifyStreamHeader assumes root vector is VECTOR_ROOT_E131_DATA */
+ return ::VerifyStreamHeader(
pbuf, buflen,
source_cid, source_name,
priority, start_code,
synchronization, sequence,
options, universe,
slot_count, pdata);
- PackBUint32(pbuf + ROOT_VECTOR_ADDR, RootLayer::VECTOR);
- if (!verify)
- return false;
-
- return true;
}
void PathwaySecure::InitStreamHeader(
diff --git a/src/sacn/securesacn.h b/src/sacn/securesacn.h
index 7b47825d..787128e7 100644
--- a/src/sacn/securesacn.h
+++ b/src/sacn/securesacn.h
@@ -97,12 +97,12 @@ class PathwaySecure
* @return True if header ok
*/
static bool VerifyStreamHeader(
- quint8* pbuf, uint buflen,
+ const quint8* pbuf, uint buflen,
CID &source_cid, char* source_name,
quint8 &priority, quint8 &start_code,
quint16 &synchronization, quint8 &sequence,
quint8 &options, quint16 &universe,
- quint16 &slot_count, quint8* &pdata);
+ quint16 &slot_count, const quint8* &pdata);
/**
* @brief Setup stream header
diff --git a/src/sacn/streamcommon.cpp b/src/sacn/streamcommon.cpp
index ea942252..693afedb 100644
--- a/src/sacn/streamcommon.cpp
+++ b/src/sacn/streamcommon.cpp
@@ -275,12 +275,12 @@ void SetStreamHeaderSequence(quint8* pbuf, quint8 seq, bool draft)
* pdata is the offset into the buffer where the data is stored
*/
e_ValidateStreamHeader ValidateStreamHeader(
- quint8* pbuf, uint buflen,
+ const quint8* pbuf, uint buflen,
quint32 &root_vector,
CID &source_cid, char* source_name, quint8 &priority,
quint8 &start_code, quint16 &synchronization,
quint8 &sequence, quint8 &options, quint16 &universe,
- quint16 &slot_count, quint8* &pdata)
+ quint16 &slot_count, const quint8* &pdata)
{
if(!pbuf)
return e_ValidateStreamHeader::StreamHeader_Invalid;
@@ -325,11 +325,11 @@ e_ValidateStreamHeader ValidateStreamHeader(
* helper function that does the actual validation of a header
* that carries the post-ratification root vector
*/
-bool VerifyStreamHeader(quint8* pbuf, uint buflen, CID &source_cid,
+bool VerifyStreamHeader(const quint8* pbuf, uint buflen, CID &source_cid,
char* source_name, quint8 &priority,
quint8 &start_code, quint16 &synchronization, quint8 &sequence,
quint8 &options, quint16 &universe,
- quint16 &slot_count, quint8* &pdata)
+ quint16 &slot_count, const quint8* &pdata)
{
if(!pbuf)
return false;
@@ -377,7 +377,7 @@ bool VerifyStreamHeader(quint8* pbuf, uint buflen, CID &source_cid,
source_cid.Unpack(pbuf + CID_ADDR);
std::fill(source_name, source_name + SOURCE_NAME_SIZE, '\0');
- std::string_view(reinterpret_cast(pbuf + SOURCE_NAME_ADDR), SOURCE_NAME_SIZE).copy(source_name, SOURCE_NAME_SIZE - 1);
+ std::string_view(reinterpret_cast(pbuf + SOURCE_NAME_ADDR), SOURCE_NAME_SIZE).copy(source_name, SOURCE_NAME_SIZE - 1);
priority = UpackBUint8(pbuf + PRIORITY_ADDR);
start_code = UpackBUint8(pbuf + START_CODE_ADDR);
synchronization = UpackBUint16(pbuf + SYNC_ADDR);
@@ -386,6 +386,11 @@ bool VerifyStreamHeader(quint8* pbuf, uint buflen, CID &source_cid,
universe = UpackBUint16(pbuf + UNIVERSE_ADDR);
slot_count = UpackBUint16(pbuf + PROP_COUNT_ADDR) - 1; //The property value count includes the start code byte
pdata = pbuf + STREAM_HEADER_SIZE;
+
+ // Validate maximum slot count
+ if (slot_count > DMX_SLOT_MAX)
+ return false;
+
quint16 post_amble_size = UpackBUint16(pbuf + POSTAMBLE_SIZE_ADDR);
/*Do final length validation*/
@@ -402,11 +407,11 @@ bool VerifyStreamHeader(quint8* pbuf, uint buflen, CID &source_cid,
* ratification of the standard.
*/
bool VerifyStreamHeaderForDraft(
- quint8* pbuf, uint buflen,
+ const quint8* pbuf, uint buflen,
CID &source_cid, char* source_name, quint8 &priority,
quint8 &start_code, quint8 &sequence,
quint16 &universe, quint16 &slot_count,
- quint8* &pdata)
+ const quint8* &pdata)
{
if(!pbuf)
return false;
@@ -457,7 +462,7 @@ bool VerifyStreamHeaderForDraft(
source_cid.Unpack(pbuf + CID_ADDR);
std::fill(source_name, source_name + SOURCE_NAME_SIZE, '\0');
- std::string_view(reinterpret_cast(pbuf + DRAFT_SOURCE_NAME_ADDR), DRAFT_SOURCE_NAME_SIZE).copy(source_name, DRAFT_SOURCE_NAME_SIZE - 1);
+ std::string_view(reinterpret_cast(pbuf + DRAFT_SOURCE_NAME_ADDR), DRAFT_SOURCE_NAME_SIZE).copy(source_name, DRAFT_SOURCE_NAME_SIZE - 1);
priority = UpackBUint8(pbuf + DRAFT_PRIORITY_ADDR);
if(priority == 0)
diff --git a/src/sacn/streamcommon.h b/src/sacn/streamcommon.h
index f45ce6db..0540b835 100644
--- a/src/sacn/streamcommon.h
+++ b/src/sacn/streamcommon.h
@@ -215,23 +215,23 @@ enum e_ValidateStreamHeader
StreamHeader_Unknown
};
e_ValidateStreamHeader ValidateStreamHeader(
- quint8* pbuf, uint buflen,
+ const quint8* pbuf, uint buflen,
quint32 &root_vector,
CID &source_cid, char* source_sp, quint8 &priority,
quint8 &start_code, quint16 &synchronization, quint8 &sequence,
quint8 &options, quint16 &universe,
- quint16 &slot_count, quint8* &pdata);
+ quint16 &slot_count, const quint8* &pdata);
/*
* helper function that does the actual validation of a header
* that carries the post-ratification root vector
*/
bool VerifyStreamHeader(
- quint8 *pbuf, uint buflen,
+ const quint8 *pbuf, uint buflen,
CID &source_cid, char* source_name, quint8 &priority,
quint8 &start_code, quint16 &synchronization, quint8 &sequence,
quint8 &options, quint16 &universe,
- quint16 &slot_count, quint8* &pdata);
+ quint16 &slot_count, const quint8* &pdata);
/*
* helper function that does the actual validation of a header
* that carries the early draft's root vector
@@ -239,11 +239,11 @@ bool VerifyStreamHeader(
* ratification of the standard.
*/
bool VerifyStreamHeaderForDraft(
- quint8* pbuf, uint buflen,
+ const quint8* pbuf, uint buflen,
CID &source_cid, char* source_space, quint8 &priority,
quint8 &start_code, quint8 &sequence,
quint16 &universe, quint16 &slot_count,
- quint8* &pdata);
+ const quint8* &pdata);
/*
* Returns true if contains draft root vector value
diff --git a/src/sacn/streamingacn.cpp b/src/sacn/streamingacn.cpp
index 95ebcaaa..147b9c2d 100644
--- a/src/sacn/streamingacn.cpp
+++ b/src/sacn/streamingacn.cpp
@@ -42,8 +42,7 @@
#include
#include
-#include
-#include
+
#ifdef QT_GUI_LIB
#include
#else
@@ -66,254 +65,328 @@ QString GetProtocolVersionString(StreamingACNProtocolVersion value)
}
}
-sACNSource::sACNSource() :
- src_valid(false),
- lastseq(0),
- waited_for_dd(false),
- doing_dmx(false), //if true, we are processing dmx data from this source
- doing_per_channel(false), //If true, we are tracking per-channel priority messages for this source
- isPreview(false),
- universe(0),
- slot_count(0),
- priority(0),
- seqErr(0),
- jumps(0)
+sACNSource::sACNSource(const CID& source_cid, uint16_t universe)
+ : src_cid(source_cid)
+ , universe(universe)
{
- std::fill(level_array, level_array + sizeof(level_array), 0);
- std::fill(priority_array, priority_array + sizeof(priority_array), 0);
}
-sACNManager &sACNManager::Instance()
+void sACNSource::storeReceivedLevels(const uint8_t* pdata, uint16_t rx_slot_count)
{
- static sACNManager s_instance;
- return s_instance;
+ // Copy the last array back
+ last_level_array = level_array;
+ // Fill in the new array
+ if (rx_slot_count < level_array.size())
+ level_array.fill(0);
+ memcpy(level_array.data(), pdata, rx_slot_count);
+
+ // Slot count change, re-merge all slots
+ if (slot_count != rx_slot_count)
+ {
+ slot_count = rx_slot_count;
+ source_params_change = true;
+ source_levels_change = true;
+ dirty_array.fill(true);
+ return;
+ }
+
+ // Compare the two
+ for (int i = 0; i < slot_count; ++i)
+ {
+ if (level_array[i] != last_level_array[i])
+ {
+ dirty_array[i] |= true;
+ source_levels_change = true;
+ }
+ }
+}
+
+void sACNSource::storeReceivedPriorities(const uint8_t* pdata, uint16_t rx_slot_count)
+{
+ // Copy the last array back
+ last_priority_array = priority_array;
+ // Fill in the new array
+ if (rx_slot_count < priority_array.size())
+ priority_array.fill(0);
+
+ memcpy(priority_array.data(), pdata, rx_slot_count);
+
+ // Compare the two
+ for (int i = 0; i < DMX_SLOT_MAX; i++)
+ {
+ if (priority_array[i] != last_priority_array[i])
+ {
+ dirty_array[i] |= true;
+ source_levels_change = true;
+ }
+ }
+}
+
+sACNManager& sACNManager::Instance()
+{
+ static sACNManager s_instance;
+ return s_instance;
+}
+
+sACNManager::~sACNManager()
+{
+ QMutexLocker lock(&sACNManager_mutex);
+
+ // Stop and delete all the threads
+ for (QThread* thread : m_threadPool)
+ {
+ thread->exit();
+ thread->wait();
+ delete thread;
+ }
+ m_threadPool.clear();
+
+ // Stop the Tock layer
+ Tock_StopLib();
}
sACNManager::sACNManager() : QObject()
{
- // Start Tock layer
- Tock_StartLib();
+ // Start Tock layer
+ Tock_StartLib();
- // Start E1.31 Universe Discovery
- sACNDiscoveryTX::start();
- sACNDiscoveryRX::start();
+ // Start E1.31 Universe Discovery
+ sACNDiscoveryTX::start();
+ sACNDiscoveryRX::start();
+
+ // Create the threadpool
+ const int threadCount = QThread::idealThreadCount();
+ for (int i = 0; i < threadCount; ++i)
+ {
+ QThread* thread = new QThread(this);
+ thread->setObjectName(QStringLiteral("sACNManagerPool %1").arg(i));
+ thread->start(QThread::HighPriority);
+ m_threadPool.push_back(thread);
+ }
}
-static void strongPointerDeleteListener(sACNListener *obj)
+static void strongPointerDeleteListener(sACNListener* obj)
{
- obj->deleteLater();
- sACNManager::Instance().listenerDelete(obj);
+ obj->deleteLater();
+ sACNManager::Instance().listenerDelete(obj);
}
-static void strongPointerDeleteSender(sACNSentUniverse *obj)
+static void strongPointerDeleteSender(sACNSentUniverse* obj)
{
- obj->deleteLater();
- sACNManager::Instance().senderDelete(obj);
+ obj->deleteLater();
+ sACNManager::Instance().senderDelete(obj);
}
sACNManager::tListener sACNManager::getListener(quint16 universe)
{
- QMutexLocker locker(&sACNManager_mutex);
- // Notes on the memory management of sACNListeners :
- // This function creates a QSharedPointer to the listener, which is handed to the classes that
- // want to use it. It stores a QWeakPointer, which gets set to null when all instances of the shared
- // pointer are gone
-
- QSharedPointer strongPointer;
- if(!m_listenerHash.contains(universe))
- {
- qDebug() << "Creating listener for universe " << universe;
-
- // Create thread and move listener to thread
- QThread *thread = new QThread;
- thread->setObjectName(QString("Universe %1 RX").arg(universe));
-
- sACNListener *listener = new sACNListener(universe);
- listener->moveToThread(thread);
- connect(thread, &QThread::started, listener, &sACNListener::startReception);
- connect(thread, &QThread::finished, listener, &sACNListener::deleteLater);
- connect(thread, &QThread::finished, thread, &sACNListener::deleteLater);
- thread->start(QThread::HighPriority);
-
- // Emit sources from all, known, universes
- connect(listener, &sACNListener::sourceFound, this,
- [=](sACNSource *source)
- {
- emit sourceFound(universe, source);
- });
- connect(listener, &sACNListener::sourceLost, this,
- [=](sACNSource *source)
- {
- emit sourceLost(universe, source);
- });
- connect(listener, &sACNListener::sourceResumed, this,
- [=](sACNSource *source)
- {
- emit sourceResumed(universe, source);
- });
- connect(listener, &sACNListener::sourceChanged, this,
- [=](sACNSource *source)
- {
- emit sourceChanged(universe, source);
- });
-
- m_listenerThreads[universe] = thread;
-
- // Create strong pointer to return
- strongPointer = QSharedPointer(listener, strongPointerDeleteListener);
- m_listenerHash[universe] = strongPointer.toWeakRef();
- m_objToUniverse[listener] = universe;
-
- locker.unlock();
- emit newListener(universe);
- }
- else
- {
- strongPointer = m_listenerHash[universe].toStrongRef();
- }
- if(strongPointer.isNull())
- {
- #ifdef QT_GUI_LIB
- QMessageBox msgBox;
- msgBox.setText(tr("Unable to allocate listener object\r\n\r\nsACNView must close now"));
- msgBox.exec();
- #else
- qDebug() << "Unable to allocate listener object\r\n\r\nsACNView must close now";
- #endif
- qApp->exit(-1);
+ QMutexLocker locker(&sACNManager_mutex);
+ // Notes on the memory management of sACNListeners :
+ // This function creates a QSharedPointer to the listener, which is handed to the classes that
+ // want to use it. It stores a QWeakPointer, which gets set to null when all instances of the shared
+ // pointer are gone
+
+ QSharedPointer strongPointer;
+ if (!m_listenerHash.contains(universe))
+ {
+ qDebug() << "Creating listener for universe " << universe;
+
+ // Create listener and move to thread
+ sACNListener* listener = new sACNListener(universe);
+
+ // Choose a thread
+ QThread* thread = GetThread();
+ listener->moveToThread(thread);
+
+ connect(thread, &QThread::finished, listener, &sACNListener::deleteLater);
+ connect(thread, &QThread::finished, thread, &sACNListener::deleteLater);
+
+ // Emit sources from all, known, universes
+ connect(listener, &sACNListener::sourceFound, this,
+ [=](sACNSource* source)
+ {
+ emit sourceFound(universe, source);
+ });
+ connect(listener, &sACNListener::sourceLost, this,
+ [=](sACNSource* source)
+ {
+ emit sourceLost(universe, source);
+ });
+ connect(listener, &sACNListener::sourceResumed, this,
+ [=](sACNSource* source)
+ {
+ emit sourceResumed(universe, source);
+ });
+ connect(listener, &sACNListener::sourceChanged, this,
+ [=](sACNSource* source)
+ {
+ emit sourceChanged(universe, source);
+ });
- }
+ // Create strong pointer to return
+ strongPointer = QSharedPointer(listener, strongPointerDeleteListener);
+ m_listenerHash[universe] = strongPointer.toWeakRef();
+ m_objToUniverse[listener] = universe;
- return strongPointer;
+ // Start listening
+ QMetaObject::invokeMethod(listener, "startReception", Qt::QueuedConnection);
+
+ locker.unlock();
+ emit newListener(universe);
+ }
+ else
+ {
+ strongPointer = m_listenerHash[universe].toStrongRef();
+ }
+ if (strongPointer.isNull())
+ {
+#ifdef QT_GUI_LIB
+ QMessageBox msgBox;
+ msgBox.setText(tr("Unable to allocate listener object\r\n\r\nsACNView must close now"));
+ msgBox.exec();
+#else
+ qDebug() << "Unable to allocate listener object\r\n\r\nsACNView must close now";
+#endif
+ qApp->exit(-1);
+
+ }
+
+ return strongPointer;
}
-void sACNManager::listenerDelete(QObject *obj)
+void sACNManager::listenerDelete(QObject* obj)
{
- QMutexLocker locker(&sACNManager_mutex);
- int universe = m_objToUniverse[obj];
+ QMutexLocker locker(&sACNManager_mutex);
+ int universe = m_objToUniverse[obj];
- qDebug() << "Destroying listener for universe " << universe;
+ qDebug() << "Destroying listener for universe " << universe;
- m_listenerHash.remove(universe);
+ m_listenerHash.remove(universe);
- m_objToUniverse.remove(obj);
+ m_objToUniverse.remove(obj);
- m_listenerThreads[universe]->exit();
- m_listenerThreads[universe]->wait();
- m_listenerThreads.remove(universe);
- emit deletedListener(universe);
+ emit deletedListener(universe);
+}
+
+QThread* sACNManager::GetThread()
+{
+ QThread* result = m_threadPool[m_nextThread];
+ ++m_nextThread;
+ if (m_nextThread == m_threadPool.size())
+ m_nextThread = 0;
+ return result;
}
sACNManager::tSender sACNManager::createSender(CID cid, quint16 universe)
{
- qDebug() << "Creating sender for CID" << CID::CIDIntoQString(cid) << "universe" << universe;
+ qDebug() << "Creating sender for CID" << CID::CIDIntoQString(cid) << "universe" << universe;
- sACNSentUniverse *sender = new sACNSentUniverse(universe);
- sender->setCID(cid);
- connect(sender, &sACNSentUniverse::universeChange, this, &sACNManager::senderUniverseChanged);
- connect(sender, &sACNSentUniverse::cidChange, this, &sACNManager::senderCIDChanged);
+ sACNSentUniverse* sender = new sACNSentUniverse(universe);
+ sender->setCID(cid);
+ connect(sender, &sACNSentUniverse::universeChange, this, &sACNManager::senderUniverseChanged);
+ connect(sender, &sACNSentUniverse::cidChange, this, &sACNManager::senderCIDChanged);
- // Create strong pointer to return
- QSharedPointer strongPointer = QSharedPointer(sender, strongPointerDeleteSender);
- m_senderHash[cid][universe] = strongPointer.toWeakRef();
- m_objToUniverse[sender] = universe;
- m_objToCid[sender] = cid;
+ // Create strong pointer to return
+ QSharedPointer strongPointer = QSharedPointer(sender, strongPointerDeleteSender);
+ m_senderHash[cid][universe] = strongPointer.toWeakRef();
+ m_objToUniverse[sender] = universe;
+ m_objToCid[sender] = cid;
- sACNDiscoveryTX::getInstance()->sendDiscoveryPacketNow();
+ sACNDiscoveryTX::getInstance()->sendDiscoveryPacketNow();
- return strongPointer;
+ return strongPointer;
}
sACNManager::tSender sACNManager::getSender(quint16 universe, CID cid)
{
- QMutexLocker locker(&sACNManager_mutex);
- // Notes on the memory management of sACNSenders :
- // This function creates a QSharedPointer to the sender, which is handed to the classes that
- // want to use it. It stores a QWeakPointer, which gets set to null when all instances of the shared
- // pointer are gone
-
- QSharedPointer strongPointer;
- if(!m_senderHash.contains(cid))
- {
- strongPointer = createSender(cid, universe);
+ QMutexLocker locker(&sACNManager_mutex);
+ // Notes on the memory management of sACNSenders :
+ // This function creates a QSharedPointer to the sender, which is handed to the classes that
+ // want to use it. It stores a QWeakPointer, which gets set to null when all instances of the shared
+ // pointer are gone
+
+ QSharedPointer strongPointer;
+ if (!m_senderHash.contains(cid))
+ {
+ strongPointer = createSender(cid, universe);
+ }
+ else
+ {
+ if (!m_senderHash[cid].contains(universe)) {
+ strongPointer = createSender(cid, universe);
}
- else
- {
- if(!m_senderHash[cid].contains(universe)) {
- strongPointer = createSender(cid, universe);
- } else {
- strongPointer = m_senderHash[cid][universe].toStrongRef();
- }
+ else {
+ strongPointer = m_senderHash[cid][universe].toStrongRef();
}
+ }
- if(strongPointer.isNull())
- {
- #ifdef QT_GUI_LIB
- QMessageBox msgBox;
- msgBox.setText(tr("Unable to allocate sender object\r\n\r\nsACNView must close now"));
- msgBox.exec();
- #else
- qDebug() << "Unable to allocate sender object\r\n\r\nsACNView must close now";
- #endif
- qApp->exit(-1);
+ if (strongPointer.isNull())
+ {
+#ifdef QT_GUI_LIB
+ QMessageBox msgBox;
+ msgBox.setText(tr("Unable to allocate sender object\r\n\r\nsACNView must close now"));
+ msgBox.exec();
+#else
+ qDebug() << "Unable to allocate sender object\r\n\r\nsACNView must close now";
+#endif
+ qApp->exit(-1);
- }
+ }
- return strongPointer;
+ return strongPointer;
}
-void sACNManager::senderDelete(QObject *obj)
+void sACNManager::senderDelete(QObject* obj)
{
- if (!m_objToUniverse.contains(obj) && !m_objToCid.contains(obj))
- return;
+ if (!m_objToUniverse.contains(obj) && !m_objToCid.contains(obj))
+ return;
- QMutexLocker locker(&sACNManager_mutex);
- quint16 universe = m_objToUniverse[obj];
- CID cid = m_objToCid[obj];
+ QMutexLocker locker(&sACNManager_mutex);
+ quint16 universe = m_objToUniverse[obj];
+ CID cid = m_objToCid[obj];
- qDebug() << "Destroying sender for CID" << CID::CIDIntoQString(cid) << "universe" << universe;
+ qDebug() << "Destroying sender for CID" << CID::CIDIntoQString(cid) << "universe" << universe;
- m_senderHash[cid].remove(universe);
+ m_senderHash[cid].remove(universe);
- m_objToUniverse.remove(obj);
- m_objToCid.remove(obj);
- emit deletedSender(cid, universe);
+ m_objToUniverse.remove(obj);
+ m_objToCid.remove(obj);
+ emit deletedSender(cid, universe);
}
void sACNManager::senderUniverseChanged()
{
- if (!m_objToUniverse.contains(sender()) && !m_objToCid.contains(sender()))
- return;
+ if (!m_objToUniverse.contains(sender()) && !m_objToCid.contains(sender()))
+ return;
- sACNSentUniverse *sACNSender = (sACNSentUniverse*)sender();
- if (!sACNSender) return;
- CID cid = m_objToCid[sender()];
- quint16 oldUniverse = m_objToUniverse[sender()];
- quint16 newUniverse = sACNSender->universe();
+ sACNSentUniverse* sACNSender = (sACNSentUniverse*)sender();
+ if (!sACNSender) return;
+ CID cid = m_objToCid[sender()];
+ quint16 oldUniverse = m_objToUniverse[sender()];
+ quint16 newUniverse = sACNSender->universe();
- m_senderHash[cid][newUniverse] = m_senderHash[cid][oldUniverse];
- m_senderHash[cid].remove(oldUniverse);
+ m_senderHash[cid][newUniverse] = m_senderHash[cid][oldUniverse];
+ m_senderHash[cid].remove(oldUniverse);
- m_objToUniverse[sender()] = newUniverse;
+ m_objToUniverse[sender()] = newUniverse;
- qDebug() << "Sender for CID" << CID::CIDIntoQString(cid) << "was universe" << oldUniverse << "now universe" << newUniverse;
+ qDebug() << "Sender for CID" << CID::CIDIntoQString(cid) << "was universe" << oldUniverse << "now universe" << newUniverse;
}
void sACNManager::senderCIDChanged()
{
- if (!m_objToUniverse.contains(sender()) && !m_objToCid.contains(sender()))
- return;
+ if (!m_objToUniverse.contains(sender()) && !m_objToCid.contains(sender()))
+ return;
- sACNSentUniverse *sACNSender = (sACNSentUniverse*)sender();
- if (!sACNSender) return;
- CID oldCID = m_objToCid[sender()];
- CID newCID = sACNSender->cid();
+ sACNSentUniverse* sACNSender = (sACNSentUniverse*)sender();
+ if (!sACNSender) return;
+ CID oldCID = m_objToCid[sender()];
+ CID newCID = sACNSender->cid();
- m_senderHash[newCID] = m_senderHash[oldCID];
- m_senderHash.remove(oldCID);
+ m_senderHash[newCID] = m_senderHash[oldCID];
+ m_senderHash.remove(oldCID);
- m_objToCid[sender()] = newCID;
+ m_objToCid[sender()] = newCID;
- qDebug() << "Sender CID" << CID::CIDIntoQString(oldCID) << "now CID" << CID::CIDIntoQString(newCID);
+ qDebug() << "Sender CID" << CID::CIDIntoQString(oldCID) << "now CID" << CID::CIDIntoQString(newCID);
}
diff --git a/src/sacn/streamingacn.h b/src/sacn/streamingacn.h
index 68eed843..ede0cc75 100644
--- a/src/sacn/streamingacn.h
+++ b/src/sacn/streamingacn.h
@@ -21,13 +21,11 @@
#ifndef STREAMINGACN_H
#define STREAMINGACN_H
-#include
#include
#include
#include
#include
#include
-#include
#include
#include "CID.h"
@@ -35,6 +33,8 @@
#include "streamcommon.h"
#include "fpscounter.h"
+#include
+
// Forward Declarations
class sACNListener;
class sACNSentUniverse;
@@ -42,10 +42,10 @@ class sACNSource;
enum StreamingACNProtocolVersion
{
- sACNProtocolUnknown = 0,
- sACNProtocolDraft,
- sACNProtocolRelease,
- sACNProtocolPathwaySecure // Pathway Connectivity Secure DMX Protocol
+ sACNProtocolUnknown = 0,
+ sACNProtocolDraft,
+ sACNProtocolRelease,
+ sACNProtocolPathwaySecure // Pathway Connectivity Secure DMX Protocol
};
QString GetProtocolVersionString(StreamingACNProtocolVersion value);
@@ -53,116 +53,128 @@ QString GetProtocolVersionString(StreamingACNProtocolVersion value);
// The sACNManager class is a singleton that manages the lifespan of sACNTransmitters and sACNListeners.
class sACNManager : public QObject
{
- Q_OBJECT
+ Q_OBJECT
public:
- static sACNManager &Instance();
+ static sACNManager& Instance();
+
+ typedef QSharedPointer tListener;
+ typedef QWeakPointer wListener;
+
+ typedef QSharedPointer tSender;
+ typedef QWeakPointer wSender;
- typedef QSharedPointer tListener;
- typedef QSharedPointer tSender;
+ ~sACNManager();
+
+ inline static tock GetTock() { return Tock_GetTock(); }
+ inline static qint64 nsecsElapsed() { return Tock_GetTock().Get().count(); }
+ inline static qint64 elapsed() { return std::chrono::duration_cast(Tock_GetTock().Get()).count(); }
+ inline static qreal secsElapsed() { return std::chrono::duration(Tock_GetTock().Get()).count(); }
public slots:
- void listenerDelete(QObject *obj = Q_NULLPTR);
+ void listenerDelete(QObject* obj = Q_NULLPTR);
- void senderDelete(QObject *obj = Q_NULLPTR);
+ void senderDelete(QObject* obj = Q_NULLPTR);
private:
- sACNManager();
- QMutex sACNManager_mutex;
+ sACNManager();
+ QMutex sACNManager_mutex;
+
+ QHash m_objToUniverse;
+ QHash m_objToCid;
- QHash m_objToUniverse;
- QHash m_objToCid;
+ QHash m_listenerHash;
- QHash> m_listenerHash;
- QHash m_listenerThreads;
+ // The pool of threads to use for listener objects
+ std::vector m_threadPool;
+ size_t m_nextThread = 0;
- tSender createSender(CID cid, quint16 universe);
- QHash> > m_senderHash;
+ QThread* GetThread();
+
+ tSender createSender(CID cid, quint16 universe);
+ QHash > m_senderHash;
public:
- tListener getListener(quint16 universe);
- const decltype(m_listenerHash) &getListenerList() { return m_listenerHash; }
+ tListener getListener(quint16 universe);
+ const decltype(m_listenerHash)& getListenerList() const { return m_listenerHash; }
- tSender getSender(quint16 universe, CID cid = CID::CreateCid());
- const decltype(m_senderHash) &getSenderList() { return m_senderHash; }
+ tSender getSender(quint16 universe, CID cid = CID::CreateCid());
+ const decltype(m_senderHash)& getSenderList() const { return m_senderHash; }
signals:
- void newSender();
- void deletedSender(CID cid, quint16 universe);
+ void newSender();
+ void deletedSender(CID cid, quint16 universe);
- void newListener(quint16 universe);
- void deletedListener(quint16 universe);
+ void newListener(quint16 universe);
+ void deletedListener(quint16 universe);
- void sourceFound(quint16 universe, sACNSource *source);
- void sourceLost(quint16 universe, sACNSource *source);
- void sourceResumed(quint16 universe, sACNSource *source);
- void sourceChanged(quint16 universe, sACNSource *source);
+ void sourceFound(quint16 universe, sACNSource* source);
+ void sourceLost(quint16 universe, sACNSource* source);
+ void sourceResumed(quint16 universe, sACNSource* source);
+ void sourceChanged(quint16 universe, sACNSource* source);
private slots:
- void senderUniverseChanged();
- void senderCIDChanged();
+ void senderUniverseChanged();
+ void senderCIDChanged();
private:
};
-class sACNSource : public QObject
+class sACNSource
{
- Q_OBJECT
+ Q_DISABLE_COPY(sACNSource)
public:
- explicit sACNSource();
- CID src_cid;
- bool src_valid;
- bool src_stable;
- quint8 lastseq;
- ttimer active; //If this expires, we haven't received any data in over a second
- //The per-channel priority alternate start code policy requires we detect the source only after
- //a STARTCODE_PRIORITY packet was received or 1.5 seconds have expired
- bool waited_for_dd;
- bool doing_dmx; //if true, we are processing dmx data from this source
- bool doing_per_channel; //If true, we are tracking per-channel priority messages for this source
- bool isPreview;
- ttimer priority_wait; //if !initially_notified, used to track if a source is finally detected
- //(either by receiving priority or timeout). If doing_per_channel,
- //used to time out the 0xdd packets to see if we lost per-channel priority
- quint16 universe;
- quint8 level_array[DMX_SLOT_MAX];
- quint16 slot_count; // Number of slots actually received
- quint8 priority_array[DMX_SLOT_MAX];
- quint8 last_level_array[DMX_SLOT_MAX];
- quint8 last_priority_array[DMX_SLOT_MAX];
- bool dirty_array[DMX_SLOT_MAX]; // Set if an individual level or priority has changed
- bool source_params_change; // Set if any parameter of the source changes between packets
- bool source_levels_change;
-
- quint8 priority;
- quint16 synchronization;
- sACNManager::tListener sync_listener;
- QString name;
- QHostAddress ip;
- FpsCounter fpscounter;
- // The number of sequence errors from this source
- int seqErr;
- // The number of jumps (increments by anything other than 1) of this source
- int jumps;
- // Protocol Version
- StreamingACNProtocolVersion protocol_version;
-
- // Pathways Secure DMX
- struct {
- bool passwordOk = false;
- bool sequenceOk = false;
- bool digestOk = false;
- bool isSecure() const {
- return passwordOk && sequenceOk && digestOk;
- }
- } pathway_secure;
-
-public slots:
- void resetSeqErr() {
- seqErr = 0;
- }
-
- void resetJumps() {
- jumps = 0;
- }
+ explicit sACNSource(const CID& source_cid, uint16_t universe);
+ const CID src_cid;
+ const uint16_t universe = 0;
+ bool src_valid = false;
+ bool src_stable = false;
+ uint8_t lastseq = 0;
+ ttimer active; //If this expires, we haven't received any data in over a second
+ //The per-channel priority alternate start code policy requires we detect the source only after
+ //a STARTCODE_PRIORITY packet was received or 1.5 seconds have expired
+ bool waited_for_dd = false;
+ bool doing_dmx = false; //if true, we are processing dmx data from this source
+ bool doing_per_channel = false; // If true, we are tracking per-channel priority messages for this source
+ bool isPreview = false;
+ ttimer priority_wait; // If !initially_notified, used to track if a source is finally detected
+ // (either by receiving priority or timeout). If doing_per_channel,
+ // used to time out the 0xdd packets to see if we lost per-channel priority
+ std::array level_array = {};
+ std::array last_level_array = {};
+ std::array priority_array = {};
+ std::array last_priority_array = {};
+ std::array dirty_array = {}; // Set if an individual level or priority has changed
+ quint16 slot_count = 0; // Number of slots actually received
+ bool source_params_change = false; // Set if any parameter of the source changes between packets
+ bool source_levels_change = false; // Set if any leveles
+
+ uint8_t priority = 0;
+ uint16_t synchronization = 0;
+ sACNManager::tListener sync_listener;
+ QString name;
+ QHostAddress ip;
+ FpsCounter fpscounter;
+ // The number of sequence errors from this source
+ unsigned int seqErr = 0;
+ // The number of jumps (increments by anything other than 1) of this source
+ unsigned int jumps = 0;
+ // Protocol Version
+ StreamingACNProtocolVersion protocol_version;
+
+ // Pathways Secure DMX
+ struct {
+ bool passwordOk = false;
+ bool sequenceOk = false;
+ bool digestOk = false;
+ bool isSecure() const { return passwordOk && sequenceOk && digestOk; }
+ } pathway_secure;
+
+ void resetSeqErr() { seqErr = 0; }
+ void resetJumps() { jumps = 0; }
+
+ // Have received new levels, store and check for changes
+ void storeReceivedLevels(const uint8_t* pdata, uint16_t rx_slot_count);
+ // Have received new priority array, store and check for changes
+ void storeReceivedPriorities(const uint8_t* pdata, uint16_t rx_slot_count);
};
#endif // STREAMINGACN_H
diff --git a/src/ui/aboutdialog.cpp b/src/ui/aboutdialog.cpp
index e3067126..4fdd69a7 100644
--- a/src/ui/aboutdialog.cpp
+++ b/src/ui/aboutdialog.cpp
@@ -93,7 +93,7 @@ aboutDialog::aboutDialog(QWidget* parent) :
// Sort list
struct {
- bool operator()(const QWeakPointer& a, const QWeakPointer& b) const
+ bool operator()(const sACNManager::wListener& a, const sACNManager::wListener& b) const
{
auto aStrong = a.toStrongRef();
auto bStrong = b.toStrongRef();
diff --git a/src/ui/bigdisplay.cpp b/src/ui/bigdisplay.cpp
index 3e700813..f511e21f 100644
--- a/src/ui/bigdisplay.cpp
+++ b/src/ui/bigdisplay.cpp
@@ -2,150 +2,183 @@
#include "ui_bigdisplay.h"
#include "preferences.h"
-BigDisplay::BigDisplay(int universe, quint16 address, QWidget *parent) :
- QWidget(parent),
- ui(new Ui::BigDisplay)
+#include "sacn/sacnlistener.h"
+
+BigDisplay::BigDisplay(int universe, quint16 slot_index, QWidget* parent)
+ : QWidget(parent)
+ , ui(new Ui::BigDisplay)
+ , m_listener(sACNManager::Instance().getListener(universe))
{
- ui->setupUi(this);
-
- // 8bit controls
- ui->spinBox_8->setAddress(universe, address);
- connect(ui->spinBox_8, &monitorspinbox::dataReady, this, &BigDisplay::dataReady);
-
- // 16bit controls
- ui->spinBox_16_Coarse->setAddress(universe, address);
- connect(ui->spinBox_16_Coarse, &monitorspinbox::dataReady, this, &BigDisplay::dataReady);
- //--
- ui->spinBox_16_Fine->setAddress(universe, address + 1);
- connect(ui->spinBox_16_Fine, &monitorspinbox::dataReady, this, &BigDisplay::dataReady);
-
- // Colour controls
- ui->spinBox_RGB_1->setAddress(universe, address);
- connect(ui->spinBox_RGB_1, &monitorspinbox::dataReady, this, &BigDisplay::dataReady);
- //--
- ui->spinBox_RGB_2->setAddress(universe, address + 1);
- connect(ui->spinBox_RGB_2, &monitorspinbox::dataReady, this, &BigDisplay::dataReady);
- //--
- ui->spinBox_RGB_3->setAddress(universe, address + 2);
- connect(ui->spinBox_RGB_3, &monitorspinbox::dataReady, this, &BigDisplay::dataReady);
-
- // Window
- this->setWindowTitle(tr("Big Display Universe %1").arg(universe));
- this->setWindowIcon(parent->windowIcon());
-
- m_level = 0;
- displayLevel();
+ ui->setupUi(this);
+
+ int address = slot_index + 1;
+
+ // 8bit controls
+ ui->spinBox_8->setMinimum(MIN_DMX_ADDRESS);
+ ui->spinBox_8->setMaximum(MAX_DMX_ADDRESS);
+ ui->spinBox_8->setValue(address);
+
+ // 16bit controls
+ ui->spinBox_16_Coarse->setMinimum(MIN_DMX_ADDRESS);
+ ui->spinBox_16_Coarse->setMaximum(MAX_DMX_ADDRESS);
+ ui->spinBox_16_Fine->setMinimum(MIN_DMX_ADDRESS);
+ ui->spinBox_16_Fine->setMaximum(MAX_DMX_ADDRESS);
+
+ ui->spinBox_16_Coarse->setValue(address);
+ ui->spinBox_16_Fine->setValue(address + 1);
+
+ // Colour controls
+ ui->spinBox_RGB_1->setMinimum(MIN_DMX_ADDRESS);
+ ui->spinBox_RGB_1->setMaximum(MAX_DMX_ADDRESS);
+ ui->spinBox_RGB_2->setMinimum(MIN_DMX_ADDRESS);
+ ui->spinBox_RGB_2->setMaximum(MAX_DMX_ADDRESS);
+ ui->spinBox_RGB_3->setMinimum(MIN_DMX_ADDRESS);
+ ui->spinBox_RGB_3->setMaximum(MAX_DMX_ADDRESS);
+
+ ui->spinBox_RGB_1->setValue(address);
+ ui->spinBox_RGB_2->setValue(address + 1);
+ ui->spinBox_RGB_3->setValue(address + 2);
+
+ // Window
+ this->setWindowTitle(tr("Big Display Universe %1").arg(universe));
+ this->setWindowIcon(parent->windowIcon());
+
+ updateLevels();
+
+ // Refresh every four DMX frames or so
+ startTimer(88);
}
BigDisplay::~BigDisplay()
{
- delete ui;
+ delete ui;
}
void BigDisplay::displayLevel()
{
- QPalette palette = ui->lcdNumber->palette();
- QColor colour(m_level | 0xFF000000);
-
- // Display format
- if((ui->tabWidget->currentIndex() == tabModes_bit8) ||
- (ui->tabWidget->currentIndex() == tabModes_bit16) )
+ QPalette palette = ui->lcdNumber->palette();
+
+ // Display format
+ if ((ui->tabWidget->currentIndex() == tabModes_bit8) ||
+ (ui->tabWidget->currentIndex() == tabModes_bit16))
+ {
+ ui->lcdNumber->setDigitCount(5);
+ // Grey when not active
+ palette.setColor(QPalette::WindowText, m_active ? Qt::red : Qt::gray);
+ ui->lcdNumber->setPalette(palette);
+
+ switch (Preferences::Instance().GetDisplayFormat())
+ {
+ default:
+ case DisplayFormat::DECIMAL:
{
- ui->lcdNumber->setDigitCount(5);
- palette.setColor(QPalette::WindowText, Qt::red);
- ui->lcdNumber->setPalette(palette);
-
- switch (Preferences::Instance().GetDisplayFormat())
- {
- default:
- case DisplayFormat::DECIMAL:
- {
- ui->lcdNumber->setMode(QLCDNumber::Dec);
- ui->lcdNumber->display((int)m_level);
- break;
- }
- case DisplayFormat::HEXADECIMAL:
- {
- ui->lcdNumber->setMode(QLCDNumber::Hex);
- ui->lcdNumber->display((int)m_level);
- break;
- }
- case DisplayFormat::PERCENT:
- {
- ui->lcdNumber->setMode(QLCDNumber::Dec);
- // Display percent with an optional fraction
- if(ui->tabWidget->currentIndex() == tabModes_bit8)
- {
- ui->lcdNumber->display(HTOPT[m_level & 0xFF]);
- }
- else
- {
- int percent = HTOPT[(m_level & 0xFF00) >> 8];
- int fraction = m_level & 0xFF;
- double value = percent + fraction/255.0;
- ui->lcdNumber->display(value);
- }
- break;
- }
- }
+ ui->lcdNumber->setMode(QLCDNumber::Dec);
+ ui->lcdNumber->display(m_level);
+ break;
}
-
- if(ui->tabWidget->currentIndex() == tabModes_rgb)
+ case DisplayFormat::HEXADECIMAL:
{
- // Only ever display RGB as hex
-
- palette.setColor(QPalette::WindowText, colour);
- ui->lcdNumber->setPalette(palette);
- ui->lcdNumber->setDigitCount(6);
- ui->lcdNumber->setMode(QLCDNumber::Hex);
- ui->lcdNumber->display((int)m_level);
+ ui->lcdNumber->setMode(QLCDNumber::Hex);
+ ui->lcdNumber->display(m_level);
+ break;
}
+ case DisplayFormat::PERCENT:
+ {
+ ui->lcdNumber->setMode(QLCDNumber::Dec);
+ // Display percent with an optional fraction
+ if (ui->tabWidget->currentIndex() == tabModes_bit8)
+ {
+ ui->lcdNumber->display(HTOPT[m_level & 0xFF]);
+ }
+ else
+ {
+ int percent = HTOPT[(m_level & 0xFF00) >> 8];
+ int fraction = m_level & 0xFF;
+ double value = percent + fraction / 255.0;
+ ui->lcdNumber->display(value);
+ }
+ break;
+ }
+ }
+ }
+
+ if (ui->tabWidget->currentIndex() == tabModes_rgb)
+ {
+ // Only ever display RGB as hex
+ QColor colour(static_cast(m_level) | 0xFF000000);
+ palette.setColor(QPalette::WindowText, colour);
+ ui->lcdNumber->setPalette(palette);
+ ui->lcdNumber->setDigitCount(6);
+ ui->lcdNumber->setMode(QLCDNumber::Hex);
+ ui->lcdNumber->display(m_level);
+ }
}
-void BigDisplay::dataReady(int universe, quint16 address, QPointF data)
+void BigDisplay::timerEvent(QTimerEvent* /*ev*/)
{
- quint8 level = data.y() > 0 ? data.y() : 0;
-
- switch (ui->tabWidget->currentIndex())
- {
- case tabModes_bit8:
- if (address == ui->spinBox_8->address() && universe == ui->spinBox_8->universe())
- m_level = level;
-
- break;
-
- case tabModes_bit16:
- if (address == ui->spinBox_16_Coarse->address() && universe == ui->spinBox_16_Coarse->universe())
- m_level = (m_level & 0x00FF) | (0xFF00 & (level << 8));
-
- if (address == ui->spinBox_16_Fine->address() && universe == ui->spinBox_16_Fine->universe())
- m_level = (m_level & 0xFF00) | (0x00FF & level);
-
- break;
-
- case tabModes_rgb:
- if (address == ui->spinBox_RGB_1->address() && universe == ui->spinBox_RGB_1->universe())
- m_level = (m_level & 0x00FFFFu) | (0xFF0000u & ((quint32)level << 16));
-
- if (address == ui->spinBox_RGB_2->address() && universe == ui->spinBox_RGB_2->universe())
- m_level = (m_level & 0xFF00FFu) | (0x00FF00u & ((quint32)level << 8));
-
- if (address == ui->spinBox_RGB_3->address() && universe == ui->spinBox_RGB_3->universe())
- m_level = (m_level & 0xFFFF00u) | (0x0000FFu & level);
-
- break;
-
- default:
- m_level = -1;
- break;
- }
+ updateLevels();
+}
- displayLevel();
+void BigDisplay::on_tabWidget_currentChanged(int /*index*/)
+{
+ updateLevels();
}
-void BigDisplay::on_tabWidget_currentChanged(int index)
+void BigDisplay::updateLevels()
{
- Q_UNUSED(index);
- m_level = 0;
- displayLevel();
+ // Get and calculate levels
+ const auto levels = m_listener->mergedLevelsOnly();
+
+ int level = -1;
+
+ switch (ui->tabWidget->currentIndex())
+ {
+ default: break;
+
+ case tabModes_bit8:
+ {
+ level = levels[ui->spinBox_8->value() - 1];
+ }
+ break;
+
+ case tabModes_bit16:
+ {
+ const int coarse = levels[ui->spinBox_16_Coarse->value() - 1];
+ const int fine = levels[ui->spinBox_16_Fine->value() - 1];
+ if (coarse >= 0 && fine >= 0)
+ {
+ level = static_cast(coarse) << 8 | static_cast(fine);
+ }
+ }
+ break;
+
+ case tabModes_rgb:
+ {
+ const int red = levels[ui->spinBox_RGB_1->value() - 1];
+ const int green = levels[ui->spinBox_RGB_2->value() - 1];
+ const int blue = levels[ui->spinBox_RGB_3->value() - 1];
+ if (red >= 0 && green >= 0 && blue >= 0)
+ {
+ level = static_cast(red) << 16 | static_cast(green) << 8 | static_cast(blue);
+ }
+ }
+ break;
+ }
+
+ if (level < 0)
+ {
+ if (!m_active)
+ return;
+ m_active = false;
+ }
+ else
+ {
+ if (m_active && m_level == level)
+ return;
+
+ m_level = level;
+ m_active = true;
+ }
+
+ displayLevel();
}
diff --git a/src/ui/bigdisplay.h b/src/ui/bigdisplay.h
index 4b5e27e5..451150dc 100644
--- a/src/ui/bigdisplay.h
+++ b/src/ui/bigdisplay.h
@@ -3,38 +3,42 @@
#include
#include "consts.h"
+#include "streamingacn.h"
namespace Ui {
-class BigDisplay;
+ class BigDisplay;
}
class BigDisplay : public QWidget
{
- Q_OBJECT
+ Q_OBJECT
public:
- explicit BigDisplay(int universe, quint16 address, QWidget *parent = 0);
- ~BigDisplay();
+ explicit BigDisplay(int universe, quint16 slot_index, QWidget* parent = 0);
+ ~BigDisplay();
private:
- Ui::BigDisplay *ui;
+ Ui::BigDisplay* ui;
- enum tabModes
- {
- tabModes_bit8,
- tabModes_bit16,
- tabModes_rgb
- };
+ enum tabModes
+ {
+ tabModes_bit8,
+ tabModes_bit16,
+ tabModes_rgb
+ };
-private slots:
- void dataReady(int universe, quint16 address, QPointF data);
+protected:
+ void timerEvent(QTimerEvent* ev) override;
- void on_tabWidget_currentChanged(int index);
+private slots:
+ void on_tabWidget_currentChanged(int index);
private:
- void displayLevel();
-
- quint32 m_level;
+ void updateLevels();
+ void displayLevel();
+ sACNManager::tListener m_listener;
+ int m_level = 0;
+ bool m_active = false;
};
#endif // BIGDISPLAY_H
diff --git a/src/ui/glscopewindow.cpp b/src/ui/glscopewindow.cpp
new file mode 100644
index 00000000..33ad50a6
--- /dev/null
+++ b/src/ui/glscopewindow.cpp
@@ -0,0 +1,580 @@
+// Copyright 2023 Electronic Theatre Controls, Inc. or its affiliates
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#include "glscopewindow.h"
+
+#include "widgets/glscopewidget.h"
+#include "widgets/steppedspinbox.h"
+
+#include
+#include
+#include
+
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+
+#include
+
+GlScopeWindow::GlScopeWindow(int universe, QWidget* parent)
+ : QWidget(parent)
+ , m_defaultUniverse(universe)
+{
+ setWindowTitle(tr("Scope"));
+ setWindowIcon(QIcon(QStringLiteral(":/icons/scope.png")));
+
+ QBoxLayout* layout = new QVBoxLayout(this);
+ layout->setContentsMargins(2, 2, 2, 2);
+ m_splitter = new QSplitter(Qt::Vertical, this);
+ layout->addWidget(m_splitter);
+
+ // The oscilloscope view
+ QWidget* scopeWidget = new QWidget(this);
+ {
+ QBoxLayout* layout = new QVBoxLayout(scopeWidget);
+ layout->setContentsMargins(0, 0, 0, 0);
+ layout->setSpacing(1);
+
+ m_scope = new GlScopeWidget(scopeWidget);
+ connect(m_scope->model(), &ScopeModel::runningChanged, this, &GlScopeWindow::onRunningChanged);
+ connect(m_scope->model(), &ScopeModel::triggered, this, &GlScopeWindow::onTriggered);
+ layout->addWidget(m_scope);
+
+ m_scrollTime = new QScrollBar(Qt::Horizontal, scopeWidget);
+ connect(m_scrollTime, &QScrollBar::sliderMoved, this, &GlScopeWindow::onTimeSliderMoved);
+ layout->addWidget(m_scrollTime);
+ m_disableWhenRunning.push_back(m_scrollTime);
+ }
+ m_splitter->addWidget(scopeWidget);
+
+ QWidget* confWidget = new QWidget(this);
+ {
+ QBoxLayout* layoutConf = new QHBoxLayout(confWidget);
+ {
+ QLabel* lbl = nullptr;
+ QGroupBox* grpScope = new QGroupBox(tr("Scope"), confWidget);
+ QGridLayout* layoutGrp = new QGridLayout(grpScope);
+
+ int row = 0;
+ m_btnStop = new QPushButton(tr("Stop"), confWidget);
+ m_btnStop->setCheckable(true);
+ connect(m_btnStop, &QPushButton::clicked, m_scope->model(), &ScopeModel::stop);
+ layoutGrp->addWidget(m_btnStop, row, 0);
+
+ m_btnStart = new QPushButton(tr("Start"), confWidget);
+ m_btnStart->setCheckable(true);
+ connect(m_btnStart, &QPushButton::clicked, m_scope->model(), &ScopeModel::start);
+ layoutGrp->addWidget(m_btnStart, row, 1);
+
+ ++row;
+
+ lbl = new QLabel(tr("Store:"), confWidget);
+ layoutGrp->addWidget(lbl, row, 0);
+ m_recordMode = new QComboBox(confWidget);
+ m_recordMode->addItems({ tr("All Packets"), tr("Level Changes") });
+ connect(m_recordMode, QOverload::of(&QComboBox::activated), this, &GlScopeWindow::setRecordMode);
+ layoutGrp->addWidget(m_recordMode, row, 1);
+
+ m_disableWhenRunning.push_back(lbl);
+ m_disableWhenRunning.push_back(m_recordMode);
+
+ ++row;
+
+ lbl = new QLabel(tr("Run For:"), confWidget);
+ layoutGrp->addWidget(lbl, row, 0);
+ m_spinRunTime = new QSpinBox(confWidget);
+ m_spinRunTime->setRange(0, 300); // Five minutes
+ m_spinRunTime->setSingleStep(10);
+ //! Seconds suffix
+ m_spinRunTime->setSuffix(tr("s"));
+ m_spinRunTime->setSpecialValueText(tr("Forever"));
+ connect(m_spinRunTime, QOverload::of(&QSpinBox::valueChanged), m_scope->model(), &ScopeModel::setRunTime);
+ connect(m_scope->model(), &ScopeModel::runTimeChanged, m_spinRunTime, &QSpinBox::setValue);
+ layoutGrp->addWidget(m_spinRunTime, row, 1);
+
+ m_disableWhenRunning.push_back(lbl);
+ m_disableWhenRunning.push_back(m_spinRunTime);
+
+ ++row;
+
+ // Scope scale configuration
+ lbl = new QLabel(tr("Vertical Scale:"), confWidget);
+ layoutGrp->addWidget(lbl, row, 0);
+ QComboBox* verticalScale = new QComboBox(confWidget);
+ verticalScale->addItems({ tr("Percent"), tr("DMX8"), tr("DMX16") });
+ connect(verticalScale, QOverload::of(&QComboBox::activated), this, &GlScopeWindow::setVerticalScaleMode);
+ verticalScale->setCurrentIndex(static_cast(m_scope->verticalScaleMode()));
+ layoutGrp->addWidget(verticalScale, row, 1);
+
+ ++row;
+ lbl = new QLabel(tr("Time Scale:"), confWidget);
+ layoutGrp->addWidget(lbl, row, 0);
+
+ m_spinTimeScale = new SteppedSpinBox(confWidget);
+ m_spinTimeScale->setStepList({ 5,10,20,50,100,200,500,1000,2000 }); // Milliseconds
+ //! Milliseconds suffix
+ m_spinTimeScale->setSuffix(tr("ms"));
+ m_spinTimeScale->setValue(m_scope->timeDivisions());
+ connect(m_spinTimeScale, QOverload::of(&QSpinBox::valueChanged), this, &GlScopeWindow::onTimeDivisionsChanged);
+ connect(m_scope, &GlScopeWidget::timeDivisionsChanged, m_spinTimeScale, &QSpinBox::setValue);
+ layoutGrp->addWidget(m_spinTimeScale, row, 1);
+
+ ++row;
+ m_timeFormat = new QComboBox(confWidget);
+ m_timeFormat->addItems({
+ //! Elapsed time
+ tr("Elapsed"),
+ //! Wallclock time
+ tr("Wallclock")
+ });
+
+ connect(m_timeFormat, QOverload::of(&QComboBox::currentIndexChanged), this, &GlScopeWindow::setTimeFormat);
+ layoutGrp->addWidget(m_timeFormat, row, 0, 1, 2);
+
+ // Divider
+ ++row;
+ QFrame* line = new QFrame(confWidget);
+ line->setFrameShape(QFrame::HLine);
+ layoutGrp->addWidget(line, row, 0, 1, 2);
+
+ ++row;
+ // Trigger setup
+ m_triggerType = new QComboBox(confWidget);
+ m_triggerType->addItems({
+ //! No trigger, runs immediately
+ tr("Free Run"),
+ //! Triggers when above the target level
+ tr("Above"),
+ //! Triggers when below the target level
+ tr("Below"),
+ //! Triggers when passes through or leaves the target level
+ tr("Crossed Level")
+ });
+ connect(m_triggerType, QOverload::of(&QComboBox::currentIndexChanged), this, &GlScopeWindow::setTriggerType);
+ layoutGrp->addWidget(m_triggerType, row, 0, 1, 2);
+
+ m_disableWhenRunning.push_back(m_triggerType);
+
+ ++row;
+ lbl = new QLabel(tr("Trigger Level:"), confWidget);
+ layoutGrp->addWidget(lbl, row, 0);
+ m_spinTriggerLevel = new QSpinBox(this);
+ m_spinTriggerLevel->setRange(0, 65535);
+ connect(m_spinTriggerLevel, QOverload::of(&QSpinBox::valueChanged), m_scope->model(), &ScopeModel::setTriggerLevel);
+ layoutGrp->addWidget(m_spinTriggerLevel, row, 1);
+
+ m_triggerSetup.push_back(lbl);
+ m_triggerSetup.push_back(m_spinTriggerLevel);
+
+ // Set initial trigger type and values
+ m_triggerType->setCurrentIndex(0);
+ m_spinTriggerLevel->setValue(127);
+
+ ++row;
+ m_chkSyncViews = new QCheckBox(tr("Trigger Receive Views"), confWidget);
+ layoutGrp->addWidget(m_chkSyncViews, row, 0, 1, 2, Qt::AlignHCenter);
+ m_disableWhenRunning.push_back(m_chkSyncViews);
+
+ // Spacer at the bottom
+ ++row;
+ layoutGrp->setRowStretch(row, 1);
+
+ layoutConf->addWidget(grpScope);
+ }
+
+ {
+ QGroupBox* grpChans = new QGroupBox(tr("Channels"), confWidget);
+ QBoxLayout* layoutGrp = new QVBoxLayout(grpChans);
+
+ m_tableView = new QTableView(this);
+ m_tableView->verticalHeader()->hide();
+ m_tableView->setSelectionMode(QAbstractItemView::ExtendedSelection);
+ m_tableView->setAlternatingRowColors(true);
+ m_tableView->setItemDelegateForColumn(ScopeModel::COL_COLOUR, new ColorPickerDelegate(this));
+ m_tableView->setVerticalScrollBarPolicy(Qt::ScrollBarAlwaysOn);
+ m_tableView->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
+
+ // Sorting
+ QSortFilterProxyModel* sortProxy = new QSortFilterProxyModel(m_tableView);
+ sortProxy->setSourceModel(m_scope->model());
+ sortProxy->setSortRole(ScopeModel::DataSortRole);
+
+ m_tableView->setModel(sortProxy);
+ m_tableView->setSortingEnabled(true);
+ m_tableView->sortByColumn(0, Qt::AscendingOrder);
+ m_tableView->horizontalHeader()->setStretchLastSection(true);
+ m_tableView->resizeColumnsToContents();
+
+ layoutGrp->addWidget(m_tableView);
+
+ {
+ QBoxLayout* layoutBtns = new QHBoxLayout();
+
+ QPushButton* addChan = new QPushButton(tr("Add"), confWidget);
+ connect(addChan, &QPushButton::clicked, this, &GlScopeWindow::addTrace);
+ layoutBtns->addWidget(addChan);
+ m_disableWhenRunning.push_back(addChan);
+
+ QPushButton* removeChan = new QPushButton(tr("Remove"), confWidget);
+ connect(removeChan, &QPushButton::clicked, this, &GlScopeWindow::removeTrace);
+ layoutBtns->addWidget(removeChan);
+ m_disableWhenRunning.push_back(removeChan);
+
+ QPushButton* removeAllChan = new QPushButton(tr("Remove All"), confWidget);
+ connect(removeAllChan, &QPushButton::clicked, this, &GlScopeWindow::removeAllTraces);
+ layoutBtns->addWidget(removeAllChan);
+ m_disableWhenRunning.push_back(removeAllChan);
+
+ QFrame* line = new QFrame(this);
+ line->setFrameShape(QFrame::VLine);
+ layoutBtns->addWidget(line);
+
+ QPushButton* btnSave = new QPushButton(tr("Save"), confWidget);
+ connect(btnSave, &QPushButton::clicked, this, &GlScopeWindow::saveTraces);
+ layoutBtns->addWidget(btnSave);
+ m_disableWhenRunning.push_back(btnSave);
+
+ QPushButton* btnLoad = new QPushButton(tr("Load"), confWidget);
+ connect(btnLoad, &QPushButton::clicked, this, &GlScopeWindow::loadTraces);
+ layoutBtns->addWidget(btnLoad);
+ m_disableWhenRunning.push_back(btnLoad);
+
+ layoutBtns->addStretch();
+
+ layoutGrp->addLayout(layoutBtns);
+ }
+
+ layoutConf->addWidget(grpChans);
+ }
+ }
+
+ m_splitter->addWidget(confWidget);
+
+ // Can't collapse the scope
+ m_splitter->setCollapsible(0, false);
+
+ // Refresh
+ updateConfiguration();
+ onRunningChanged(m_scope->model()->isRunning());
+
+ // Now connect signals for other receiver views
+ connect(this, SIGNAL(startOtherViews()), parent, SIGNAL(startReceiverViews()));
+ connect(this, SIGNAL(stopOtherViews()), parent, SIGNAL(stopReceiverViews()));
+}
+
+GlScopeWindow::~GlScopeWindow()
+{
+ // Must delete the scope first
+ delete m_scope;
+}
+
+void GlScopeWindow::onRunningChanged(bool running)
+{
+ if (m_chkSyncViews->isChecked() && !running)
+ emit stopOtherViews();
+
+ m_btnStart->setChecked(running);
+ m_btnStop->setChecked(!running);
+
+ // Enable/disable the start and stop buttons
+ m_btnStart->setEnabled(!running && m_scope->model()->rowCount() > 0);
+ m_btnStop->setEnabled(running);
+
+ for (QWidget* w : m_disableWhenRunning)
+ w->setEnabled(!running);
+
+ // Disable invalid trigger setup
+ for (QWidget* w : m_triggerSetup)
+ w->setEnabled(!running && m_triggerType->currentIndex() != static_cast(ScopeModel::Trigger::FreeRun));
+
+ // Reset to start
+ if (running)
+ m_scope->setScopeView();
+
+ // Force to follow now when running
+ m_scope->setFollowNow(running);
+
+ updateTimeScrollBars();
+}
+
+void GlScopeWindow::onTimeSliderMoved(int value)
+{
+ qreal startTime = value;
+ startTime = startTime / 1000;
+ QRectF scopeView = m_scope->scopeView();
+ scopeView.moveLeft(startTime);
+ m_scope->setScopeView(scopeView);
+}
+
+void GlScopeWindow::onTimeDivisionsChanged(int value)
+{
+ m_scope->setTimeDivisions(value);
+ updateTimeScrollBars();
+}
+
+void GlScopeWindow::setTimeFormat(int value)
+{
+ m_scope->setTimeFormat(static_cast(value));
+}
+
+void GlScopeWindow::setRecordMode(int idx)
+{
+ m_scope->model()->setStoreAllPoints(idx == 0);
+}
+
+void GlScopeWindow::setVerticalScaleMode(int idx)
+{
+ m_scope->setVerticalScaleMode(static_cast(idx));
+}
+
+void GlScopeWindow::setTriggerType(int idx)
+{
+ m_scope->model()->setTriggerType(static_cast(idx));
+ // Enable/disable trigger settings
+ for (QWidget* w : m_triggerSetup)
+ w->setEnabled(idx != static_cast(ScopeModel::Trigger::FreeRun));
+}
+
+void GlScopeWindow::addTrace(bool)
+{
+ QColor traceColor = QColor::fromHsv(m_lastTraceHue, m_lastTraceSat, 255);
+ // Shift colour, won't quickly repeat
+ m_lastTraceHue += 40;
+ if (m_lastTraceHue >= 360)
+ {
+ m_lastTraceHue -= 360;
+ m_lastTraceSat -= 30;
+ if (m_lastTraceSat < 0)
+ m_lastTraceSat += 255;
+ }
+
+ if (m_scope->model()->rowCount() == 0)
+ {
+ m_scope->model()->addTrace(traceColor, m_defaultUniverse, MIN_DMX_ADDRESS);
+ m_btnStart->setEnabled(true);
+ return;
+ }
+
+ // Add an 8bit trace defaulting to the next item, based on the current index
+ QModelIndex current = m_tableView->currentIndex();
+ if (!current.isValid())
+ current = m_tableView->model()->index(m_tableView->model()->rowCount() - 1, ScopeModel::COL_UNIVERSE);
+
+ uint16_t universe = current.model()->index(current.row(), ScopeModel::COL_UNIVERSE).data(Qt::DisplayRole).toUInt();
+
+ uint16_t address_hi;
+ uint16_t address_lo;
+ {
+ const QString addr_string = current.model()->index(current.row(), ScopeModel::COL_ADDRESS).data(Qt::DisplayRole).toString();
+ if (!ScopeTrace::extractAddress(addr_string, address_hi, address_lo))
+ {
+ qDebug() << "Bad format, logic error:" << addr_string;
+ address_hi = 1;
+ address_lo = 0;
+ }
+ }
+
+ if (current.column() == ScopeModel::COL_UNIVERSE)
+ {
+ // If Universe column is selected, add the same slot(s) in the next unused Universe
+ do
+ {
+ ++universe;
+ } while (m_scope->model()->addTrace(traceColor, universe, address_hi, address_lo) == ScopeModel::AddResult::Exists);
+ }
+ else
+ {
+ // If any other column is selected, add the next unused 8/16bit slot on the same universe
+ if (universe < MIN_SACN_UNIVERSE)
+ universe = MIN_SACN_UNIVERSE;
+ do
+ {
+ ++address_hi;
+
+ // 16 bit, default to coarse/fine as is by far the most common
+ if (address_lo != 0)
+ {
+ ++address_hi;
+ address_lo = address_hi + 1;
+ }
+
+ // Next universe
+ if (address_hi > MAX_DMX_ADDRESS)
+ {
+ ++universe;
+ address_hi = MIN_DMX_ADDRESS;
+ if (address_lo != 0)
+ address_lo = address_hi + 1;
+ }
+ } while (m_scope->model()->addTrace(traceColor, universe, address_hi, address_lo) == ScopeModel::AddResult::Exists);
+ }
+
+ // Select the item that was added
+ QAbstractProxyModel* proxy = qobject_cast(m_tableView->model());
+ if (proxy)
+ {
+ const QModelIndex srcIndex = m_scope->model()->findFirstTraceIndex(universe, address_hi, address_lo, proxy->mapToSource(current).column());
+ if (srcIndex.isValid())
+ m_tableView->setCurrentIndex(proxy->mapFromSource(srcIndex));
+ }
+}
+
+void GlScopeWindow::removeTrace(bool)
+{
+ QItemSelectionModel* selection = m_tableView->selectionModel();
+ if (!selection->hasSelection())
+ return;
+
+ // Get the items to delete
+ QModelIndexList selected = selection->selectedIndexes();
+ m_scope->model()->removeTraces(selected);
+}
+
+void GlScopeWindow::removeAllTraces(bool)
+{
+ m_scope->model()->removeAllTraces();
+ m_btnStart->setEnabled(false);
+}
+
+void GlScopeWindow::saveTraces(bool)
+{
+ const QString filename = QFileDialog::getSaveFileName(this, tr("Save Traces"), QString(), QStringLiteral("CSV (*.csv)"));
+ if (filename.isEmpty())
+ return;
+
+ QFile csvExport(filename);
+ if (!csvExport.open(QIODevice::WriteOnly))
+ return;
+
+ m_scope->model()->saveTraces(csvExport);
+}
+
+void GlScopeWindow::loadTraces(bool)
+{
+ const QString filename = QFileDialog::getOpenFileName(this, tr("Load Traces"), QString(), QStringLiteral("CSV (*.csv)"));
+ if (filename.isEmpty())
+ return;
+
+ QFile csvExport(filename);
+ if (!csvExport.open(QIODevice::ReadOnly))
+ return;
+
+ m_scope->model()->loadTraces(csvExport);
+
+ // Update
+ updateTimeScrollBars();
+ updateConfiguration();
+ m_btnStart->setEnabled(m_scope->model()->rowCount() > 0);
+}
+
+void GlScopeWindow::onTriggered()
+{
+ if (m_chkSyncViews->isChecked())
+ emit startOtherViews();
+}
+
+void GlScopeWindow::updateTimeScrollBars()
+{
+ // Disable scrolling when running
+ if (m_scope->model()->isRunning())
+ {
+ m_scrollTime->setMinimum(0);
+ m_scrollTime->setMaximum(0);
+ return;
+ }
+
+ const QRectF extents = m_scope->model()->traceExtents();
+ const qreal viewWidth = m_scope->scopeView().width();
+ m_scrollTime->setEnabled(extents.width() > viewWidth);
+
+ // Use milliseconds
+ m_scrollTime->setMinimum(extents.left() * 1000);
+ const qreal maxVal = (extents.right() - viewWidth) * 1000;
+ m_scrollTime->setMaximum(maxVal > 0 ? maxVal : 0);
+ m_scrollTime->setPageStep(viewWidth * 1000);
+
+ // And jump to an appropriate end
+ if (extents.right() > viewWidth)
+ m_scrollTime->setValue(m_scrollTime->maximum());
+ else
+ m_scrollTime->setValue(m_scrollTime->minimum());
+
+ onTimeSliderMoved(m_scrollTime->value());
+ m_scrollTime->setEnabled(true);
+}
+
+void GlScopeWindow::updateConfiguration()
+{
+ // Read values back from the scope model
+ m_recordMode->setCurrentIndex(m_scope->model()->storeAllPoints() ? 0 : 1);
+ m_spinRunTime->setValue(m_scope->model()->runTime());
+ m_triggerType->setCurrentIndex(static_cast(m_scope->model()->triggerType()));
+ m_spinTriggerLevel->setValue(m_scope->model()->triggerLevel());
+}
+
+// QColorDialog doesn't autocentre when used as a delegate
+void ColorDialog::showEvent(QShowEvent* ev)
+{
+ QRect parentRect(parentWidget()->mapToGlobal(QPoint(0, 0)), parentWidget()->size());
+ move(QStyle::alignedRect(Qt::LeftToRight, Qt::AlignCenter, size(), parentRect).topLeft());
+
+ QColorDialog::showEvent(ev);
+}
+
+
+ColorPickerDelegate::ColorPickerDelegate(QWidget* parent)
+ : QStyledItemDelegate(parent)
+{
+ m_dialog = new ColorDialog(parent);
+}
+
+QWidget* ColorPickerDelegate::createEditor(QWidget* parent, const QStyleOptionViewItem& /*option*/, const QModelIndex& /*idx*/) const
+{
+ return m_dialog;
+}
+
+void ColorPickerDelegate::destroyEditor(QWidget* editor, const QModelIndex& /*idx*/) const
+{
+ // Don't delete it, reuse it instead
+ if (editor == m_dialog)
+ return;
+}
+
+void ColorPickerDelegate::setEditorData(QWidget* editor, const QModelIndex& idx) const
+{
+ ColorDialog* dialog = qobject_cast(editor);
+ if (dialog)
+ {
+ QColor color = idx.data(Qt::BackgroundRole).value();
+ dialog->setCurrentColor(color);
+ }
+}
+
+void ColorPickerDelegate::setModelData(QWidget* editor, QAbstractItemModel* model, const QModelIndex& idx) const
+{
+ ColorDialog* dialog = qobject_cast(editor);
+ if (dialog)
+ {
+ QColor color = dialog->selectedColor();
+ if (color.isValid())
+ model->setData(idx, color, Qt::EditRole);
+ }
+}
diff --git a/src/ui/glscopewindow.h b/src/ui/glscopewindow.h
new file mode 100644
index 00000000..b75a7c71
--- /dev/null
+++ b/src/ui/glscopewindow.h
@@ -0,0 +1,118 @@
+// Copyright 2023 Electronic Theatre Controls, Inc. or its affiliates
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#pragma once
+
+#include "consts.h"
+
+#include
+#include
+#include
+
+class GlScopeWidget;
+class SteppedSpinBox;
+
+class QCheckBox;
+class QComboBox;
+class QPushButton;
+class QScrollBar;
+class QSpinBox;
+class QSplitter;
+class QTableView;
+
+class GlScopeWindow : public QWidget
+{
+ Q_OBJECT
+public:
+ explicit GlScopeWindow(int universe, QWidget* parent = nullptr);
+ ~GlScopeWindow();
+
+private:
+ Q_SLOT void onRunningChanged(bool running);
+ Q_SLOT void onTimeSliderMoved(int value);
+ Q_SLOT void onTimeDivisionsChanged(int value);
+ Q_SLOT void setTimeFormat(int value);
+
+ Q_SLOT void setRecordMode(int idx);
+ Q_SLOT void setVerticalScaleMode(int idx);
+ Q_SLOT void setTriggerType(int idx);
+
+ Q_SLOT void addTrace(bool);
+ Q_SLOT void removeTrace(bool);
+ Q_SLOT void removeAllTraces(bool);
+
+ Q_SLOT void saveTraces(bool);
+ Q_SLOT void loadTraces(bool);
+
+ Q_SLOT void onTriggered();
+
+ // Signals to start/stop other open rx views
+ Q_SIGNAL void startOtherViews();
+ Q_SIGNAL void stopOtherViews();
+
+private:
+ QSplitter* m_splitter = nullptr;
+ GlScopeWidget* m_scope = nullptr;
+ QScrollBar* m_scrollTime = nullptr;
+ QComboBox* m_recordMode = nullptr;
+ QSpinBox* m_spinRunTime = nullptr;
+ SteppedSpinBox* m_spinTimeScale = nullptr;
+ QComboBox* m_timeFormat = nullptr;
+ QComboBox* m_triggerType = nullptr;
+ QSpinBox* m_spinTriggerLevel = nullptr;
+ QPushButton* m_btnStart = nullptr;
+ QPushButton* m_btnStop = nullptr;
+ QCheckBox* m_chkSyncViews = nullptr;
+ QTableView* m_tableView = nullptr;
+
+ // Widgets to disable when running and enable when stopped
+ std::vector m_disableWhenRunning;
+ // Widgets to disable when trigger is Free Run
+ std::vector m_triggerSetup;
+
+ int m_defaultUniverse = MIN_SACN_UNIVERSE;
+
+ int m_lastTraceHue = 0;
+ int m_lastTraceSat = 255;
+
+ void updateTimeScrollBars();
+ void updateConfiguration();
+};
+
+class ColorDialog : public QColorDialog
+{
+ Q_OBJECT
+
+public:
+ ColorDialog(QWidget* parent = nullptr) : QColorDialog(parent) {}
+
+protected:
+ void showEvent(QShowEvent* ev) override;
+};
+
+class ColorPickerDelegate : public QStyledItemDelegate
+{
+ Q_OBJECT
+
+public:
+ ColorPickerDelegate(QWidget* parent = nullptr);
+
+ QWidget* createEditor(QWidget* parent, const QStyleOptionViewItem& option, const QModelIndex& idx) const override;
+ void destroyEditor(QWidget* editor, const QModelIndex& idx) const override;
+ void setEditorData(QWidget* editor, const QModelIndex& idx) const override;
+ void setModelData(QWidget* editor, QAbstractItemModel* model, const QModelIndex& idx) const override;
+
+private:
+ QDialog* m_dialog = nullptr;
+};
diff --git a/src/ui/mdimainwindow.cpp b/src/ui/mdimainwindow.cpp
index d50e2e8f..8354a2b4 100644
--- a/src/ui/mdimainwindow.cpp
+++ b/src/ui/mdimainwindow.cpp
@@ -15,7 +15,7 @@
#include "mdimainwindow.h"
#include "ui_mdimainwindow.h"
-#include "scopewindow.h"
+#include "glscopewindow.h"
#include "universeview.h"
#include "transmitwindow.h"
#include "preferences.h"
@@ -98,7 +98,7 @@ void MDIMainWindow::closeEvent(QCloseEvent* ev)
void MDIMainWindow::on_actionScopeView_triggered(bool checked)
{
Q_UNUSED(checked);
- ScopeWindow* scopeWindow = new ScopeWindow(ui->sbUniverseList->value(), this);
+ GlScopeWindow* scopeWindow = new GlScopeWindow(ui->sbUniverseList->value(), this);
showWidgetAsSubWindow(scopeWindow);
}
@@ -212,6 +212,16 @@ void MDIMainWindow::on_actionMultiUniverse_triggered()
QWidget* MDIMainWindow::showWidgetAsSubWindow(QWidget* w)
{
+ // Connect cross-window triggering signals
+ // Check if extant
+ const QMetaObject* meta = w->metaObject();
+ if (meta->indexOfSlot("startRx()") != -1)
+ {
+ // Classes should support both or neither
+ connect(this, SIGNAL(startReceiverViews()), w, SLOT(startRx()));
+ connect(this, SIGNAL(stopReceiverViews()), w, SLOT(stopRx()));
+ }
+
switch (Preferences::Instance().GetWindowMode())
{
default:
@@ -262,7 +272,7 @@ void MDIMainWindow::restoreSubWindows()
{
if (window.name == "Scope")
{
- ScopeWindow* scopeWindow = new ScopeWindow(MIN_SACN_UNIVERSE, this);
+ GlScopeWindow* scopeWindow = new GlScopeWindow(MIN_SACN_UNIVERSE, this);
showWidgetAsSubWindow(scopeWindow)->restoreGeometry(window.geometry);
}
@@ -344,7 +354,7 @@ QWidget* MDIMainWindow::addFloatWidget(QWidget* w)
void MDIMainWindow::StoreWidgetGeometry(const QWidget* window, const QWidget* widget, QList& result)
{
- if (qobject_cast(widget) != Q_NULLPTR)
+ if (qobject_cast(widget) != Q_NULLPTR)
{
SubWindowInfo i;
i.name = "Scope";
diff --git a/src/ui/mdimainwindow.h b/src/ui/mdimainwindow.h
index 90a01b93..0e428c82 100644
--- a/src/ui/mdimainwindow.h
+++ b/src/ui/mdimainwindow.h
@@ -40,6 +40,11 @@ class MDIMainWindow : public QMainWindow
explicit MDIMainWindow(QWidget* parent = 0);
~MDIMainWindow();
+ /// Start all receiver views
+ Q_SIGNAL void startReceiverViews();
+ /// Stop all receiver views
+ Q_SIGNAL void stopReceiverViews();
+
protected:
void showEvent(QShowEvent* ev) override;
void closeEvent(QCloseEvent* ev) override;
diff --git a/src/ui/multiview.cpp b/src/ui/multiview.cpp
index 8953e711..bf1b7a99 100644
--- a/src/ui/multiview.cpp
+++ b/src/ui/multiview.cpp
@@ -3,11 +3,13 @@
#include "ui_multiview.h"
#include "consts.h"
+#include "preferences.h"
#include "models/sacnsourcetablemodel.h"
#include "models/csvmodelexport.h"
#include
#include
+#include
MultiView::MultiView(QWidget* parent)
: QWidget(parent)
@@ -15,14 +17,31 @@ MultiView::MultiView(QWidget* parent)
, m_sourceTableModel(new SACNSourceTableModel(this))
{
ui->setupUi(this);
- ui->sourceTableView->setModel(m_sourceTableModel);
- ui->sourceTableView->horizontalHeader()->setSectionResizeMode(QHeaderView::ResizeToContents);
+ QSortFilterProxyModel* sortProxy = new QSortFilterProxyModel(this);
+ sortProxy->setSourceModel(m_sourceTableModel);
+ ui->sourceTableView->setModel(sortProxy);
+ ui->sourceTableView->horizontalHeader()->resizeSections(QHeaderView::ResizeToContents);
ui->spinUniverseMin->setMinimum(MIN_SACN_UNIVERSE);
ui->spinUniverseMin->setMaximum(MAX_SACN_UNIVERSE);
ui->spinUniverseMax->setMinimum(MIN_SACN_UNIVERSE);
ui->spinUniverseMax->setMaximum(MAX_SACN_UNIVERSE);
+
+ ui->spinShort->setMaximum(800);
+ ui->spinShort->setValue(m_sourceTableModel->shortInterval());
+ connect(ui->spinShort, QOverload::of(&QSpinBox::valueChanged), m_sourceTableModel, &SACNSourceTableModel::setShortInterval);
+
+ ui->spinLong->setMaximum(ui->spinShort->maximum());
+ ui->spinLong->setValue(m_sourceTableModel->longInterval());
+ connect(ui->spinLong, QOverload::of(&QSpinBox::valueChanged), m_sourceTableModel, &SACNSourceTableModel::setLongInterval);
+
+ ui->spinStatic->setMaximum(ui->spinShort->maximum());
+ ui->spinStatic->setValue(m_sourceTableModel->staticInterval());
+ connect(ui->spinStatic, QOverload::of(&QSpinBox::valueChanged), m_sourceTableModel, &SACNSourceTableModel::setStaticInterval);
+
+ // Maybe don't show the Secure column
+ ui->sourceTableView->setColumnHidden(SACNSourceTableModel::COL_PATHWAY_SECURE, !Preferences::Instance().GetPathwaySecureRx());
}
MultiView::~MultiView()
@@ -34,13 +53,13 @@ void MultiView::on_btnStartStop_clicked(bool checked)
{
if (checked)
{
+ ui->btnStartStop->setText(tr("Stop"));
ui->spinUniverseMin->setEnabled(false);
ui->spinUniverseMax->setEnabled(false);
// Clear and restart listening for the large number of universes
- m_sourceTableModel->resetCounters();
m_sourceTableModel->clear();
-
+
// Hold onto the old listeners so any overlaps will not be destructed
std::map old_listeners;
old_listeners.swap(m_listeners);
@@ -58,10 +77,10 @@ void MultiView::on_btnStartStop_clicked(bool checked)
}
else
{
+ ui->btnStartStop->setText(tr("Start"));
ui->spinUniverseMin->setEnabled(true);
ui->spinUniverseMax->setEnabled(true);
-
- // TODO: Actually pause
+ m_sourceTableModel->pause();
}
}
diff --git a/src/ui/multiview.h b/src/ui/multiview.h
index a10ad531..61336a37 100644
--- a/src/ui/multiview.h
+++ b/src/ui/multiview.h
@@ -34,6 +34,11 @@ class MultiView : public QWidget
explicit MultiView(QWidget* parent = 0);
~MultiView();
+ // Trigger API
+ Q_SLOT void startRx() { on_btnStartStop_clicked(true); }
+ Q_SLOT void stopRx() { on_btnStartStop_clicked(false); }
+
+
protected slots:
void on_btnStartStop_clicked(bool checked);
void on_btnResetCounters_clicked();
diff --git a/src/ui/scopewindow.cpp b/src/ui/scopewindow.cpp
deleted file mode 100644
index 69414ee3..00000000
--- a/src/ui/scopewindow.cpp
+++ /dev/null
@@ -1,300 +0,0 @@
-// Copyright 2016 Tom Steer
-// http://www.tomsteer.net
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-#include "scopewindow.h"
-#include "ui_scopewindow.h"
-#include "consts.h"
-#include "preferences.h"
-#include "sacnlistener.h"
-#include
-#include
-#include
-
-static const QList COLOURS({
- Qt::white,
- Qt::red ,
- Qt::darkRed,
- Qt::green,
- Qt::darkGreen,
- Qt::blue,
- Qt::darkBlue,
- Qt::cyan,
- Qt::darkCyan,
- Qt::magenta,
- Qt::darkMagenta,
- Qt::yellow,
- Qt::darkYellow
- });
-
-static const QList TIMEBASES({
- 2000,
- 1000,
- 500,
- 200,
- 100,
- 50,
- 20,
- 10,
- 5,
-});
-
-ScopeWindow::ScopeWindow(int universe, QWidget *parent) :
- QWidget(parent),
- ui(new Ui::ScopeWindow),
- m_defaultUniverse(universe)
-{
- ui->setupUi(this);
- ui->dlTimebase->setMinimum(0);
- ui->dlTimebase->setMaximum(TIMEBASES.count() - 1);
- connect(ui->dlTimebase, &QAbstractSlider::valueChanged, this, &ScopeWindow::timebaseChanged);
- ui->tableWidget->setRowCount(0);
- ui->btnStart->setEnabled(false);
- ui->btnStop->setEnabled(false);
-
- m_radioGroup = new QButtonGroup(this);
- connect(m_radioGroup, &QButtonGroup::idPressed, this, &ScopeWindow::on_buttonGroup_buttonPressed);
- connect(ui->sbTriggerDelay, QOverload::of(&QSpinBox::valueChanged), ui->widget, &ScopeWidget::setTriggerDelay);
- connect(ui->widget, &ScopeWidget::stopped, this, &ScopeWindow::on_scopeWidget_stopped);
-
- // Set initial value
- ui->dlTimebase->setValue(2);
-
- // Setup trigger spinbox
- ui->sbTriggerLevel->setMinimum(0);
- ui->sbTriggerLevel->setMaximum(Preferences::Instance().GetMaxLevel());
- ui->sbTriggerLevel->setValue(Preferences::Instance().GetMaxLevel()/2);
-}
-
-ScopeWindow::~ScopeWindow()
-{
- delete ui;
-}
-
-void ScopeWindow::timebaseChanged(int value)
-{
- int timebase = TIMEBASES[value];
- if(timebase<1000)
- ui->lbTimebase->setText(tr("%1 ms/div").arg(timebase));
- else
- ui->lbTimebase->setText(tr("%1 s/div").arg(timebase/1000));
- ui->widget->setTimebase(timebase);
-}
-
-void ScopeWindow::on_btnStart_pressed()
-{
- ui->widget->start();
- ui->btnStart->setEnabled(false);
- ui->btnStop->setEnabled(true);
-}
-
-void ScopeWindow::on_btnStop_pressed()
-{
- ui->widget->stop();
- ui->btnStart->setEnabled(true);
- ui->btnStop->setEnabled(false);
-}
-
-void ScopeWindow::on_btnAddChannel_pressed()
-{
- int rowNumber = m_channels.count();
- ScopeChannel *channel = new ScopeChannel(m_defaultUniverse, rowNumber % 512);
-
- QColor col((Qt::GlobalColor)COLOURS[rowNumber % COLOURS.count()]);
- channel->setColor(col);
- ui->widget->addChannel(channel);
- m_channels << channel;
-
- ui->tableWidget->setRowCount(m_channels.count());
- QTableWidgetItem *item = new QTableWidgetItem(QString::number(channel->universe()));
- ui->tableWidget->setItem(m_channels.count()-1, COL_UNIVERSE, item);
-
- item = new QTableWidgetItem(QString::number(channel->address()+1));
- ui->tableWidget->setItem(m_channels.count()-1, COL_ADDRESS, item);
-
- item = new QTableWidgetItem(tr("Yes"));
- item->setCheckState(Qt::Checked);
- ui->tableWidget->setItem(m_channels.count()-1, COL_ENABLED, item);
-
- item = new QTableWidgetItem();
- item->setBackground(col);
- item->setFlags(Qt::ItemIsEnabled);
- ui->tableWidget->setItem(m_channels.count()-1, COL_COLOUR, item);
-
- QWidget *containerWidget = new QWidget(this);
- QRadioButton *radio = new QRadioButton(containerWidget);
- m_radioGroup->addButton(radio, m_channels.count()-1);
- QHBoxLayout *layout = new QHBoxLayout;
- layout->setContentsMargins(0,0,0,0);
- layout->addStretch();
- layout->addWidget(radio);
- layout->addStretch();
- containerWidget->setLayout(layout);
-
- ui->tableWidget->setCellWidget(m_channels.count()-1, COL_TRIGGER, containerWidget);
-
-
- item = new QTableWidgetItem(tr("No"));
- item->setCheckState(Qt::Unchecked);
- ui->tableWidget->setItem(m_channels.count()-1, COL_16BIT, item);
-
- if(!ui->widget->running())
- ui->btnStart->setEnabled(true);
-}
-
-void ScopeWindow::on_btnRemoveChannel_pressed()
-{
- int index = ui->tableWidget->currentRow();
- if(index<0) return;
-
- ScopeChannel *channel = m_channels[index];
- ui->widget->removeChannel(channel);
- ui->tableWidget->removeRow(index);
- m_channels.removeAt(index);
-
- if(ui->tableWidget->rowCount() == 0)
- {
- ui->widget->stop();
- ui->btnStart->setEnabled(false);
- ui->btnStop->setEnabled(false);
- }
-}
-
-
-void ScopeWindow::on_tableWidget_cellDoubleClicked(int row, int col)
-{
- if(col==COL_COLOUR)
- {
- ScopeChannel *channel = m_channels[row];
- QColorDialog dlg;
- QColor newColor = dlg.getColor(channel->color(), this);
- channel->setColor(newColor);
- QTableWidgetItem *w = ui->tableWidget->item(row, col);
- w->setBackground(newColor);
- }
-}
-
-void ScopeWindow::on_tableWidget_itemChanged(QTableWidgetItem * item)
-{
- ScopeChannel *ch = m_channels[item->row()];
- int address = ch->address();
- int universe = ch->universe();
- bool ok;
-
- switch(item->column())
- {
- case COL_ADDRESS:
- address = item->text().toInt(&ok);
- if(address>=1 && address<=512 && ok)
- {
- sACNManager::tListener listener;
- if(!m_universes.contains(ch->universe()))
- {
- m_universes[ch->universe()] = sACNManager::Instance().getListener(ch->universe());
- }
-
- listener = m_universes[ch->universe()];
-
- listener->unMonitorAddress(ch->address(), this);
- ch->setAddress(address-1);
- listener->monitorAddress(ch->address(), this);
- ch->clear();
- }
- else
- {
- ui->tableWidget->blockSignals(true);
- item->setText(QString::number(ch->address()+1));
- ui->tableWidget->blockSignals(false);
- }
- break;
-
- case COL_UNIVERSE:
- universe = item->text().toInt(&ok);
- if(universe>=1 && universe<=MAX_SACN_UNIVERSE && ok && ch->universe()!=universe)
- {
- // Changing universe
- sACNManager::tListener listener;
- if(!m_universes.contains(universe))
- {
- m_universes[ch->universe()] = sACNManager::Instance().getListener(universe);
- }
-
- listener = m_universes[ch->universe()];
-
- ch->setUniverse(universe);
- disconnect(listener.data(), 0, this->ui->widget, 0);
- connect(listener.data(), &sACNListener::dataReady, this->ui->widget, &ScopeWidget::dataReady);
- listener->monitorAddress(ch->address(), this);
- ch->clear();
- }
- else
- {
- ui->tableWidget->blockSignals(true);
- item->setText(QString::number(ch->universe()));
- ui->tableWidget->blockSignals(false);
- }
- break;
- case COL_ENABLED:
- if(item->checkState() == Qt::Checked)
- {
- item->setText(tr("Yes"));
- ch->setEnabled(true);
- }
- else
- {
- item->setText(tr("No"));
- ch->setEnabled(false);
- }
- break;
-
- case COL_16BIT:
- if(item->checkState() == Qt::Checked)
- {
- item->setText(tr("Yes"));
- ch->setSixteenBit(true);
- }
- else
- {
- item->setText(tr("No"));
- ch->setSixteenBit(false);
- }
- break;
- }
-}
-
-void ScopeWindow::on_buttonGroup_buttonPressed(int id)
-{
- int row = id;
-
- int universe = ui->tableWidget->item(row, COL_UNIVERSE)->text().toInt();
- int address = ui->tableWidget->item(row, COL_ADDRESS)->text().toInt()-1;
-
- ui->widget->setTriggerAddress(universe, address);
-}
-
-void ScopeWindow::on_cbTriggerMode_currentIndexChanged(int index)
-{
- ui->widget->setTriggerMode((ScopeWidget::TriggerMode)index);
-}
-
-void ScopeWindow::on_sbTriggerLevel_valueChanged(int value)
-{
- ui->widget->setTriggerThreshold(value);
-}
-
-void ScopeWindow::on_scopeWidget_stopped()
-{
- ui->btnStart->setEnabled(true);
- ui->btnStop->setEnabled(false);
-}
diff --git a/src/ui/scopewindow.h b/src/ui/scopewindow.h
deleted file mode 100644
index c9d9e2fd..00000000
--- a/src/ui/scopewindow.h
+++ /dev/null
@@ -1,68 +0,0 @@
-// Copyright 2016 Tom Steer
-// http://www.tomsteer.net
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-#ifndef SCOPEWINDOW_H
-#define SCOPEWINDOW_H
-
-class ScopeChannel;
-class QTableWidgetItem;
-
-#include
-#include
-#include
-#include "streamingacn.h"
-#include "consts.h"
-
-namespace Ui {
-class ScopeWindow;
-}
-
-class ScopeWindow : public QWidget
-{
- Q_OBJECT
-
-public:
- explicit ScopeWindow(int universe = MIN_SACN_UNIVERSE, QWidget *parent = 0);
- ~ScopeWindow();
-private slots:
- void timebaseChanged(int value);
- void on_btnStart_pressed();
- void on_btnStop_pressed();
- void on_btnAddChannel_pressed();
- void on_btnRemoveChannel_pressed();
- void on_tableWidget_cellDoubleClicked(int row, int col);
- void on_tableWidget_itemChanged(QTableWidgetItem * item);
- void on_buttonGroup_buttonPressed(int id);
- void on_cbTriggerMode_currentIndexChanged(int index);
- void on_sbTriggerLevel_valueChanged(int value);
- void on_scopeWidget_stopped();
-private:
- Ui::ScopeWindow *ui;
- QList m_channels;
- QButtonGroup *m_radioGroup;
- QHash m_universes;
-
- enum {
- COL_UNIVERSE,
- COL_ADDRESS,
- COL_ENABLED,
- COL_COLOUR,
- COL_TRIGGER,
- COL_16BIT
- };
- int m_defaultUniverse;
-};
-
-#endif // SCOPEWINDOW_H
diff --git a/src/ui/universeview.cpp b/src/ui/universeview.cpp
index 3b80b346..fcd382fa 100644
--- a/src/ui/universeview.cpp
+++ b/src/ui/universeview.cpp
@@ -57,8 +57,16 @@ UniverseView::UniverseView(int universe, QWidget *parent) :
ui->sbUniverse->setValue(universe);
m_sourceTableModel = new SACNSourceTableModel(this);
- ui->tableView->setModel(m_sourceTableModel);
- ui->tableView->horizontalHeader()->setSectionResizeMode(QHeaderView::ResizeToContents);
+ QSortFilterProxyModel* sortProxy = new QSortFilterProxyModel(this);
+ sortProxy->setSourceModel(m_sourceTableModel);
+ ui->tableView->setModel(sortProxy);
+ ui->tableView->horizontalHeader()->resizeSections(QHeaderView::ResizeToContents);
+ // Don't need to display the Universe column
+ ui->tableView->setColumnHidden(SACNSourceTableModel::COL_UNIVERSE, true);
+ // Don't show the time summary column
+ ui->tableView->setColumnHidden(SACNSourceTableModel::COL_TIME_SUMMARY, true);
+ // Maybe don't show the Secure column
+ ui->tableView->setColumnHidden(SACNSourceTableModel::COL_PATHWAY_SECURE, !Preferences::Instance().GetPathwaySecureRx());
}
UniverseView::~UniverseView()
@@ -260,12 +268,15 @@ void UniverseView::openBigDisplay(quint16 address)
void UniverseView::on_btnPause_clicked()
{
ui->universeDisplay->pause();
+ m_sourceTableModel->pause();
+
this->disconnect(m_listener.data());
ui->btnGo->setEnabled(true);
ui->btnPause->setEnabled(false);
ui->sbUniverse->setEnabled(true);
m_bindWarningShown = false;
+
setWindowTitle(tr("Universe View"));
}
diff --git a/src/ui/universeview.h b/src/ui/universeview.h
index a506fc2c..ce9c4fbb 100644
--- a/src/ui/universeview.h
+++ b/src/ui/universeview.h
@@ -37,6 +37,11 @@ class UniverseView : public QWidget
explicit UniverseView(int universe = MIN_SACN_UNIVERSE, QWidget *parent = 0);
~UniverseView();
void startListening(int universe);
+
+ // Trigger API
+ Q_SLOT void startRx() { on_btnGo_clicked(); }
+ Q_SLOT void stopRx() { on_btnPause_clicked(); }
+
protected slots:
void refreshTitle();
void on_btnGo_clicked();
@@ -54,6 +59,7 @@ protected slots:
protected:
virtual void resizeEvent(QResizeEvent *event);
virtual void showEvent(QShowEvent *event);
+
private:
void resizeColumns();
bool m_bindWarningShown = false;
diff --git a/src/widgets/glscopewidget.cpp b/src/widgets/glscopewidget.cpp
new file mode 100644
index 00000000..c604d1ca
--- /dev/null
+++ b/src/widgets/glscopewidget.cpp
@@ -0,0 +1,1875 @@
+// Copyright 2023 Electronic Theatre Controls, Inc. or its affiliates
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#include "glscopewidget.h"
+
+#include
+#include
+#include
+#include
+#include
+
+static constexpr qreal AXIS_LABEL_WIDTH = 45.0;
+static constexpr qreal AXIS_LABEL_HEIGHT = 20.0;
+static constexpr qreal TOP_GAP = 10.0;
+static constexpr qreal RIGHT_GAP = 15.0;
+static constexpr qreal AXIS_TO_WINDOW_GAP = 5.0;
+static constexpr qreal AXIS_TICK_SIZE = 10.0;
+
+static const QString CaptureOptionsTitle = QStringLiteral("Capture Options");
+static const QString RowTitleColor = QStringLiteral("Color");
+static const QString ColumnTitleWallclockTime = QStringLiteral("Wallclock");
+static const QString ColumnTitleTimestamp = QStringLiteral("Time (s)");
+
+static const QString ShortTimeFormatString = QStringLiteral("hh:mm:ss");
+static const QString TimeFormatString = QStringLiteral("hh:mm:ss.zzz");
+static const QString DateTimeFormatString = QStringLiteral("yyyy-MM-dd ") + TimeFormatString;
+
+static constexpr qreal kMaxDmx16 = 65535;
+static constexpr qreal kMaxDmx8 = 255;
+
+template
+T roundCeilMultiple(T value, T multiple)
+{
+ if (multiple == 0) return value;
+ return static_cast(std::ceil(static_cast(value) / static_cast(multiple)) * static_cast(multiple));
+}
+
+bool ScopeTrace::extractUniverseAddress(QStringView address_string, uint16_t& universe, uint16_t& address_hi, uint16_t& address_lo)
+{
+ if (address_string.front() != QLatin1Char('U'))
+ return false;
+
+ // Extract the universe
+ const qsizetype univ_str_end = address_string.indexOf(QLatin1Char('.'));
+ bool ok = false;
+ if (univ_str_end > 0)
+ {
+ const QStringView univ_str = address_string.mid(1, univ_str_end - 1);
+ universe = univ_str.toUInt(&ok);
+ if (!ok || universe == 0 || universe > MAX_SACN_UNIVERSE)
+ return false;
+
+ return extractAddress(address_string.mid(univ_str_end + 1), address_hi, address_lo);
+ }
+
+ return false;
+}
+
+bool ScopeTrace::extractAddress(QStringView address_string, uint16_t& address_hi, uint16_t& address_lo)
+{
+ // Extract address_hi
+ const qsizetype addr_str_end = address_string.indexOf(QLatin1Char('/'));
+ bool ok = false;
+ if (addr_str_end > 0)
+ {
+ const QStringView slot_hi_str = address_string.left(addr_str_end);
+ address_hi = slot_hi_str.toUInt(&ok);
+ if (!ok)
+ return false;
+
+ // Extract slot_lo
+ const QStringView slot_lo_str = address_string.mid(addr_str_end + 1);
+ address_lo = slot_lo_str.toUInt(&ok);
+ }
+ else
+ {
+ const QStringView slot_hi_str = address_string.left(addr_str_end);
+ address_hi = slot_hi_str.toUInt(&ok);
+ address_lo = 0;
+ if (!ok)
+ return false;
+ }
+
+ return ok;
+}
+
+QString ScopeTrace::universeAddressString(uint16_t universe, uint16_t address_hi, uint16_t address_lo)
+{
+ return QStringLiteral("U") + QString::number(universe) + QLatin1Char('.') + addressString(address_hi, address_lo);
+}
+
+QString ScopeTrace::addressString(uint16_t address_hi, uint16_t address_lo)
+{
+ if (address_lo > 0 && address_lo <= MAX_DMX_ADDRESS)
+ return QString::number(address_hi) + QLatin1Char('/') + QString::number(address_lo);
+ return QString::number(address_hi);
+}
+
+QString ScopeTrace::universeAddressString() const
+{
+ return universeAddressString(universe(), addressHi(), addressLo());
+}
+
+QString ScopeTrace::addressString() const
+{
+ return addressString(addressHi(), addressLo());
+}
+
+bool ScopeTrace::setUniverse(uint16_t new_universe, bool clear_values)
+{
+ // Invalid or unchanged
+ if (new_universe == m_universe || new_universe < 0 || new_universe > MAX_SACN_UNIVERSE)
+ return false;
+
+ m_universe = new_universe;
+
+ if (clear_values)
+ clear();
+
+ return true;
+}
+
+bool ScopeTrace::setAddress(uint16_t address_hi, uint16_t address_lo, bool clear_values)
+{
+ // Validate
+ if (address_hi < 1 || address_hi > MAX_DMX_ADDRESS || address_lo > MAX_DMX_ADDRESS)
+ return false;
+
+ if (addressHi() == address_hi && addressLo() == address_lo)
+ return false; // No change
+
+ m_slot_hi = address_hi - 1;
+ m_slot_lo = address_lo == 0 ? MAX_DMX_ADDRESS : address_lo - 1;
+
+ if (clear_values)
+ clear();
+
+ return true;
+}
+
+bool ScopeTrace::setAddress(QStringView addressString, bool clear_values)
+{
+ uint16_t address_hi, address_lo;
+ if (extractAddress(addressString, address_hi, address_lo))
+ return setAddress(address_hi, address_lo, clear_values);
+
+ return false;
+}
+
+template::value, bool> = true>
+inline bool fillValue(T& value, uint16_t slot_hi, uint16_t slot_lo, const std::array& level_array)
+{
+ // Assumes slot numbers are valid
+ if (slot_lo < MAX_DMX_ADDRESS)
+ {
+ // 16 bit
+ // Do nothing if no level yet
+ if (level_array[slot_hi] < 0 || level_array[slot_lo] < 0)
+ return false;
+
+ value = static_cast(level_array[slot_hi] << 8) | static_cast(level_array[slot_lo]);
+ return true;
+ }
+ // 8 bit
+ // Do nothing if no level
+ if (level_array[slot_hi] < 0)
+ return false;
+
+ value = static_cast(level_array[slot_hi]);
+ return true;
+}
+
+void ScopeTrace::addPoint(float timestamp, const std::array& level_array, bool storeAllPoints)
+{
+ float value;
+ if (fillValue(value, m_slot_hi, m_slot_lo, level_array))
+ {
+ QMutexLocker lock(&m_mutex);
+ if (storeAllPoints)
+ {
+ m_trace.emplace_back(timestamp, value);
+ return;
+ }
+ // If level did not change in the last two, only update timestamp
+ const size_t trace_size = m_trace.size();
+ if (trace_size > 2 && m_trace[trace_size - 1].y() == value && m_trace[trace_size - 2].y() == value)
+ {
+ m_trace.back().setX(timestamp);
+ }
+ else
+ {
+ m_trace.emplace_back(timestamp, value);
+ }
+ }
+}
+
+void ScopeTrace::setFirstPoint(float timestamp, const std::array& level_array)
+{
+ float value;
+ if (fillValue(value, m_slot_hi, m_slot_lo, level_array))
+ {
+ QMutexLocker lock(&m_mutex);
+ if (m_trace.empty())
+ m_trace.emplace_back(timestamp, value);
+ else
+ m_trace[0] = { timestamp,value };
+ }
+}
+
+void ScopeTrace::applyOffset(float offset)
+{
+ QMutexLocker lock(&m_mutex);
+ for (auto& point : m_trace)
+ point.setX(point.x() - offset);
+}
+
+ScopeModel::AddResult ScopeModel::addTrace(const QColor& color, uint16_t universe, uint16_t address_hi, uint16_t address_lo)
+{
+ // Verify validity
+ if (!color.isValid())
+ return AddResult::Invalid;
+
+ if (universe < MIN_SACN_UNIVERSE)
+ return AddResult::Invalid;
+
+ if (address_hi == 0)
+ return AddResult::Invalid;
+
+ if (universe > MAX_SACN_UNIVERSE)
+ return AddResult::Invalid;
+
+ if (address_hi > MAX_DMX_ADDRESS)
+ return AddResult::Invalid;
+
+ if (address_lo > MAX_DMX_ADDRESS)
+ address_lo = 0;
+ else if (address_lo > 0)
+ {
+ // 16bit, adjust vertical scale
+ setMaxValue(kMaxDmx16);
+ }
+
+ auto univ_it = m_traceLookup.find(universe);
+ if (univ_it == m_traceLookup.end())
+ {
+ // Set the first one to be the trigger
+ if (m_traceTable.empty())
+ {
+ m_trigger.universe = universe;
+ m_trigger.address_hi = address_hi;
+ m_trigger.address_lo = address_lo;
+ }
+
+ beginInsertRows(QModelIndex(), m_traceTable.size(), m_traceTable.size());
+ ScopeTrace* trace = new ScopeTrace(color, universe, address_hi, address_lo, m_reservation);
+ m_traceTable.push_back(trace);
+ m_traceLookup.emplace(universe, std::vector(1, trace));
+ endInsertRows();
+
+ // Maybe start listening
+ if (isRunning())
+ {
+ addListener(universe);
+ }
+
+ return AddResult::Added;
+ }
+
+ // If we have already got this trace, skip
+ auto& univs_item = univ_it->second;
+ for (auto& item : univs_item)
+ {
+ if (item->addressHi() == address_hi && item->addressLo() == address_lo)
+ {
+ return AddResult::Exists;
+ }
+ }
+
+ // Add this new trace to the existing universe
+ beginInsertRows(QModelIndex(), m_traceTable.size(), m_traceTable.size());
+ ScopeTrace* trace = new ScopeTrace(color, universe, address_hi, address_lo, m_reservation);
+ m_traceTable.push_back(trace);
+ univs_item.push_back(trace);
+ endInsertRows();
+ return AddResult::Added;
+}
+
+// Removes all traces that match this
+void ScopeModel::removeTrace(uint16_t universe, uint16_t address_hi, uint16_t address_lo)
+{
+ auto univ_it = m_traceLookup.find(universe);
+ if (univ_it == m_traceLookup.end())
+ return;
+
+ bool found = false;
+ auto& univ_item = univ_it->second;
+ for (auto it = univ_item.begin(); it != univ_item.end(); /**/)
+ {
+ if ((*it)->addressHi() == address_hi && (*it)->addressLo() == address_lo)
+ {
+ found = true;
+ it = univ_item.erase(it);
+ }
+ else
+ {
+ ++it;
+ }
+ }
+
+ // Find the index in the tracetable and delete the object
+ if (found)
+ {
+ for (auto it = m_traceTable.begin(); it != m_traceTable.end(); /**/)
+ {
+ ScopeTrace* trace = (*it);
+ if (trace->universe() == universe && trace->addressHi() == address_hi && trace->addressLo() == address_lo)
+ {
+ const int row = it - m_traceTable.begin();
+ beginRemoveRows(QModelIndex(), row, row);
+ it = m_traceTable.erase(it);
+ delete trace;
+ endRemoveRows();
+ }
+ else
+ {
+ ++it;
+ }
+ }
+ }
+
+ // If last trace on a universe, stop listening
+ if (univ_item.empty())
+ {
+ for (auto it = m_listeners.begin(); it != m_listeners.end(); /**/)
+ {
+ if ((*it)->universe() == universe)
+ {
+ // Disconnect signal
+ (*it)->removeDirectCallback(this);
+ it = m_listeners.erase(it);
+ }
+ else
+ {
+ ++it;
+ }
+ }
+ }
+}
+
+void ScopeModel::removeTraces(const QModelIndexList& indexes)
+{
+ // Create list of items to remove
+ std::vector traceRows;
+ for (const QModelIndex& idx : indexes)
+ {
+ if (idx.isValid() && idx.row() < rowCount())
+ {
+ traceRows.push_back(idx.row());
+ }
+ }
+
+ // Start at the end
+ std::sort(traceRows.begin(), traceRows.end(), std::greater());
+
+ size_t prevRow = std::numeric_limits::max();
+
+ for (size_t row : traceRows)
+ {
+ if (prevRow == row)
+ continue; // This was a duplicate
+
+ beginRemoveRows(QModelIndex(), row, row);
+
+ ScopeTrace* trace = m_traceTable[row];
+ removeFromLookup(trace, trace->universe());
+
+ m_traceTable.erase(m_traceTable.begin() + row);
+ delete trace;
+ endRemoveRows();
+
+ prevRow = row;
+ }
+}
+
+ScopeTrace* ScopeModel::findTrace(uint16_t universe, uint16_t address_hi, uint16_t address_lo)
+{
+ const auto univ_it = m_traceLookup.find(universe);
+ if (univ_it != m_traceLookup.end())
+ {
+ const auto& univ_item = univ_it->second;
+ for (const auto& item : univ_item)
+ {
+ if (item->addressHi() == address_hi && item->addressLo() == address_lo)
+ return item;
+ }
+ }
+
+ // Not found
+ return nullptr;
+}
+
+QModelIndex ScopeModel::findFirstTraceIndex(uint16_t universe, uint16_t address_hi, uint16_t address_lo, int column) const
+{
+ for (size_t row = 0; row < m_traceTable.size(); ++row)
+ {
+ const ScopeTrace* trace = m_traceTable[row];
+ if (trace && trace->universe() == universe && trace->addressHi() == address_hi && trace->addressLo() == address_lo)
+ {
+ return index(row, column);
+ }
+ }
+ return QModelIndex();
+}
+
+ScopeModel::ScopeModel(QObject* parent)
+ : QAbstractTableModel(parent)
+{
+ private_removeAllTraces();
+ connect(this, &ScopeModel::queueStop, this, &ScopeModel::stop, Qt::QueuedConnection);
+ connect(this, &ScopeModel::queueTriggered, this, &ScopeModel::triggered, Qt::QueuedConnection);
+}
+
+ScopeModel::~ScopeModel()
+{
+ private_removeAllTraces();
+}
+
+QVariant ScopeModel::headerData(int section, Qt::Orientation orientation, int role) const
+{
+ if (role == Qt::DisplayRole && orientation == Qt::Horizontal)
+ {
+ switch (section)
+ {
+ case COL_UNIVERSE: return tr("Universe");
+ case COL_ADDRESS: return tr("Address");
+ case COL_COLOUR: return tr("Colour");
+ case COL_TRIGGER: return tr("Trigger");
+ }
+ }
+ return QVariant();
+}
+
+int ScopeModel::rowCount(const QModelIndex& parent) const
+{
+ if (parent.isValid())
+ return 0;
+
+ return m_traceTable.size();
+}
+
+Qt::ItemFlags ScopeModel::flags(const QModelIndex& index) const
+{
+ if (!index.isValid())
+ return Qt::ItemFlags();
+
+ switch (index.column())
+ {
+ default: return Qt::ItemFlags();
+ case COL_UNIVERSE: return Qt::ItemIsEnabled | Qt::ItemIsSelectable | Qt::ItemIsEditable | Qt::ItemIsUserCheckable;
+ case COL_ADDRESS: return Qt::ItemIsEnabled | Qt::ItemIsSelectable | Qt::ItemIsEditable;
+ case COL_COLOUR: return Qt::ItemIsEnabled | Qt::ItemIsSelectable | Qt::ItemIsEditable;
+ case COL_TRIGGER: return Qt::ItemIsEnabled | Qt::ItemIsUserCheckable;
+ }
+}
+
+QVariant ScopeModel::data(const QModelIndex& index, int role) const
+{
+ if (!index.isValid())
+ return QVariant();
+
+ if (index.column() < COL_COUNT && index.row() < rowCount())
+ {
+ const ScopeTrace* trace = m_traceTable.at(index.row());
+ if (trace)
+ {
+ switch (index.column())
+ {
+ default: break;
+ case COL_UNIVERSE:
+ if (role == Qt::DisplayRole || role == Qt::EditRole || role == DataSortRole) return trace->universe();
+ if (role == Qt::CheckStateRole) return trace->enabled() ? Qt::Checked : Qt::Unchecked;
+ break;
+ case COL_ADDRESS:
+ if (role == Qt::DisplayRole || role == Qt::EditRole) return trace->addressString();
+ if (role == DataSortRole) return uint32_t(trace->addressHi()) << 16 | trace->addressLo();
+ break;
+ case COL_COLOUR:
+ if (role == Qt::BackgroundRole || role == Qt::DisplayRole || role == Qt::EditRole) return trace->color();
+ if (role == DataSortRole) return static_cast(trace->color().rgba());
+ break;
+ case COL_TRIGGER:
+ if (role == Qt::CheckStateRole)
+ return m_trigger.isTriggerTrace(*trace) ? Qt::Checked : Qt::Unchecked;
+ if (role == DataSortRole) return m_trigger.isTriggerTrace(*trace) ? 0 : 1;
+ break;
+ }
+ }
+ }
+
+ return QVariant();
+}
+
+bool ScopeModel::setData(const QModelIndex& idx, const QVariant& value, int role)
+{
+ if (!idx.isValid())
+ return false;
+
+ if (idx.column() < COL_COUNT && idx.row() < rowCount())
+ {
+ ScopeTrace* trace = m_traceTable.at(idx.row());
+ if (trace)
+ {
+ switch (idx.column())
+ {
+ default: break;
+
+ case COL_UNIVERSE:
+ if (role == Qt::CheckStateRole)
+ {
+ trace->setEnabled(value.toBool());
+ emit dataChanged(idx, idx, { Qt::CheckStateRole });
+ emit traceVisibilityChanged();
+ return true;
+ }
+ if (role == Qt::EditRole)
+ {
+ // Maybe update the trigger
+ const bool isTrigger = m_trigger.isTriggerTrace(*trace);
+ if (moveTrace(trace, value.toUInt()))
+ {
+ if (isTrigger)
+ m_trigger.setTrigger(*trace);
+ emit dataChanged(idx, idx, { Qt::DisplayRole, Qt::EditRole, DataSortRole });
+ return true;
+ }
+ }
+ break;
+
+ case COL_ADDRESS:
+ if (role == Qt::EditRole)
+ {
+ // If 16bit changed, recheck the vertical scale
+ const bool was16bit = trace->isSixteenBit();
+ // Maybe update the trigger
+ const bool isTrigger = m_trigger.isTriggerTrace(*trace);
+ if (trace->setAddress(value.toString(), true))
+ {
+ if (was16bit != trace->isSixteenBit())
+ {
+ if (trace->isSixteenBit())
+ setMaxValue(kMaxDmx16);
+ else
+ updateMaxValue();
+ }
+ if (isTrigger)
+ m_trigger.setTrigger(*trace);
+ emit dataChanged(idx, idx, { Qt::DisplayRole, Qt::EditRole, DataSortRole });
+ return true;
+ }
+ }
+ break;
+ case COL_COLOUR:
+ if (role == Qt::EditRole)
+ {
+ QColor color = value.value();
+ if (color.isValid())
+ {
+ trace->setColor(color);
+ emit dataChanged(idx, idx, { Qt::BackgroundRole, Qt::DisplayRole, Qt::EditRole, DataSortRole });
+ return true;
+ }
+ }
+ break;
+
+ case COL_TRIGGER:
+ if (role == Qt::CheckStateRole)
+ {
+ if (trace->isValid())
+ {
+ m_trigger.setTrigger(*trace);
+ emit dataChanged(index(0, COL_TRIGGER), index(rowCount() - 1, COL_TRIGGER), { Qt::CheckStateRole, DataSortRole });
+ return true;
+ }
+ }
+ break;
+ }
+ }
+ }
+
+ return false;
+}
+
+void ScopeModel::removeAllTraces()
+{
+ beginResetModel();
+ private_removeAllTraces();
+ endResetModel();
+}
+
+void ScopeModel::private_removeAllTraces()
+{
+ stop();
+ m_traceLookup.clear();
+
+ for (ScopeTrace* trace : m_traceTable)
+ delete trace;
+
+ m_traceTable.clear();
+
+ // Set extents back to default
+ m_endTime = 0;
+ setMaxValue(kMaxDmx8);
+}
+
+void ScopeModel::clearValues()
+{
+ // Clear all the trace values
+ for (ScopeTrace* trace : m_traceTable)
+ {
+ trace->clear();
+ }
+
+ // Reset time extents
+ m_startOffset = 0;
+ m_endTime = 0;
+ m_wallclockTrigger_ms = 0;
+}
+
+QString ScopeModel::captureConfigurationString() const
+{
+ QString result = (m_storeAllPoints ? QLatin1String("All Packets,") : QLatin1String("Level Changes,"));
+
+ result.append(m_trigger.configurationString());
+ result.append(QLatin1Char(','));
+
+ if (m_runTime > 0)
+ result.append(QStringLiteral("Run For %1 sec,").arg(m_runTime));
+
+ result.chop(1);
+ return result;
+}
+
+void ScopeModel::setCaptureConfiguration(const QString& configString)
+{
+ if (configString.isEmpty())
+ return;
+
+ m_storeAllPoints = configString.contains(QLatin1String("All Packets"), Qt::CaseInsensitive);
+
+ m_trigger.setConfiguration(configString);
+
+ qsizetype runTimePos = configString.indexOf(QLatin1String("Run For"));
+ const qsizetype runTimeSecPos = configString.indexOf(QLatin1String("sec"), runTimePos);
+ if (runTimePos < 0 || runTimeSecPos < 0)
+ {
+ m_runTime = 0;
+ }
+ else
+ {
+ bool ok;
+ runTimePos += 8;
+ const qreal time = configString.mid(runTimePos, runTimeSecPos - runTimePos).toDouble(&ok);
+ if (ok)
+ m_runTime = time;
+ }
+}
+
+inline void ConfigureTextStream(QTextStream& stream)
+{
+#if QT_VERSION < QT_VERSION_CHECK(6,0,0)
+ stream.setCodec("UTF-8");
+#else
+ stream.setEncoding(QStringConverter::Utf8);
+#endif
+ stream.setLocale(QLocale::c());
+ stream.setRealNumberNotation(QTextStream::FixedNotation);
+ stream.setRealNumberPrecision(3);
+}
+
+bool ScopeModel::saveTraces(QIODevice& file) const
+{
+ if (!file.isWritable())
+ return false;
+
+ // Cannot write while running as would block the receive threads
+ if (isRunning())
+ return false;
+
+ QTextStream out(&file);
+ ConfigureTextStream(out);
+
+ // Table:
+ // Capture Options:,All Packets/Level Changes
+ //
+ // 2024-01-15, Color, red, green, ...
+ // Wallclock, Time (s),U1.1, U1.2/3, ... (Given as Universe.CoarseDMX/FineDmx (1-512)
+ // 12:00:00.000, 0.000, 255, 0, ...
+ // 12:00:00.020, 0.020, 128, 128, ...
+ // 12:00:00.040, 0.040, 127, 255, ...
+
+ // Export capture configuration line
+ out << CaptureOptionsTitle << QLatin1String(":,") << captureConfigurationString() << QStringLiteral(",hh:mm:ss.000");
+ out << "\n\n";
+
+ // First row time
+ float this_row_time = std::numeric_limits::max();
+
+ // Iterators for each trace
+ using ValueIterator = std::vector::const_iterator;
+ // Copy the values
+ struct ValueItem
+ {
+ ValueItem(const ScopeTrace* trace) : values(trace->values().value())
+ {
+ current = values.begin();
+ }
+ const std::vector values;
+ ValueIterator current;
+ };
+ std::vector traces_values;
+ traces_values.reserve(rowCount());
+
+ QDateTime datetime = QDateTime::currentDateTime();
+
+ QString color_header;
+ if (asWallclockTime(datetime, 0.0))
+ {
+ color_header = datetime.toString(DateTimeFormatString);
+ }
+ color_header = color_header + QStringLiteral(",") + RowTitleColor;
+
+ QString name_header = ColumnTitleWallclockTime + QStringLiteral(",") + ColumnTitleTimestamp;
+
+ // Header rows sorted by universe
+ for (const auto& universe : m_traceLookup)
+ {
+ for (const ScopeTrace* trace : universe.second)
+ {
+ // Get the values for this column
+ traces_values.emplace_back(trace);
+
+ // Assemble the color header string
+ color_header.append(QLatin1Char(','));
+ color_header.append(trace->color().name());
+
+ // Assemble the name header string
+ name_header.append(QLatin1String(", "));
+ name_header.append(trace->universeAddressString());
+
+ // Find the first and last row timestamps
+ if (!traces_values.back().values.empty())
+ {
+ if (traces_values.back().values.front()[0] < this_row_time)
+ this_row_time = traces_values.back().values.front()[0];
+ }
+ }
+ }
+ out << color_header << '\n' << name_header;
+
+ // Value rows
+ while (this_row_time < std::numeric_limits::max())
+ {
+ // Start new row and output timestamp
+ QString wallclock;
+ if (asWallclockTime(datetime, this_row_time))
+ {
+ wallclock = datetime.toString(TimeFormatString);
+ }
+ out << '\n' << wallclock << ',' << this_row_time;
+ float next_row_time = std::numeric_limits::max();
+
+ for (auto& value_its : traces_values)
+ {
+ // Next Column
+ out << ',';
+ if (value_its.current != value_its.values.end())
+ {
+ // This column has a value for this time
+ if (qFuzzyCompare(this_row_time, (*value_its.current)[0]))
+ {
+ // Output a value for this timestamp and step forward
+ out << static_cast((*value_its.current)[1]);
+ ++value_its.current;
+ }
+ // Determine the next row time
+ if (value_its.current != value_its.values.end() && (*value_its.current)[0] < next_row_time)
+ {
+ next_row_time = (*value_its.current)[0];
+ }
+ }
+ }
+ // On to the next row
+ this_row_time = next_row_time;
+ }
+
+ return true;
+}
+
+struct TitleRows {
+ QString config;
+ QString colors;
+ QString universes;
+};
+
+TitleRows FindUniverseTitles(QTextStream& in)
+{
+ TitleRows result;
+ // Find start of data
+ QString line;
+ while (in.readLineInto(&line))
+ {
+ if (line.startsWith(CaptureOptionsTitle))
+ {
+ result.config = line;
+ }
+ else if (line.contains(RowTitleColor))
+ {
+ // Probably the title line
+ result.colors = line;
+ result.universes = in.readLine();
+ if (result.universes.startsWith(ColumnTitleTimestamp) || result.universes.startsWith(ColumnTitleWallclockTime))
+ {
+ return result;
+ }
+ return TitleRows(); // Failed
+
+ }
+ }
+ return TitleRows();
+}
+
+bool ScopeModel::loadTraces(QIODevice& file)
+{
+ if (!file.isReadable())
+ return false;
+
+ QTextStream in(&file);
+ ConfigureTextStream(in);
+
+ const auto title_line = FindUniverseTitles(in);
+ if (title_line.universes.isEmpty())
+ return false;
+
+ // Split the title lines to find the trace colors and names
+#if QT_VERSION < QT_VERSION_CHECK(6,0,0)
+ auto colors = title_line.colors.splitRef(QLatin1Char(','), Qt::KeepEmptyParts);
+ auto titles = title_line.universes.splitRef(QLatin1Char(','), Qt::KeepEmptyParts);
+#else
+ auto colors = QStringView{ title_line.colors }.split(QLatin1Char(','), Qt::KeepEmptyParts);
+ auto titles = QStringView{ title_line.universes }.split(QLatin1Char(','), Qt::KeepEmptyParts);
+#endif
+
+ // Find data columns
+ int timeColumn = -1; // Column index for time offset
+ QDateTime wallclockTrigger;
+ // Data is always in the column after time offset
+ for (int i = 0; timeColumn == -1 && i < colors.size(); ++i)
+ {
+ if (colors.isEmpty())
+ return false;
+ if (titles.isEmpty())
+ return false;
+
+ // Grab the zero datetime from Colors
+ if (titles.front() == ColumnTitleWallclockTime)
+ wallclockTrigger = QDateTime::fromString(colors.front().toString(), DateTimeFormatString);
+ else if (titles.front() == ColumnTitleTimestamp)
+ timeColumn = i;
+
+ // Remove the row header titles
+ colors.pop_front();
+ titles.pop_front();
+ }
+
+ // Time is required
+ if (timeColumn < 0)
+ return false;
+
+ const int firstTraceColumn = timeColumn + 1; // Column index of first trace
+
+ // Remove empty colors from the end
+ while (colors.last().isEmpty())
+ colors.pop_back();
+ // Remove empty items from the end
+ while (titles.last().isEmpty())
+ titles.pop_back();
+
+ if (colors.size() != titles.size() || titles.empty())
+ return false; // No or invalid data
+
+ // Fairly likely to be valid, stop and clear my data now
+ beginResetModel();
+ private_removeAllTraces();
+
+ // Read the wallclock trigger datetime
+ if (wallclockTrigger.isValid())
+ m_wallclockTrigger_ms = wallclockTrigger.toMSecsSinceEpoch();
+ else // Or set it to a midnight
+ m_wallclockTrigger_ms = QDateTime::fromString(QStringLiteral("1975-01-01 00:00:00.000"), DateTimeFormatString).toMSecsSinceEpoch();
+
+ struct UnivSlots
+ {
+ uint16_t universe = 0;
+ uint16_t address_hi = 0;
+ uint16_t address_lo = 0;
+ };
+ std::vector trace_idents;
+
+ for (qsizetype i = 0; i < titles.size(); ++i)
+ {
+ // Grab color
+ const QColor color(colors[i]);
+
+ // Find universe and patch
+ const auto& full_title = titles[i];
+
+ UnivSlots univ_slots;
+ if (!ScopeTrace::extractUniverseAddress(full_title.trimmed(), univ_slots.universe, univ_slots.address_hi, univ_slots.address_lo))
+ {
+ trace_idents.push_back(UnivSlots()); // Skip this column
+ continue;
+ }
+
+ if (addTrace(color, univ_slots.universe, univ_slots.address_hi, univ_slots.address_lo) == AddResult::Added)
+ trace_idents.push_back(univ_slots);
+ else
+ trace_idents.push_back(UnivSlots());
+ }
+
+ // Now have all the trace idents and the container will not change
+ // Get all the pointers in order with null for bad columns
+ std::vector traces;
+ for (const auto& ident : trace_idents)
+ {
+ traces.push_back(findTrace(ident.universe, ident.address_hi, ident.address_lo));
+ }
+
+ QString data_line;
+ float prev_timestamp = 0;
+ while (in.readLineInto(&data_line))
+ {
+ if (data_line.isEmpty())
+ continue;
+
+#if QT_VERSION < QT_VERSION_CHECK(6,0,0)
+ const auto data = data_line.splitRef(QLatin1Char(','), Qt::KeepEmptyParts);
+#else
+ const auto data = QStringView{ data_line }.split(QLatin1Char(','), Qt::KeepEmptyParts);
+#endif
+ // Ignore any lines that do not have a column for all traces
+ if (data.size() < traces.size() + firstTraceColumn)
+ continue;
+
+ // Time moves ever forward. Ignore any lines in the past
+ bool ok = false;
+ const float timestamp = data[timeColumn].toFloat(&ok);
+ if (!ok || prev_timestamp > timestamp)
+ continue;
+
+ prev_timestamp = timestamp;
+
+ for (size_t i = 0; i < traces.size(); ++i)
+ {
+ ScopeTrace* trace = traces[i];
+ if (trace)
+ {
+ bool ok = false;
+ const float level = data[i + firstTraceColumn].toFloat(&ok);
+ if (ok)
+ trace->addValue({ timestamp, level });
+ }
+ }
+ }
+
+ m_endTime = prev_timestamp;
+
+ // Finally, update the config
+ setCaptureConfiguration(title_line.config);
+
+ endResetModel();
+ // Force a re-render
+ emit traceVisibilityChanged();
+ return true;
+}
+
+bool ScopeModel::listeningToUniverse(uint16_t universe) const
+{
+ return m_traceLookup.count(universe) != 0;
+}
+
+void ScopeModel::start()
+{
+ if (isRunning())
+ return;
+
+ // Clear all values and start the clock
+ // Cannot permit time to go backwards
+ clearValues();
+
+ m_running = true;
+
+ // Set up the Trigger or start immediately
+ if (m_trigger.isTrigger())
+ {
+ m_trigger.last_level = -1;
+ }
+ else
+ {
+ triggerNow(sACNManager::secsElapsed());
+ }
+
+ for (const auto& universe : m_traceLookup)
+ {
+ addListener(universe.first);
+ }
+
+ emit runningChanged(true);
+}
+
+void ScopeModel::stop()
+{
+ if (!isRunning())
+ return;
+
+ m_running = false;
+
+ // Disconnect all
+ for (auto& listener : m_listeners)
+ {
+ listener->removeDirectCallback(this);
+ }
+ // And clear/shutdown
+ m_listeners.clear();
+ emit runningChanged(false);
+}
+
+void ScopeModel::setRunTime(qreal seconds)
+{
+ if (runTime() == seconds)
+ return;
+
+ // Validate and emit new or existing value
+ if (seconds >= 0)
+ m_runTime = seconds;
+
+ emit runTimeChanged(runTime());
+}
+
+// Triggers
+void ScopeModel::setTriggerType(Trigger mode)
+{
+ m_trigger.mode = mode;
+ emit traceVisibilityChanged();
+}
+
+void ScopeModel::setTriggerLevel(uint16_t level)
+{
+ m_trigger.level = level;
+ emit traceVisibilityChanged();
+}
+
+bool ScopeModel::moveTrace(ScopeTrace* trace, uint16_t new_universe, bool clear_values)
+{
+ const uint16_t old_universe = trace->universe();
+
+ if (!trace->setUniverse(new_universe, clear_values))
+ return false; // New universe invalid or unchanged
+
+ // Remove it from the old lookup
+ removeFromLookup(trace, old_universe);
+
+ // Add the ScopeTrace to the new universe in map
+ auto univ_it = m_traceLookup.find(new_universe);
+ if (univ_it == m_traceLookup.end())
+ {
+ // First trace on this universe
+ m_traceLookup.emplace(new_universe, std::vector(1, trace));
+
+ // Maybe start listening
+ if (isRunning())
+ addListener(new_universe);
+ }
+ else
+ {
+ // Add this new trace to the existing universe
+ auto& univs_item = univ_it->second;
+ univs_item.push_back(trace);
+ }
+ return true;
+}
+
+void ScopeModel::removeFromLookup(ScopeTrace* trace, uint16_t old_universe)
+{
+ // Remove from old universe
+ auto univ_it = m_traceLookup.find(old_universe);
+ if (univ_it != m_traceLookup.end())
+ {
+ // Remove from old Universe
+ auto& univ_item = univ_it->second;
+ for (auto it = univ_item.begin(); it != univ_item.end(); /**/)
+ {
+ if ((*it) == trace)
+ {
+ it = univ_item.erase(it);
+ }
+ else
+ {
+ ++it;
+ }
+ }
+
+ // If was last trace on a universe, stop listening
+ if (univ_item.empty())
+ {
+ for (auto it = m_listeners.begin(); it != m_listeners.end(); /**/)
+ {
+ if ((*it)->universe() == old_universe)
+ {
+ // Disconnect signal
+ (*it)->removeDirectCallback(this);
+ it = m_listeners.erase(it);
+ }
+ else
+ {
+ ++it;
+ }
+ }
+ }
+ }
+}
+
+void ScopeModel::addListener(uint16_t universe)
+{
+ auto listener = sACNManager::Instance().getListener(universe);
+ listener->addDirectCallback(this);
+ m_listeners.push_back(listener);
+}
+
+void ScopeModel::updateMaxValue()
+{
+ for (const ScopeTrace* trace : m_traceTable)
+ {
+ if (trace->isSixteenBit())
+ {
+ setMaxValue(kMaxDmx16);
+ return;
+ }
+ }
+ setMaxValue(kMaxDmx8);
+}
+
+void ScopeModel::setMaxValue(qreal maxValue)
+{
+ if (m_maxValue == maxValue)
+ return;
+ m_maxValue = maxValue;
+ maxValueChanged();
+}
+
+void ScopeModel::triggerNow(qreal offset)
+{
+ {
+ // Determine approximate offset to wallclock time by grabbing both
+ const qint64 now_ms = sACNManager::nsecsElapsed() / 1000000;
+ const qint64 nowWallclock_ms = QDateTime::currentMSecsSinceEpoch();
+ m_wallclockTrigger_ms = (nowWallclock_ms - now_ms) + (offset / 1000.0);
+ }
+
+ m_startOffset = offset;
+ // Update the offsets of all traces
+ for (ScopeTrace* trace : m_traceTable)
+ {
+ trace->applyOffset(offset);
+ }
+ // Reset the counters
+ for (sACNManager::tListener& listener : m_listeners)
+ {
+ auto sources = listener->getSourceList();
+ for (sACNSource* source : sources)
+ {
+ source->resetSeqErr();
+ source->resetJumps();
+ source->fpscounter.ClearHistogram();
+ }
+ }
+
+ emit queueTriggered();
+}
+
+QRectF ScopeModel::traceExtents() const
+{
+ return QRectF(0, 0, m_endTime, m_maxValue);
+}
+
+qreal ScopeModel::endTime() const
+{
+ if (isTriggered())
+ return (qreal(sACNManager::elapsed()) / 1000) - m_startOffset;
+
+ return m_endTime;
+}
+
+bool ScopeModel::asWallclockTime(QDateTime& datetime, qreal time) const
+{
+ if (m_wallclockTrigger_ms == 0)
+ return false;
+
+ datetime.setMSecsSinceEpoch(m_wallclockTrigger_ms + ((time + m_startOffset) * 1000));
+ return true;
+}
+
+void ScopeModel::sACNListenerDmxReceived(tock packet_tock, int universe, const std::array& levels)
+{
+ if (!m_running)
+ return;
+
+ // Find traces for universe
+ auto it = m_traceLookup.find(universe);
+ if (it == m_traceLookup.end())
+ return;
+
+ const qreal timestamp = std::chrono::duration(packet_tock.Get()).count();
+
+ if (!isTriggered())
+ {
+ // Only store one level for each trace until the trigger is fired
+ for (ScopeTrace* trace : it->second)
+ {
+ trace->setFirstPoint(timestamp, levels);
+ }
+
+ uint16_t current_level;
+ if (universe == m_trigger.universe && fillValue(current_level, m_trigger.address_hi - 1, m_trigger.address_lo - 1, levels))
+ {
+ // Have a current level, maybe start the clock
+ switch (m_trigger.mode)
+ {
+ default: break;
+ case Trigger::Above:
+ if (current_level > m_trigger.level)
+ triggerNow(timestamp);
+ break;
+ case Trigger::Below:
+ if (current_level < m_trigger.level)
+ triggerNow(timestamp);
+ break;
+ case Trigger::LevelCross:
+ if ((m_trigger.last_level == m_trigger.level && current_level != m_trigger.level) // Was at, now not
+ || (m_trigger.last_level > m_trigger.level && current_level < m_trigger.level) // Was above, now below
+ || (m_trigger.last_level != -1 && m_trigger.last_level < m_trigger.level && current_level > m_trigger.level) // Was below, now above
+ )
+ triggerNow(timestamp);
+ break;
+ }
+ m_trigger.last_level = current_level;
+ }
+ return;
+ }
+
+ // Time in seconds
+ {
+ m_endTime = timestamp - m_startOffset;
+ qDebug() << m_endTime;
+ if (m_runTime > 0 && m_endTime >= m_runTime)
+ {
+ emit queueStop();
+ return;
+ }
+ }
+
+ for (ScopeTrace* trace : it->second)
+ {
+ trace->addPoint(m_endTime, levels, m_storeAllPoints);
+ }
+}
+
+GlScopeWidget::GlScopeWidget(QWidget* parent)
+ : QOpenGLWidget(parent)
+{
+ setUpdateBehavior(QOpenGLWidget::NoPartialUpdate);
+
+ m_model = new ScopeModel(this);
+ connect(m_model, &ScopeModel::runningChanged, this, &GlScopeWidget::onRunningChanged);
+ connect(m_model, &ScopeModel::traceVisibilityChanged, this, qOverload<>(&QOpenGLWidget::update));
+
+ setMinimumSize(200, 200);
+ setVerticalScaleMode(VerticalScale::Percent);
+ setScopeView();
+}
+
+GlScopeWidget::~GlScopeWidget()
+{
+ cleanupGL();
+}
+
+void GlScopeWidget::setVerticalScaleMode(VerticalScale scaleMode)
+{
+ switch (scaleMode)
+ {
+ default:
+ return; // Invalid, do nothing
+ case VerticalScale::Percent:
+ {
+ m_levelInterval = 10;
+ m_scopeView.setBottom(kMaxDmx8); // The 16 bit matrix downscales to 8bit
+ } break;
+ case VerticalScale::Dmx8:
+ {
+ m_levelInterval = 20;
+ m_scopeView.setBottom(kMaxDmx8);
+ } break;
+ case VerticalScale::Dmx16:
+ {
+ m_levelInterval = 10000;
+ m_scopeView.setBottom(kMaxDmx16);
+ } break;
+ }
+
+ m_verticalScaleMode = scaleMode;
+
+ updateMVPMatrix();
+ update();
+}
+
+void GlScopeWidget::setScopeView(const QRectF& rect)
+{
+ if (rect.isEmpty())
+ {
+ m_scopeView = m_model->traceExtents();
+ m_scopeView.setRight(m_defaultIntervalCount * m_timeInterval);
+ setVerticalScaleMode(m_verticalScaleMode);
+ }
+ else if (rect == m_scopeView)
+ {
+ return;
+ }
+ else
+ {
+ m_scopeView = rect;
+ }
+
+ updateMVPMatrix();
+ update();
+}
+
+void GlScopeWidget::setTimeDivisions(int milliseconds)
+{
+ if (timeDivisions() == milliseconds)
+ return;
+
+ // Must not go lower than 1ms
+ if (milliseconds < 1)
+ milliseconds = 1;
+
+ m_timeInterval = static_cast(milliseconds) / 1000.0;
+ m_scopeView.setWidth(m_timeInterval * m_defaultIntervalCount);
+ updateMVPMatrix();
+ update();
+
+ emit timeDivisionsChanged(milliseconds);
+}
+
+void GlScopeWidget::setTimeFormat(TimeFormat format)
+{
+ if (format == m_timeFormat)
+ return;
+
+ m_timeFormat = format;
+ update();
+
+ onRunningChanged(m_model->isRunning());
+
+ emit timeFormatChanged();
+}
+
+void GlScopeWidget::initializeGL()
+{
+ // Reparenting to a different top-level window causes the OpenGL Context to be destroyed and recreated
+ connect(context(), &QOpenGLContext::aboutToBeDestroyed, this, &GlScopeWidget::cleanupGL);
+
+ initializeOpenGLFunctions();
+
+ glClearColor(0, 0, 0, 1);
+
+ // Compile shaders
+ // 2D passthrough shader
+ const char* vertexShaderSource =
+ "in vec2 vertex;\n"
+ "uniform mat4 mvp;\n"
+ "void main()\n"
+ "{\n"
+ " gl_Position = mvp * vec4(vertex.x, vertex.y, 0, 1.0);\n"
+ "}\n";
+
+ const char* fragmentShaderSource =
+ "uniform vec4 color;\n"
+ "void main()\n"
+ "{\n"
+ " gl_FragColor = color;\n"
+ "}\n";
+
+ m_program = new QOpenGLShaderProgram(this);
+ if (!m_program->addShaderFromSourceCode(QOpenGLShader::Vertex, vertexShaderSource))
+ {
+ qDebug() << "Vertex Shader Failed:" << m_program->log();
+ cleanupGL();
+ return;
+ }
+
+ if (!m_program->addShaderFromSourceCode(QOpenGLShader::Fragment, fragmentShaderSource))
+ {
+ qDebug() << "Fragment Shader Failed:" << m_program->log();
+ cleanupGL();
+ return;
+ }
+
+ if (!m_program->link())
+ {
+ qDebug() << "Shader Link Failed:" << m_program->log();
+ cleanupGL();
+ return;
+ }
+
+ m_vertexLocation = m_program->attributeLocation("vertex");
+ Q_ASSERT(m_vertexLocation != -1);
+ m_matrixUniform = m_program->uniformLocation("mvp");
+ Q_ASSERT(m_matrixUniform != -1);
+ m_colorUniform = m_program->uniformLocation("color");
+ Q_ASSERT(m_colorUniform != -1);
+}
+
+void GlScopeWidget::cleanupGL()
+{
+ // The context is about to be destroyed, it may be recreated later
+ if (m_program == nullptr)
+ return; // Never started
+
+ makeCurrent();
+
+ delete m_program;
+ m_program = nullptr;
+
+ doneCurrent();
+}
+
+inline void DrawLevelAxisText(QPainter& painter, const QFontMetricsF& metrics, const QRectF& scopeWindow, int level, qreal y_scale, const QString& postfix)
+{
+ qreal y = scopeWindow.height() - (level * y_scale);
+
+ // TODO: use QStaticText to optimise the text layout
+ const QString text = QString::number(level) + postfix;
+
+ QRectF fontRect = metrics.boundingRect(text);
+ fontRect.moveBottomRight(QPointF(-AXIS_TICK_SIZE, y + (fontRect.height() / 2.0)));
+
+ painter.drawText(fontRect, text, QTextOption(Qt::AlignLeft));
+}
+
+void GlScopeWidget::paintGL()
+{
+ const qreal endTime = m_model->endTime();
+ if (m_followNow)
+ {
+ if (endTime > m_scopeView.width())
+ {
+ m_scopeView.moveRight(endTime);
+ updateMVPMatrix();
+ }
+ }
+
+ glClear(GL_COLOR_BUFFER_BIT);
+
+ // Pixel-perfect grid lines
+ std::vector gridLines;
+ gridLines.reserve((11 + 14) * 2); // 10 ticks across, 13 up for 0-255
+
+ // Colors
+ static const QColor gridColor(0x43, 0x43, 0x43);
+ static const QColor textColor(Qt::white);
+ static const QColor timeCursorColor(Qt::white);
+ static const QColor triggerCursorColor(Qt::gray);
+
+ // Draw the axes using QPainter as is easiest way to get the text
+ // It's ok if the labels aren't pixel perfect
+ {
+ QPen textPen;
+ textPen.setColor(textColor);
+
+ QPainter painter(this);
+
+ painter.setRenderHint(QPainter::Antialiasing, true);
+ painter.setPen(textPen);
+
+ // Origin is the bottom left of the scope window
+ QPointF origin(rect().bottomLeft().x() + AXIS_LABEL_WIDTH, rect().bottomLeft().y() - AXIS_LABEL_HEIGHT);
+
+ QRectF scopeWindow;
+ scopeWindow.setBottomLeft(origin);
+ scopeWindow.setTop(rect().top() + TOP_GAP);
+ scopeWindow.setRight(rect().right() - RIGHT_GAP);
+
+ // Draw nothing if tiny
+ if (scopeWindow.width() < AXIS_TICK_SIZE)
+ return;
+
+ QFont font;
+ QFontMetricsF metrics(font);
+
+ painter.translate(scopeWindow.topLeft().x(), scopeWindow.topLeft().y());
+
+ // Draw vertical (level) axis
+ {
+ const float lineLeft = m_scopeView.left();
+ const float lineRight = std::fmaxf(m_scopeView.right(), endTime);
+
+ if (m_verticalScaleMode == VerticalScale::Percent)
+ {
+ const QString postfix = QStringLiteral("%");
+ const qreal max_value = 100.0;
+ const qreal y_scale = scopeWindow.height() / 100.0; // Percent
+ const float value_scale = kMaxDmx8 / 100.0f;
+
+ // From bottom to top
+ for (qreal value = m_scopeView.top(); value < max_value; value += m_levelInterval)
+ {
+ // Grid lines in trace space
+ gridLines.emplace_back(lineLeft, static_cast(value) * value_scale);
+ gridLines.emplace_back(lineRight, static_cast(value) * value_scale);
+ DrawLevelAxisText(painter, metrics, scopeWindow, value, y_scale, postfix);
+ }
+
+ // Final row, may be uneven
+ // Grid lines in trace space
+ gridLines.emplace_back(lineLeft, static_cast(m_scopeView.bottom()));
+ gridLines.emplace_back(lineRight, static_cast(m_scopeView.bottom()));
+ DrawLevelAxisText(painter, metrics, scopeWindow, max_value, y_scale, postfix);
+ }
+ else
+ {
+ const QString postfix;
+ const int max_value = m_scopeView.bottom();
+ const qreal y_scale = scopeWindow.height() / m_scopeView.bottom();
+
+ // From bottom to top
+ for (int value = m_scopeView.top(); value < max_value; value += m_levelInterval)
+ {
+ // Grid lines in trace space
+ gridLines.emplace_back(lineLeft, static_cast(value));
+ gridLines.emplace_back(lineRight, static_cast(value));
+ DrawLevelAxisText(painter, metrics, scopeWindow, value, y_scale, postfix);
+ }
+
+ // Final row, may be uneven
+ // Grid lines in trace space
+ gridLines.emplace_back(lineLeft, max_value);
+ gridLines.emplace_back(lineRight, max_value);
+ DrawLevelAxisText(painter, metrics, scopeWindow, max_value, y_scale, postfix);
+ }
+ }
+
+ // Draw horizontal (time) axis
+ painter.resetTransform();
+ painter.translate(scopeWindow.bottomLeft().x(), scopeWindow.bottomLeft().y());
+
+ const qreal x_scale = scopeWindow.width() / m_scopeView.width();
+
+ if (m_timeFormat == TimeFormat::Elapsed)
+ {
+ const bool milliseconds = (m_timeInterval < 1.0);
+ for (qreal time = roundCeilMultiple(m_scopeView.left(), m_timeInterval); time < m_scopeView.right() + 0.001; time += m_timeInterval)
+ {
+ // Grid lines in trace space
+ gridLines.emplace_back(static_cast(time), 0.0f);
+ gridLines.emplace_back(static_cast(time), m_scopeView.bottom());
+
+ const qreal x = (time - m_scopeView.left()) * x_scale;
+
+ const QString text = milliseconds ? QStringLiteral("%1ms").arg(time * 1000.0) : QStringLiteral("%1s").arg(time);
+ QRectF fontRect = metrics.boundingRect(text);
+ fontRect.moveCenter(QPointF(x, AXIS_LABEL_HEIGHT / 2.0));
+ painter.drawText(fontRect, text, QTextOption(Qt::AlignLeft));
+ }
+ }
+ else
+ {
+ QDateTime datetime = QDateTime::currentDateTime();
+
+ for (qreal time = roundCeilMultiple(m_scopeView.left(), m_timeInterval); time < m_scopeView.right() + 0.001; time += m_timeInterval)
+ {
+ // Grid lines in trace space
+ gridLines.emplace_back(static_cast(time), 0.0f);
+ gridLines.emplace_back(static_cast(time), m_scopeView.bottom());
+
+ const qreal x = (time - m_scopeView.left()) * x_scale;
+
+ if (!m_model->asWallclockTime(datetime, time))
+ {
+ // Add time interval
+ datetime = datetime.addMSecs(m_timeInterval * 1000);
+ }
+
+ const QString text = x_scale > 80.0 ? datetime.toString(TimeFormatString) : datetime.toString(ShortTimeFormatString);
+
+ QRectF fontRect = metrics.boundingRect(text);
+ fontRect.moveCenter(QPointF(x, AXIS_LABEL_HEIGHT / 2.0));
+ painter.drawText(fontRect, text, QTextOption(Qt::AlignLeft));
+ }
+ }
+ }
+
+ // Unable to render at all
+ if (!m_program)
+ return;
+
+ m_program->bind();
+ glEnableVertexAttribArray(m_vertexLocation);
+
+ m_program->setUniformValue(m_matrixUniform, m_mvpMatrix);
+
+ // Draw the gridlines
+ {
+ m_program->setUniformValue(m_colorUniform, gridColor);
+ glVertexAttribPointer(m_vertexLocation, 2, GL_FLOAT, GL_FALSE, 0, gridLines.data());
+ glDrawArrays(GL_LINES, 0, gridLines.size());
+ }
+
+ if (m_model->isTriggered())
+ {
+ // Draw the current time
+ m_program->setUniformValue(m_colorUniform, timeCursorColor);
+ const std::vector nowLine = {
+ {static_cast(m_model->endTime()), 0},
+ {static_cast(m_model->endTime()), static_cast(m_scopeView.bottom())}
+ };
+ glVertexAttribPointer(m_vertexLocation, 2, GL_FLOAT, GL_FALSE, 0, nowLine.data());
+ glDrawArrays(GL_LINE_STRIP, 0, 2);
+ }
+ else if (m_model->triggerType() != ScopeModel::Trigger::FreeRun)
+ {
+ // Draw the trigger level marker
+ m_program->setUniformValue(m_colorUniform, triggerCursorColor);
+
+ const std::vector triggerLine = makeTriggerLine(m_model->triggerType());
+
+ glVertexAttribPointer(m_vertexLocation, 2, GL_FLOAT, GL_FALSE, 0, triggerLine.data());
+ glDrawArrays(GL_TRIANGLES, 0, triggerLine.size());
+ }
+
+
+ for (ScopeTrace* trace : m_model->traces())
+ {
+ if (!trace->enabled())
+ continue;
+
+ // Change the scale as needed
+ if (m_verticalScaleMode == VerticalScale::Percent)
+ {
+ m_program->setUniformValue(m_matrixUniform, trace->isSixteenBit() ? m_mvpMatrix16 : m_mvpMatrix);
+ }
+
+ m_program->setUniformValue(m_colorUniform, trace->color());
+ const auto levels = trace->values();
+ glVertexAttribPointer(m_vertexLocation, 2, GL_FLOAT, GL_FALSE, 0, levels.value().data());
+ glDrawArrays(GL_LINE_STRIP, 0, levels.value().size());
+ }
+
+ glDisableVertexAttribArray(m_vertexLocation);
+ m_program->release();
+}
+
+void GlScopeWidget::resizeGL(int w, int h)
+{
+ // Pixel-to-pixel projection
+ m_viewMatrix.setToIdentity();
+ m_viewMatrix.ortho(0, w, 0, h, -1.0f, 1.0f);
+
+ updateMVPMatrix();
+}
+
+void GlScopeWidget::timerEvent(QTimerEvent* /*ev*/)
+{
+ // Schedule an update
+ update();
+}
+
+void GlScopeWidget::onRunningChanged(bool running)
+{
+ // Must update the timescale when displaying wallclock time
+ if (running || (m_timeFormat == TimeFormat::Wallclock && !m_model->isTriggered()))
+ {
+ if (m_renderTimer == 0)
+ {
+ // Redraw at the screen refresh or 5fps, whichever is greater
+ const qreal framerate = screen()->refreshRate();
+ m_renderTimer = startTimer(framerate > 5.0 ? static_cast(std::ceil(1000.0 / framerate)) : 200);
+ }
+ }
+ else
+ {
+ killTimer(m_renderTimer);
+ m_renderTimer = 0;
+ }
+}
+
+void GlScopeWidget::updateMVPMatrix()
+{
+ QMatrix4x4 modelMatrix;
+
+ // Translate to origin
+ modelMatrix.translate(AXIS_LABEL_WIDTH, AXIS_LABEL_HEIGHT);
+
+ // Horizontal scale
+ const qreal pix_width = rect().width() - AXIS_LABEL_WIDTH - RIGHT_GAP;
+ const qreal x_scale = pix_width / m_scopeView.width();
+
+ // Vertical scale
+ const qreal pix_height = rect().height() - AXIS_LABEL_HEIGHT - TOP_GAP;
+ const qreal y_scale = pix_height / m_scopeView.height();
+
+ modelMatrix.scale(x_scale, y_scale, 1);
+
+ // Translate to current time and vertical offset
+ modelMatrix.translate(-m_scopeView.left(), -m_scopeView.top(), 0);
+
+ m_mvpMatrix = m_viewMatrix * modelMatrix;
+
+ if (m_verticalScaleMode == VerticalScale::Percent)
+ {
+ // Vertical scale for 16bit
+ const qreal y_scale16 = pix_height / (m_scopeView.height() * 256.0);
+
+ QMatrix4x4 modelMatrix16;
+ modelMatrix16.translate(AXIS_LABEL_WIDTH, AXIS_LABEL_HEIGHT);
+ modelMatrix16.scale(x_scale, y_scale16, 1);
+
+ // Translate to current time and vertical offset
+ modelMatrix16.translate(-m_scopeView.left(), -m_scopeView.top(), 0);
+
+ m_mvpMatrix16 = m_viewMatrix * modelMatrix16;
+ }
+}
+
+std::vector GlScopeWidget::makeTriggerLine(ScopeModel::Trigger type)
+{
+ const float level = static_cast(m_model->triggerLevel());
+ const float h_offset = m_timeInterval / 6.0;
+ float v_offset = 0;
+
+ switch (type)
+ {
+ default: return std::vector();
+
+ case ScopeModel::Trigger::Above: v_offset = 10.0f; break;
+ case ScopeModel::Trigger::Below: v_offset = -10.0f; break;
+ case ScopeModel::Trigger::LevelCross:
+ // Two triangles point-to-point
+ return {
+ { static_cast(m_scopeView.left()) - h_offset, level + 5.0f},
+ { static_cast(m_scopeView.left()), level },
+ { static_cast(m_scopeView.left()) - h_offset, level - 5.0f },
+ { static_cast