From 349068029c9e0efdfa953329df3e05b406b45aed Mon Sep 17 00:00:00 2001 From: David Graeff Date: Mon, 22 Jan 2018 23:22:27 +0100 Subject: [PATCH 01/11] Use Qt3D for the scope window; Make Dso parameters/logic gui independant New features * Extend marker system: Allow unlimited markers and zoomviews * Extend math channels: Dynamic math channels. Math Modes: +,-,* * Add a self-calibration for gnd-offset/gain-limit values. Bug fix * Export: Always keep a pointer to the the last sampleSet. This ways exports work if the user uses a one-time trigger and want to export the visible graphs. * Disable roll-mode for DSO6022 Documentation * API of DsoControl documented and slightly changed. Easier maintenance * DsoControl: Samples are always in [-1,1] range now and user-gain/offsets are applied in post/graphgenerator. * Qt3d is used for the scope window now. No direct shader programming for different platforms or GL versions anymore. * Move settings related files to own subfolder * Make GUI docks independant objects. They react on settings changes and manipulate settings or the dsocontrol object directly. * Split HantekDsoControl into DsoControl+DsoLoop+DsoCommandQueue * sispinbox does not allow a fixed set of values anymore. Made it over complicated and we have QComboBox for this purpose anyway. Optimize * Allow DSO models to not always have 9 fixed gain steps. The DSO6022 for example only has 2. * Remove a lot of allocations in the fetch-samples/convert/show-graph hot-path. We have a PostProcessingResult pool now. * Use the new GlScope class to draw the export graph, no dublicate code and it does look exactly like on screen (except different dimensions). --- docs/adddevice.md | 36 +- openhantek/CMakeLists.txt | 47 +- .../configdialog/DsoConfigAnalysisPage.cpp | 14 +- .../src/configdialog/DsoConfigAnalysisPage.h | 6 +- .../src/configdialog/DsoConfigColorsPage.cpp | 110 +- .../src/configdialog/DsoConfigColorsPage.h | 43 +- .../src/configdialog/DsoConfigFilesPage.cpp | 2 +- .../src/configdialog/DsoConfigFilesPage.h | 6 +- .../src/configdialog/DsoConfigScopePage.cpp | 11 +- .../src/configdialog/DsoConfigScopePage.h | 6 +- openhantek/src/configdialog/configdialog.cpp | 6 +- openhantek/src/configdialog/configdialog.h | 7 +- openhantek/src/docks/DebugDock.cpp | 156 ++ openhantek/src/docks/DebugDock.h | 37 + openhantek/src/docks/HorizontalDock.cpp | 383 ++--- openhantek/src/docks/HorizontalDock.h | 80 +- openhantek/src/docks/SpectrumDock.cpp | 100 -- openhantek/src/docks/SpectrumDock.h | 62 - openhantek/src/docks/TriggerDock.cpp | 170 ++- openhantek/src/docks/TriggerDock.h | 45 +- openhantek/src/docks/VoltageDock.cpp | 129 -- openhantek/src/docks/VoltageDock.h | 76 - .../src/docks/VoltageOrSpectrumDock.cpp | 288 ++++ openhantek/src/docks/VoltageOrSpectrumDock.h | 53 + openhantek/src/docks/dockwindows.cpp | 16 +- openhantek/src/docks/dockwindows.h | 3 +- openhantek/src/docks/gainAdjustDock.cpp | 253 ++++ openhantek/src/docks/gainAdjustDock.h | 37 + openhantek/src/dsowidget.cpp | 614 -------- openhantek/src/dsowidget.h | 130 -- openhantek/src/exporting/exportcsv.cpp | 94 +- openhantek/src/exporting/exportcsv.h | 18 +- openhantek/src/exporting/exporterinterface.h | 66 +- .../src/exporting/exporterprocessor.cpp | 6 - openhantek/src/exporting/exporterprocessor.h | 14 - openhantek/src/exporting/exporterregistry.cpp | 89 +- openhantek/src/exporting/exporterregistry.h | 41 +- openhantek/src/exporting/exportimage.cpp | 47 +- openhantek/src/exporting/exportimage.h | 18 +- openhantek/src/exporting/exportprint.cpp | 31 +- openhantek/src/exporting/exportprint.h | 18 +- openhantek/src/exporting/exportsettings.cpp | 22 + openhantek/src/exporting/exportsettings.h | 15 +- .../src/exporting/legacyexportdrawer.cpp | 401 ++--- openhantek/src/exporting/legacyexportdrawer.h | 32 +- openhantek/src/exporting/readme.md | 6 +- openhantek/src/glscope.cpp | 475 ------ openhantek/src/glscope.h | 117 -- openhantek/src/glscopegraph.cpp | 81 - openhantek/src/glscopegraph.h | 27 - openhantek/src/hantekdso/channelusage.cpp | 30 + openhantek/src/hantekdso/channelusage.h | 45 + openhantek/src/hantekdso/controlsettings.cpp | 19 - openhantek/src/hantekdso/controlsettings.h | 66 - .../src/hantekdso/controlspecification.cpp | 5 - .../src/hantekdso/controlspecification.h | 94 -- openhantek/src/hantekdso/devicesettings.cpp | 136 ++ openhantek/src/hantekdso/devicesettings.h | 261 ++++ openhantek/src/hantekdso/dsocommandqueue.cpp | 118 ++ openhantek/src/hantekdso/dsocommandqueue.h | 86 ++ openhantek/src/hantekdso/dsocontrol.cpp | 893 +++++++++++ openhantek/src/hantekdso/dsocontrol.h | 244 +++ openhantek/src/hantekdso/dsoloop.cpp | 388 +++++ openhantek/src/hantekdso/dsoloop.h | 74 + openhantek/src/hantekdso/dsomodel.cpp | 7 +- openhantek/src/hantekdso/dsomodel.h | 19 +- openhantek/src/hantekdso/dsosamples.h | 42 +- openhantek/src/hantekdso/enums.cpp | 154 +- openhantek/src/hantekdso/enums.h | 33 +- openhantek/src/hantekdso/errorcodes.h | 7 +- openhantek/src/hantekdso/hantekdsocontrol.cpp | 1326 ----------------- openhantek/src/hantekdso/hantekdsocontrol.h | 296 ---- openhantek/src/hantekdso/modelregistry.cpp | 7 +- openhantek/src/hantekdso/modelregistry.h | 13 +- .../src/hantekdso/models/modelDSO2090.cpp | 91 +- .../src/hantekdso/models/modelDSO2090.h | 7 +- .../src/hantekdso/models/modelDSO2150.cpp | 72 +- .../src/hantekdso/models/modelDSO2150.h | 5 +- .../src/hantekdso/models/modelDSO2250.cpp | 54 +- .../src/hantekdso/models/modelDSO2250.h | 5 +- .../src/hantekdso/models/modelDSO5200.cpp | 69 +- .../src/hantekdso/models/modelDSO5200.h | 7 +- .../src/hantekdso/models/modelDSO6022.cpp | 87 +- .../src/hantekdso/models/modelDSO6022.h | 7 +- .../src/hantekdso/modelspecification.cpp | 5 + openhantek/src/hantekdso/modelspecification.h | 143 ++ openhantek/src/hantekdso/readme.md | 49 +- openhantek/src/hantekdso/states.h | 5 - openhantek/src/hantekprotocol/bulkStructs.cpp | 2 +- .../hantekprotocol/{bulkcode.h => codes.h} | 143 +- .../src/hantekprotocol/controlStructs.cpp | 34 +- .../src/hantekprotocol/controlStructs.h | 23 +- openhantek/src/hantekprotocol/controlcode.h | 140 -- openhantek/src/hantekprotocol/definitions.h | 6 - openhantek/src/hantekprotocol/types.h | 10 + openhantek/src/main.cpp | 118 +- openhantek/src/mainwindow.cpp | 282 ++-- openhantek/src/mainwindow.h | 21 +- openhantek/src/mainwindow.ui | 61 +- openhantek/src/post/enums.cpp | 70 + openhantek/src/post/enums.h | 56 + openhantek/src/post/graphgenerator.cpp | 174 +-- openhantek/src/post/graphgenerator.h | 25 +- openhantek/src/post/mathchannelgenerator.cpp | 68 +- openhantek/src/post/mathchannelgenerator.h | 13 +- openhantek/src/post/postprocessing.cpp | 61 +- openhantek/src/post/postprocessing.h | 25 +- .../src/post/postprocessingsettings.cpp | 77 +- openhantek/src/post/postprocessingsettings.h | 63 +- openhantek/src/post/ppresult.cpp | 41 +- openhantek/src/post/ppresult.h | 74 +- openhantek/src/post/processor.h | 6 + openhantek/src/post/readme.md | 2 +- openhantek/src/post/selfcalibration.cpp | 67 + openhantek/src/post/selfcalibration.h | 38 + openhantek/src/post/softwaretrigger.cpp | 71 +- openhantek/src/post/softwaretrigger.h | 21 +- openhantek/src/post/spectrumgenerator.cpp | 99 +- openhantek/src/post/spectrumgenerator.h | 23 +- openhantek/src/scopesettings.h | 80 - openhantek/src/scopeview/glframe.cpp | 154 ++ openhantek/src/scopeview/glframe.h | 107 ++ openhantek/src/scopeview/glicon.cpp | 111 ++ openhantek/src/scopeview/glicon.h | 87 ++ openhantek/src/scopeview/glmousedevice.cpp | 6 + openhantek/src/scopeview/glmousedevice.h | 52 + openhantek/src/scopeview/glmoveresizesnap.cpp | 184 +++ openhantek/src/scopeview/glmoveresizesnap.h | 134 ++ openhantek/src/scopeview/glscope.cpp | 348 +++++ openhantek/src/scopeview/glscope.h | 216 +++ openhantek/src/scopeview/glscopegraph.cpp | 133 ++ openhantek/src/scopeview/glscopegraph.h | 56 + openhantek/src/scopeview/glscopegrid.cpp | 237 +++ openhantek/src/scopeview/glscopegrid.h | 24 + openhantek/src/scopeview/glscopehover.h | 24 + .../src/scopeview/glscopezoomviewport.cpp | 127 ++ .../src/scopeview/glscopezoomviewport.h | 84 ++ openhantek/src/settings.cpp | 267 ---- openhantek/src/settings.h | 38 - openhantek/src/settings/colorsettings.h | 71 + .../src/settings/markerandzoomsettings.cpp | 18 + .../src/settings/markerandzoomsettings.h | 62 + openhantek/src/settings/scopechannel.cpp | 100 ++ openhantek/src/settings/scopechannel.h | 84 ++ openhantek/src/settings/scopemathchannel.h | 49 + openhantek/src/settings/scopesettings.cpp | 146 ++ openhantek/src/settings/scopesettings.h | 87 ++ openhantek/src/settings/settings.cpp | 68 + openhantek/src/settings/settings.h | 45 + openhantek/src/settings/spectrum.h | 39 + openhantek/src/settings/viewsettings.cpp | 165 ++ openhantek/src/settings/viewsettings.h | 68 + openhantek/src/usb/usbdevice.cpp | 33 +- openhantek/src/usb/usbdevice.h | 63 +- openhantek/src/usb/usbdevicedefinitions.h | 9 +- openhantek/src/utils/debugnotify.cpp | 66 + openhantek/src/utils/debugnotify.h | 52 + openhantek/src/utils/enumclass.h | 47 - openhantek/src/utils/enumhelper.h | 44 + openhantek/src/utils/getwithdefault.h | 125 ++ openhantek/src/utils/observer.h | 20 + openhantek/src/utils/printutils.cpp | 20 +- openhantek/src/utils/printutils.h | 4 +- openhantek/src/utils/scopecoordinates.h | 65 + openhantek/src/viewconstants.h | 3 + openhantek/src/viewsettings.h | 49 - openhantek/src/widgets/colorbox.cpp | 67 +- openhantek/src/widgets/colorbox.h | 14 +- openhantek/src/widgets/dsowidget.cpp | 489 ++++++ openhantek/src/widgets/dsowidget.h | 135 ++ openhantek/src/widgets/levelslider.cpp | 592 +++----- openhantek/src/widgets/levelslider.h | 173 ++- openhantek/src/widgets/sispinbox.cpp | 196 +-- openhantek/src/widgets/sispinbox.h | 79 +- readme.md | 2 +- 175 files changed, 10188 insertions(+), 7057 deletions(-) create mode 100644 openhantek/src/docks/DebugDock.cpp create mode 100644 openhantek/src/docks/DebugDock.h delete mode 100644 openhantek/src/docks/SpectrumDock.cpp delete mode 100644 openhantek/src/docks/SpectrumDock.h delete mode 100644 openhantek/src/docks/VoltageDock.cpp delete mode 100644 openhantek/src/docks/VoltageDock.h create mode 100644 openhantek/src/docks/VoltageOrSpectrumDock.cpp create mode 100644 openhantek/src/docks/VoltageOrSpectrumDock.h create mode 100644 openhantek/src/docks/gainAdjustDock.cpp create mode 100644 openhantek/src/docks/gainAdjustDock.h delete mode 100644 openhantek/src/dsowidget.cpp delete mode 100644 openhantek/src/dsowidget.h delete mode 100644 openhantek/src/exporting/exporterprocessor.cpp delete mode 100644 openhantek/src/exporting/exporterprocessor.h create mode 100644 openhantek/src/exporting/exportsettings.cpp delete mode 100644 openhantek/src/glscope.cpp delete mode 100644 openhantek/src/glscope.h delete mode 100644 openhantek/src/glscopegraph.cpp delete mode 100644 openhantek/src/glscopegraph.h create mode 100644 openhantek/src/hantekdso/channelusage.cpp create mode 100644 openhantek/src/hantekdso/channelusage.h delete mode 100644 openhantek/src/hantekdso/controlsettings.cpp delete mode 100644 openhantek/src/hantekdso/controlsettings.h delete mode 100644 openhantek/src/hantekdso/controlspecification.cpp delete mode 100644 openhantek/src/hantekdso/controlspecification.h create mode 100644 openhantek/src/hantekdso/devicesettings.cpp create mode 100644 openhantek/src/hantekdso/devicesettings.h create mode 100644 openhantek/src/hantekdso/dsocommandqueue.cpp create mode 100644 openhantek/src/hantekdso/dsocommandqueue.h create mode 100644 openhantek/src/hantekdso/dsocontrol.cpp create mode 100644 openhantek/src/hantekdso/dsocontrol.h create mode 100644 openhantek/src/hantekdso/dsoloop.cpp create mode 100644 openhantek/src/hantekdso/dsoloop.h delete mode 100644 openhantek/src/hantekdso/hantekdsocontrol.cpp delete mode 100644 openhantek/src/hantekdso/hantekdsocontrol.h create mode 100644 openhantek/src/hantekdso/modelspecification.cpp create mode 100644 openhantek/src/hantekdso/modelspecification.h rename openhantek/src/hantekprotocol/{bulkcode.h => codes.h} (77%) delete mode 100644 openhantek/src/hantekprotocol/controlcode.h create mode 100644 openhantek/src/post/enums.cpp create mode 100644 openhantek/src/post/enums.h create mode 100644 openhantek/src/post/selfcalibration.cpp create mode 100644 openhantek/src/post/selfcalibration.h delete mode 100644 openhantek/src/scopesettings.h create mode 100644 openhantek/src/scopeview/glframe.cpp create mode 100644 openhantek/src/scopeview/glframe.h create mode 100644 openhantek/src/scopeview/glicon.cpp create mode 100644 openhantek/src/scopeview/glicon.h create mode 100644 openhantek/src/scopeview/glmousedevice.cpp create mode 100644 openhantek/src/scopeview/glmousedevice.h create mode 100644 openhantek/src/scopeview/glmoveresizesnap.cpp create mode 100644 openhantek/src/scopeview/glmoveresizesnap.h create mode 100644 openhantek/src/scopeview/glscope.cpp create mode 100644 openhantek/src/scopeview/glscope.h create mode 100644 openhantek/src/scopeview/glscopegraph.cpp create mode 100644 openhantek/src/scopeview/glscopegraph.h create mode 100644 openhantek/src/scopeview/glscopegrid.cpp create mode 100644 openhantek/src/scopeview/glscopegrid.h create mode 100644 openhantek/src/scopeview/glscopehover.h create mode 100644 openhantek/src/scopeview/glscopezoomviewport.cpp create mode 100644 openhantek/src/scopeview/glscopezoomviewport.h delete mode 100644 openhantek/src/settings.cpp delete mode 100644 openhantek/src/settings.h create mode 100644 openhantek/src/settings/colorsettings.h create mode 100644 openhantek/src/settings/markerandzoomsettings.cpp create mode 100644 openhantek/src/settings/markerandzoomsettings.h create mode 100644 openhantek/src/settings/scopechannel.cpp create mode 100644 openhantek/src/settings/scopechannel.h create mode 100644 openhantek/src/settings/scopemathchannel.h create mode 100644 openhantek/src/settings/scopesettings.cpp create mode 100644 openhantek/src/settings/scopesettings.h create mode 100644 openhantek/src/settings/settings.cpp create mode 100644 openhantek/src/settings/settings.h create mode 100644 openhantek/src/settings/spectrum.h create mode 100644 openhantek/src/settings/viewsettings.cpp create mode 100644 openhantek/src/settings/viewsettings.h create mode 100644 openhantek/src/utils/debugnotify.cpp create mode 100644 openhantek/src/utils/debugnotify.h delete mode 100644 openhantek/src/utils/enumclass.h create mode 100644 openhantek/src/utils/enumhelper.h create mode 100644 openhantek/src/utils/getwithdefault.h create mode 100644 openhantek/src/utils/observer.h create mode 100644 openhantek/src/utils/scopecoordinates.h delete mode 100644 openhantek/src/viewsettings.h create mode 100644 openhantek/src/widgets/dsowidget.cpp create mode 100644 openhantek/src/widgets/dsowidget.h diff --git a/docs/adddevice.md b/docs/adddevice.md index ade9bc9c..2aae61ef 100644 --- a/docs/adddevice.md +++ b/docs/adddevice.md @@ -6,10 +6,13 @@ We only accept new devices whoms firmware is hantek protocol compatible. Codewise you will only need to touch files within `openhantek/src/hantekdso`. ## Firmware and usb access -The firmware goes to `openhantek/res/firmware` in the hex format. Please keep to the filename +The firmware goes to `openhantek/res/firmware` in intel hex format. Please keep to the filename convention devicename-firmware.hex and devicename-loader.hex. The `openhantek/res/firmwares.qrc` should list the new files. -The firmware/60-hantek.rules file needs the usb vendor/device id to add access permissions. + +The firmware/60-hantek.rules file needs the usb vendor/device id to add access permissions for linux users. +MacOS users do not have access restrictions. On Windows there need to be a driver installed that +also manages the usb access. ## The hantek protocol The hantek protocol itself is encoded in the `src/hantekprotocol` files. @@ -24,7 +27,7 @@ You will only need to touch files within `openhantek/src/hantekdso/models`. struct ModelDSO2090 : public DSOModel { static const int ID = 0x2090; // Freely chooseable but unique id ModelDSO2090(); - void applyRequirements(HantekDsoControl* dsoControl) const override; + void applyRequirements(DsoCommandQueue* commandQueue) const override; }; ``` @@ -37,7 +40,7 @@ DSOModel(int ID, long vendorID, long productID, long vendorIDnoFirmware, long pr ``` * You need to find out the usb vendor id and product id for your digital oscilloscope after it has received - the firmware (for ``long vendorID``, ``long productID``) and before it has a valid firmware + the firmware (for ``long vendorID``, ``long productID``) and also before it has a valid firmware (for ``long vendorIDnoFirmware``, ``long productIDnoFirmware``). * The firmware token is just the devicename part of the firmware (remember that we used `devicename-firmware.hex` and `devicename-loader.hex`). @@ -56,7 +59,7 @@ DSOModel(int ID, long vendorID, long productID, long vendorIDnoFirmware, long pr specification.samplerate.multi.recordLengths = {UINT_MAX, 20480, 65536}; ``` -4. The actual commands that are send, need to be defined as well, for instance: +4. The command prototypes that are used need to be defined, for instance: ``` c++ specification.command.control.setOffset = CONTROL_SETOFFSET; @@ -69,7 +72,28 @@ DSOModel(int ID, long vendorID, long productID, long vendorIDnoFirmware, long pr specification.command.bulk.setPretrigger = BulkCode::SETTRIGGERANDSAMPLERATE; ``` -5. Add an instance of your class to the cpp file. The `DSOModel` constructor will register +5. You need to register the actual commands to the DsoCommandQueue object like this: + +``` c++ +void ModelDSO2150::applyRequirements(DsoCommandQueue *commandQueue) const { + commandQueue->addCommand(new BulkForceTrigger(), false); + commandQueue->addCommand(new BulkCaptureStart(), false); + commandQueue->addCommand(new BulkTriggerEnabled(), false); + commandQueue->addCommand(new BulkGetData(), false); + commandQueue->addCommand(new BulkGetCaptureState(), false); + commandQueue->addCommand(new BulkSetGain(), false); + + commandQueue->addCommand(new BulkSetTriggerAndSamplerate(), false); + commandQueue->addCommand(new ControlSetOffset(), false); + commandQueue->addCommand(new ControlSetRelays(), false); +} +``` + +If you define a command prototype in (4) but do not register the actual command in (5), the application +will crash. Guaranteed! +Always create new heap objects for the command queue. The queue will clean up after itself. + +6. Add an object instance of your class to the cpp file as last line. The `DSOModel` constructor will register your new model automatically to the ModelRegistry: ``` diff --git a/openhantek/CMakeLists.txt b/openhantek/CMakeLists.txt index 17bebdf4..45987fdc 100644 --- a/openhantek/CMakeLists.txt +++ b/openhantek/CMakeLists.txt @@ -1,37 +1,60 @@ project(OpenHantek CXX) -find_package(Qt5Widgets REQUIRED) -find_package(Qt5PrintSupport REQUIRED) -find_package(Qt5OpenGL REQUIRED) -find_package(OpenGL) +find_package(Qt5 REQUIRED COMPONENTS Core Gui Widgets PrintSupport 3DCore 3DExtras 3DRender 3DInput) set(CMAKE_AUTOMOC ON) set(CMAKE_AUTOUIC ON) set(CMAKE_AUTORCC ON) -if (Qt5Widgets_VERSION VERSION_LESS 5.4.0) - message(FATAL_ERROR "Minimum supported Qt5 version is 5.4.0!") +set(CMAKE_CXX_STANDARD 14) +set(CMAKE_CXX_STANDARD_REQUIRED ON) +set(CMAKE_CXX_EXTENSIONS OFF) + +if (Qt5Widgets_VERSION VERSION_LESS 5.8.0) + message(FATAL_ERROR "Minimum supported Qt5 version is 5.8.0!") endif() # include directories set(CMAKE_INCLUDE_CURRENT_DIR ON) -include_directories(src/ src/hantekdso src/widgets src/docks src/configdialog) +include_directories(src/ src/hantekdso src/settings) # src/widgets src/scopeview # collect sources and other files -file(GLOB_RECURSE SRC "src/*.cpp") -file(GLOB_RECURSE HEADERS "src/*.h") +file(GLOB_RECURSE SRC_CORE + "src/hantekdso/*.cpp" "src/hantekprotocol/*.cpp" "src/hantekprotocol/*.cpp" "src/usb/*.cpp" "src/utils/*.cpp" + "src/hantekdso/*.h" "src/hantekprotocol/*.h" "src/hantekprotocol/*.h" "src/usb/*.h" "src/utils/*.h") +file(GLOB_RECURSE SRC_POST "src/post/*.cpp" "src/post/*.h") +file(GLOB_RECURSE SRC_SETTINGS "src/settings/*.cpp" "src/settings/*.h") +file(GLOB_RECURSE SRC_EXPORT "src/exporting/*.cpp" "src/exporting/*.h") +file(GLOB_RECURSE SRC_SCOPEVIEW "src/scopeview/*.cpp" "src/scopeview/*.h") +file(GLOB SRC_UI "src/main.cpp" + "src/widgets/*.cpp" "src/widgets/*.h" + "src/mainwindow.cpp" "src/mainwindow.h" + "src/configdialog/*.cpp" "src/configdialog/*.h" + "src/docks/*.cpp" "src/docks/*.h" + "src/iconfont/*.cpp" "src/iconfont/*.h" + "src/selectdevice/*.cpp" "src/selectdevice/*.h") +set(SRC ${SRC_CORE} ${SRC_POST} ${SRC_SETTINGS} ${SRC_EXPORT} ${SRC_SCOPEVIEW} ${SRC_UI}) + file(GLOB_RECURSE UI "src/*.ui") file(GLOB_RECURSE QRC "res/*.qrc") add_custom_target(format SOURCES ".clang-format" - COMMAND "clang-format" "-style=file" "-i" "-sort-includes" ${SRC} ${HEADERS}) + COMMAND "clang-format" "-style=file" "-i" "-sort-includes" ${SRC}) add_subdirectory(translations) add_definitions(-DVERSION="${CPACK_PACKAGE_VERSION}") # make executable -add_executable(${PROJECT_NAME} ${SRC} ${HEADERS} ${UI} ${QRC} ${TRANSLATION_BIN_FILES} ${TRANSLATION_QRC}) -target_link_libraries(${PROJECT_NAME} Qt5::Widgets Qt5::PrintSupport Qt5::OpenGL ${OPENGL_LIBRARIES} ) +add_executable(${PROJECT_NAME} ${SRC} ${UI} ${QRC} ${TRANSLATION_BIN_FILES} ${TRANSLATION_QRC}) +target_link_libraries(${PROJECT_NAME} + Qt5::Core + Qt5::Gui + Qt5::Widgets + Qt5::3DCore + Qt5::3DExtras + Qt5::3DRender + Qt5::3DInput + Qt5::PrintSupport) target_compile_features(${PROJECT_NAME} PRIVATE cxx_range_for) if(MSVC) target_compile_options(${PROJECT_NAME} PRIVATE "/W4" "/wd4251" "/wd4127" "/wd4275" "/wd4200" "/nologo" "/J" "/Zi") diff --git a/openhantek/src/configdialog/DsoConfigAnalysisPage.cpp b/openhantek/src/configdialog/DsoConfigAnalysisPage.cpp index 5c02fc23..a9497968 100644 --- a/openhantek/src/configdialog/DsoConfigAnalysisPage.cpp +++ b/openhantek/src/configdialog/DsoConfigAnalysisPage.cpp @@ -2,7 +2,7 @@ #include "DsoConfigAnalysisPage.h" -DsoConfigAnalysisPage::DsoConfigAnalysisPage(DsoSettings *settings, QWidget *parent) +DsoConfigAnalysisPage::DsoConfigAnalysisPage(Settings::DsoSettings *settings, QWidget *parent) : QWidget(parent), settings(settings) { // Initialize lists for comboboxes QStringList windowFunctionStrings; @@ -15,14 +15,14 @@ DsoConfigAnalysisPage::DsoConfigAnalysisPage(DsoSettings *settings, QWidget *par windowFunctionLabel = new QLabel(tr("Window function")); windowFunctionComboBox = new QComboBox(); windowFunctionComboBox->addItems(windowFunctionStrings); - windowFunctionComboBox->setCurrentIndex((int)settings->post.spectrumWindow); + windowFunctionComboBox->setCurrentIndex((int)settings->post.spectrumWindow()); referenceLevelLabel = new QLabel(tr("Reference level")); referenceLevelSpinBox = new QDoubleSpinBox(); referenceLevelSpinBox->setDecimals(1); referenceLevelSpinBox->setMinimum(-40.0); referenceLevelSpinBox->setMaximum(100.0); - referenceLevelSpinBox->setValue(settings->post.spectrumReference); + referenceLevelSpinBox->setValue(settings->post.spectrumReference()); referenceLevelUnitLabel = new QLabel(tr("dBm")); referenceLevelLayout = new QHBoxLayout(); referenceLevelLayout->addWidget(referenceLevelSpinBox); @@ -33,7 +33,7 @@ DsoConfigAnalysisPage::DsoConfigAnalysisPage(DsoSettings *settings, QWidget *par minimumMagnitudeSpinBox->setDecimals(1); minimumMagnitudeSpinBox->setMinimum(-40.0); minimumMagnitudeSpinBox->setMaximum(100.0); - minimumMagnitudeSpinBox->setValue(settings->post.spectrumLimit); + minimumMagnitudeSpinBox->setValue(settings->post.spectrumLimit()); minimumMagnitudeUnitLabel = new QLabel(tr("dBm")); minimumMagnitudeLayout = new QHBoxLayout(); minimumMagnitudeLayout->addWidget(minimumMagnitudeSpinBox); @@ -59,7 +59,7 @@ DsoConfigAnalysisPage::DsoConfigAnalysisPage(DsoSettings *settings, QWidget *par /// \brief Saves the new settings. void DsoConfigAnalysisPage::saveSettings() { - settings->post.spectrumWindow = (Dso::WindowFunction)windowFunctionComboBox->currentIndex(); - settings->post.spectrumReference = referenceLevelSpinBox->value(); - settings->post.spectrumLimit = minimumMagnitudeSpinBox->value(); + settings->post.m_spectrumWindow = (PostProcessing::WindowFunction)windowFunctionComboBox->currentIndex(); + settings->post.m_spectrumReference = referenceLevelSpinBox->value(); + settings->post.m_spectrumLimit = minimumMagnitudeSpinBox->value(); } diff --git a/openhantek/src/configdialog/DsoConfigAnalysisPage.h b/openhantek/src/configdialog/DsoConfigAnalysisPage.h index aa408b51..7862c202 100644 --- a/openhantek/src/configdialog/DsoConfigAnalysisPage.h +++ b/openhantek/src/configdialog/DsoConfigAnalysisPage.h @@ -14,20 +14,18 @@ #include #include -//////////////////////////////////////////////////////////////////////////////// -/// \class DsoConfigAnalysisPage configpages.h /// \brief Config page for the data analysis. class DsoConfigAnalysisPage : public QWidget { Q_OBJECT public: - DsoConfigAnalysisPage(DsoSettings *settings, QWidget *parent = 0); + DsoConfigAnalysisPage(Settings::DsoSettings *settings, QWidget *parent = 0); public slots: void saveSettings(); private: - DsoSettings *settings; + Settings::DsoSettings *settings; QVBoxLayout *mainLayout; diff --git a/openhantek/src/configdialog/DsoConfigColorsPage.cpp b/openhantek/src/configdialog/DsoConfigColorsPage.cpp index 85e96e7c..67fdde90 100644 --- a/openhantek/src/configdialog/DsoConfigColorsPage.cpp +++ b/openhantek/src/configdialog/DsoConfigColorsPage.cpp @@ -1,12 +1,24 @@ // SPDX-License-Identifier: GPL-2.0+ #include "DsoConfigColorsPage.h" +#include "widgets/colorbox.h" -DsoConfigColorsPage::DsoConfigColorsPage(DsoSettings *settings, QWidget *parent) : QWidget(parent), settings(settings) { +DsoConfigColorsPage::DsoConfigColorsPage(Settings::DsoSettings *settings, QWidget *parent) + : QWidget(parent), settings(settings) { // Initialize elements - DsoSettingsView &colorSettings = settings->view; + Settings::View &colorSettings = settings->view; enum { COL_LABEL = 0, COL_SCR_CHANNEL, COL_SCR_SPECTRUM, COL_PRT_CHANNEL, COL_PRT_SPECTRUM }; + QVBoxLayout *mainLayout; + + QGroupBox *colorsGroup; + QGridLayout *colorsLayout; + + QLabel *screenColorsLabel, *printColorsLabel; + QLabel *axesLabel, *backgroundLabel, *borderLabel, *gridLabel, *markersLabel, *textLabel; + QLabel *graphLabel; + QLabel *screenChannelLabel, *screenSpectrumLabel, *printChannelLabel, *printSpectrumLabel; + // Plot Area graphLabel = new QLabel(tr("
")); // 4*80 graphLabel->setAlignment(Qt::AlignRight); @@ -18,28 +30,28 @@ DsoConfigColorsPage::DsoConfigColorsPage(DsoSettings *settings, QWidget *parent) printColorsLabel->setAlignment(Qt::AlignHCenter); axesLabel = new QLabel(tr("Axes")); - axesColorBox = new ColorBox(colorSettings.screen.axes); - printAxesColorBox = new ColorBox(colorSettings.print.axes); + axesColorBox = new ColorBox(colorSettings.screen.axes()); + printAxesColorBox = new ColorBox(colorSettings.print.axes()); backgroundLabel = new QLabel(tr("Background")); - backgroundColorBox = new ColorBox(colorSettings.screen.background); - printBackgroundColorBox = new ColorBox(colorSettings.print.background); + backgroundColorBox = new ColorBox(colorSettings.screen.background()); + printBackgroundColorBox = new ColorBox(colorSettings.print.background()); borderLabel = new QLabel(tr("Border")); - borderColorBox = new ColorBox(colorSettings.screen.border); - printBorderColorBox = new ColorBox(colorSettings.print.border); + borderColorBox = new ColorBox(colorSettings.screen.border()); + printBorderColorBox = new ColorBox(colorSettings.print.border()); gridLabel = new QLabel(tr("Grid")); - gridColorBox = new ColorBox(colorSettings.screen.grid); - printGridColorBox = new ColorBox(colorSettings.print.grid); + gridColorBox = new ColorBox(colorSettings.screen.grid()); + printGridColorBox = new ColorBox(colorSettings.print.grid()); markersLabel = new QLabel(tr("Markers")); - markersColorBox = new ColorBox(colorSettings.screen.markers); - printMarkersColorBox = new ColorBox(colorSettings.print.markers); + markersColorBox = new ColorBox(colorSettings.screen.markers()); + printMarkersColorBox = new ColorBox(colorSettings.print.markers()); textLabel = new QLabel(tr("Text")); - textColorBox = new ColorBox(colorSettings.screen.text); - printTextColorBox = new ColorBox(colorSettings.print.text); + textColorBox = new ColorBox(colorSettings.screen.text()); + printTextColorBox = new ColorBox(colorSettings.print.text()); // Graph category screenChannelLabel = new QLabel(tr("Channel")); @@ -51,14 +63,6 @@ DsoConfigColorsPage::DsoConfigColorsPage(DsoSettings *settings, QWidget *parent) printSpectrumLabel = new QLabel(tr("Spectrum")); printSpectrumLabel->setAlignment(Qt::AlignHCenter); - for (ChannelID channel = 0; channel < settings->scope.voltage.size(); ++channel) { - colorLabel.push_back(new QLabel(settings->scope.voltage[channel].name)); - screenChannelColorBox.push_back(new ColorBox(colorSettings.screen.voltage[channel])); - screenSpectrumColorBox.push_back(new ColorBox(colorSettings.screen.spectrum[channel])); - printChannelColorBox.push_back(new ColorBox(colorSettings.print.voltage[channel])); - printSpectrumColorBox.push_back(new ColorBox(colorSettings.print.spectrum[channel])); - } - // Plot Area Layout colorsLayout = new QGridLayout(); colorsLayout->setColumnStretch(COL_LABEL, 1); @@ -106,12 +110,21 @@ DsoConfigColorsPage::DsoConfigColorsPage(DsoSettings *settings, QWidget *parent) colorsLayout->addWidget(printSpectrumLabel, row, COL_PRT_SPECTRUM); ++row; - for (ChannelID channel = 0; channel < settings->scope.voltage.size(); ++channel, ++row) { - colorsLayout->addWidget(colorLabel[channel], row, COL_LABEL); - colorsLayout->addWidget(screenChannelColorBox[channel], row, COL_SCR_CHANNEL); - colorsLayout->addWidget(screenSpectrumColorBox[channel], row, COL_SCR_SPECTRUM); - colorsLayout->addWidget(printChannelColorBox[channel], row, COL_PRT_CHANNEL); - colorsLayout->addWidget(printSpectrumColorBox[channel], row, COL_PRT_SPECTRUM); + for (auto *channelSettings : settings->scope) { + ChannelColors *cc = new ChannelColors(this); + QLabel *colorLabel = new QLabel(channelSettings->name()); + cc->screenChannelColorBox = new ColorBox(colorSettings.screen.voltage(channelSettings->channelID())); + cc->screenSpectrumColorBox = new ColorBox(colorSettings.screen.spectrum(channelSettings->channelID())); + cc->printChannelColorBox = new ColorBox(colorSettings.print.voltage(channelSettings->channelID())); + cc->printSpectrumColorBox = new ColorBox(colorSettings.print.spectrum(channelSettings->channelID())); + m_channelColorMap.insert(std::make_pair(channelSettings->channelID(), cc)); + + colorsLayout->addWidget(colorLabel, row, COL_LABEL); + colorsLayout->addWidget(cc->screenChannelColorBox, row, COL_SCR_CHANNEL); + colorsLayout->addWidget(cc->screenSpectrumColorBox, row, COL_SCR_SPECTRUM); + colorsLayout->addWidget(cc->printChannelColorBox, row, COL_PRT_CHANNEL); + colorsLayout->addWidget(cc->printSpectrumColorBox, row, COL_PRT_SPECTRUM); + ++row; } colorsGroup = new QGroupBox(tr("Screen and Print Colors")); @@ -127,29 +140,34 @@ DsoConfigColorsPage::DsoConfigColorsPage(DsoSettings *settings, QWidget *parent) /// \brief Saves the new settings. void DsoConfigColorsPage::saveSettings() { - DsoSettingsView &colorSettings = settings->view; + Settings::View &colorSettings = settings->view; // Screen category - colorSettings.screen.axes = axesColorBox->getColor(); - colorSettings.screen.background = backgroundColorBox->getColor(); - colorSettings.screen.border = borderColorBox->getColor(); - colorSettings.screen.grid = gridColorBox->getColor(); - colorSettings.screen.markers = markersColorBox->getColor(); - colorSettings.screen.text = textColorBox->getColor(); + colorSettings.screen._axes = axesColorBox->getColor(); + colorSettings.screen._background = backgroundColorBox->getColor(); + colorSettings.screen._border = borderColorBox->getColor(); + colorSettings.screen._grid = gridColorBox->getColor(); + colorSettings.screen._markers = markersColorBox->getColor(); + colorSettings.screen._text = textColorBox->getColor(); // Print category - colorSettings.print.axes = printAxesColorBox->getColor(); - colorSettings.print.background = printBackgroundColorBox->getColor(); - colorSettings.print.border = printBorderColorBox->getColor(); - colorSettings.print.grid = printGridColorBox->getColor(); - colorSettings.print.markers = printMarkersColorBox->getColor(); - colorSettings.print.text = printTextColorBox->getColor(); + colorSettings.print._axes = printAxesColorBox->getColor(); + colorSettings.print._background = printBackgroundColorBox->getColor(); + colorSettings.print._border = printBorderColorBox->getColor(); + colorSettings.print._grid = printGridColorBox->getColor(); + colorSettings.print._markers = printMarkersColorBox->getColor(); + colorSettings.print._text = printTextColorBox->getColor(); // Graph category - for (ChannelID channel = 0; channel < settings->scope.voltage.size(); ++channel) { - colorSettings.screen.voltage[channel] = screenChannelColorBox[channel]->getColor(); - colorSettings.screen.spectrum[channel] = screenSpectrumColorBox[channel]->getColor(); - colorSettings.print.voltage[channel] = printChannelColorBox[channel]->getColor(); - colorSettings.print.spectrum[channel] = printSpectrumColorBox[channel]->getColor(); + for (auto &c : m_channelColorMap) { + colorSettings.screen.setVoltage(c.first, c.second->screenChannelColorBox->getColor()); + colorSettings.screen.setSpectrum(c.first, c.second->screenSpectrumColorBox->getColor()); + colorSettings.print.setVoltage(c.first, c.second->printChannelColorBox->getColor()); + colorSettings.print.setSpectrum(c.first, c.second->printSpectrumColorBox->getColor()); } + + colorSettings.screen.observer()->update(); + colorSettings.print.observer()->update(); } + +ChannelColors::ChannelColors(QObject *parent) : QObject(parent) {} diff --git a/openhantek/src/configdialog/DsoConfigColorsPage.h b/openhantek/src/configdialog/DsoConfigColorsPage.h index 00075428..27ab6c49 100644 --- a/openhantek/src/configdialog/DsoConfigColorsPage.h +++ b/openhantek/src/configdialog/DsoConfigColorsPage.h @@ -14,41 +14,36 @@ #include #include -#include "colorbox.h" +class ColorBox; + +struct ChannelColors : protected QObject { + ChannelColors(QObject *parent); + + ColorBox *screenChannelColorBox; + ColorBox *screenSpectrumColorBox; + ColorBox *printChannelColorBox; + ColorBox *printSpectrumColorBox; + + private: + Q_OBJECT +}; -//////////////////////////////////////////////////////////////////////////////// -/// \class DsoConfigColorsPage configpages.h /// \brief Config page for the colors. class DsoConfigColorsPage : public QWidget { Q_OBJECT public: - DsoConfigColorsPage(DsoSettings *settings, QWidget *parent = 0); + DsoConfigColorsPage(Settings::DsoSettings *settings, QWidget *parent = 0); public slots: void saveSettings(); private: - DsoSettings *settings; - - QVBoxLayout *mainLayout; - - QGroupBox *colorsGroup; - QGridLayout *colorsLayout; - - QLabel *screenColorsLabel, *printColorsLabel; - QLabel *axesLabel, *backgroundLabel, *borderLabel, *gridLabel, *markersLabel, *textLabel; - ColorBox *axesColorBox, *backgroundColorBox, *borderColorBox, *gridColorBox, *markersColorBox, *textColorBox; - - ColorBox *printAxesColorBox, *printBackgroundColorBox, *printBorderColorBox, *printGridColorBox, - *printMarkersColorBox, *printTextColorBox; + Settings::DsoSettings *settings; - QLabel *graphLabel; + ColorBox *axesColorBox, *backgroundColorBox, *borderColorBox, *gridColorBox, *markersColorBox, *textColorBox, + *printAxesColorBox, *printBackgroundColorBox, *printBorderColorBox, *printGridColorBox, *printMarkersColorBox, + *printTextColorBox; - QLabel *screenChannelLabel, *screenSpectrumLabel, *printChannelLabel, *printSpectrumLabel; - std::vector colorLabel; - std::vector screenChannelColorBox; - std::vector screenSpectrumColorBox; - std::vector printChannelColorBox; - std::vector printSpectrumColorBox; + std::map m_channelColorMap; }; diff --git a/openhantek/src/configdialog/DsoConfigFilesPage.cpp b/openhantek/src/configdialog/DsoConfigFilesPage.cpp index 64e3b2ab..b902b7e9 100644 --- a/openhantek/src/configdialog/DsoConfigFilesPage.cpp +++ b/openhantek/src/configdialog/DsoConfigFilesPage.cpp @@ -2,7 +2,7 @@ #include "DsoConfigFilesPage.h" -DsoConfigFilesPage::DsoConfigFilesPage(DsoSettings *settings, QWidget *parent) : QWidget(parent), settings(settings) { +DsoConfigFilesPage::DsoConfigFilesPage(Settings::DsoSettings *settings, QWidget *parent) : QWidget(parent), settings(settings) { // Export group screenColorCheckBox = new QCheckBox(tr("Export Images with Screen Colors")); screenColorCheckBox->setChecked(settings->view.screenColorImages); diff --git a/openhantek/src/configdialog/DsoConfigFilesPage.h b/openhantek/src/configdialog/DsoConfigFilesPage.h index 8bcfd719..8a9990bc 100644 --- a/openhantek/src/configdialog/DsoConfigFilesPage.h +++ b/openhantek/src/configdialog/DsoConfigFilesPage.h @@ -15,20 +15,18 @@ #include #include -//////////////////////////////////////////////////////////////////////////////// -/// \class DsoConfigFilesPage configpages.h /// \brief Config page for file loading/saving. class DsoConfigFilesPage : public QWidget { Q_OBJECT public: - DsoConfigFilesPage(DsoSettings *settings, QWidget *parent = 0); + DsoConfigFilesPage(Settings::DsoSettings *settings, QWidget *parent = 0); public slots: void saveSettings(); private: - DsoSettings *settings; + Settings::DsoSettings *settings; QVBoxLayout *mainLayout; diff --git a/openhantek/src/configdialog/DsoConfigScopePage.cpp b/openhantek/src/configdialog/DsoConfigScopePage.cpp index 2c7fdcfa..3938d463 100644 --- a/openhantek/src/configdialog/DsoConfigScopePage.cpp +++ b/openhantek/src/configdialog/DsoConfigScopePage.cpp @@ -2,7 +2,8 @@ #include "DsoConfigScopePage.h" -DsoConfigScopePage::DsoConfigScopePage(DsoSettings *settings, QWidget *parent) : QWidget(parent), settings(settings) { +DsoConfigScopePage::DsoConfigScopePage(Settings::DsoSettings *settings, QWidget *parent) + : QWidget(parent), settings(settings) { // Initialize lists for comboboxes QStringList interpolationStrings; interpolationStrings << tr("Off") << tr("Linear"); @@ -11,12 +12,12 @@ DsoConfigScopePage::DsoConfigScopePage(DsoSettings *settings, QWidget *parent) : interpolationLabel = new QLabel(tr("Interpolation")); interpolationComboBox = new QComboBox(); interpolationComboBox->addItems(interpolationStrings); - interpolationComboBox->setCurrentIndex(settings->view.interpolation); + interpolationComboBox->setCurrentIndex((unsigned)settings->view.interpolation()); digitalPhosphorDepthLabel = new QLabel(tr("Digital phosphor depth")); digitalPhosphorDepthSpinBox = new QSpinBox(); digitalPhosphorDepthSpinBox->setMinimum(2); digitalPhosphorDepthSpinBox->setMaximum(99); - digitalPhosphorDepthSpinBox->setValue(settings->view.digitalPhosphorDepth); + digitalPhosphorDepthSpinBox->setValue(settings->view.digitalPhosphor()); graphLayout = new QGridLayout(); graphLayout->addWidget(interpolationLabel, 1, 0); @@ -36,6 +37,6 @@ DsoConfigScopePage::DsoConfigScopePage(DsoSettings *settings, QWidget *parent) : /// \brief Saves the new settings. void DsoConfigScopePage::saveSettings() { - settings->view.interpolation = (Dso::InterpolationMode)interpolationComboBox->currentIndex(); - settings->view.digitalPhosphorDepth = digitalPhosphorDepthSpinBox->value(); + settings->view.setInterpolation((Dso::InterpolationMode)interpolationComboBox->currentIndex()); + settings->view.setDigitalPhosphor(settings->view.digitalPhosphor(), (unsigned)digitalPhosphorDepthSpinBox->value()); } diff --git a/openhantek/src/configdialog/DsoConfigScopePage.h b/openhantek/src/configdialog/DsoConfigScopePage.h index 77f33bd4..7d89fa59 100644 --- a/openhantek/src/configdialog/DsoConfigScopePage.h +++ b/openhantek/src/configdialog/DsoConfigScopePage.h @@ -14,20 +14,18 @@ #include #include -//////////////////////////////////////////////////////////////////////////////// -/// \class DsoConfigScopePage configpages.h /// \brief Config page for the scope screen. class DsoConfigScopePage : public QWidget { Q_OBJECT public: - DsoConfigScopePage(DsoSettings *settings, QWidget *parent = 0); + DsoConfigScopePage(Settings::DsoSettings *settings, QWidget *parent = 0); public slots: void saveSettings(); private: - DsoSettings *settings; + Settings::DsoSettings *settings; QVBoxLayout *mainLayout; diff --git a/openhantek/src/configdialog/configdialog.cpp b/openhantek/src/configdialog/configdialog.cpp index eabda2f2..02e9278a 100644 --- a/openhantek/src/configdialog/configdialog.cpp +++ b/openhantek/src/configdialog/configdialog.cpp @@ -1,7 +1,7 @@ // SPDX-License-Identifier: GPL-2.0+ /*#if defined(OS_UNIX) -#define CONFIG_PATH QDir::homePath() + "/.config/paranoiacs.net/openhantek" +#define CONFIG_PATH QDir::homePath() + "/.config/openhantek/openhantek" #define CONFIG_FILE CONFIG_PATH "/openhantek.conf" #elif defined(OS_DARWIN) #define CONFIG_PATH QDir::homePath() + "/Library/Application Support/OpenHantek" @@ -9,7 +9,7 @@ #elif defined(OS_WINDOWS) //#define CONFIG_PATH QDir::homePath() + "" // Too hard to get and this OS sucks anyway, ignore it -#define CONFIG_FILE "HKEY_CURRENT_USER\\Software\\paranoiacs.net\\OpenHantek" +#define CONFIG_FILE "HKEY_CURRENT_USER\\Software\\OpenHantek\\OpenHantek" #endif*/ #define CONFIG_LIST_WIDTH 128 ///< The width of the page selection widget @@ -39,7 +39,7 @@ anyway, ignore it /// \param settings The target settings object. /// \param parent The parent widget. /// \param flags Flags for the window manager. -DsoConfigDialog::DsoConfigDialog(DsoSettings *settings, QWidget *parent, Qt::WindowFlags flags) +DsoConfigDialog::DsoConfigDialog(Settings::DsoSettings *settings, QWidget *parent, Qt::WindowFlags flags) : QDialog(parent, flags), settings(settings) { this->setWindowTitle(tr("Settings")); diff --git a/openhantek/src/configdialog/configdialog.h b/openhantek/src/configdialog/configdialog.h index f6b577fe..7db11f15 100644 --- a/openhantek/src/configdialog/configdialog.h +++ b/openhantek/src/configdialog/configdialog.h @@ -6,8 +6,9 @@ class DsoConfigAnalysisPage; class DsoConfigColorsPage; class DsoConfigFilesPage; class DsoConfigScopePage; +namespace Settings { class DsoSettings; - +} class QHBoxLayout; class QListWidget; class QListWidgetItem; @@ -22,7 +23,7 @@ class DsoConfigDialog : public QDialog { Q_OBJECT public: - DsoConfigDialog(DsoSettings *settings, QWidget *parent = 0, Qt::WindowFlags flags = 0); + DsoConfigDialog(Settings::DsoSettings *settings, QWidget *parent = 0, Qt::WindowFlags flags = 0); ~DsoConfigDialog(); public slots: @@ -34,7 +35,7 @@ class DsoConfigDialog : public QDialog { private: void createIcons(); - DsoSettings *settings; + Settings::DsoSettings *settings; QVBoxLayout *mainLayout; QHBoxLayout *horizontalLayout; diff --git a/openhantek/src/docks/DebugDock.cpp b/openhantek/src/docks/DebugDock.cpp new file mode 100644 index 00000000..c02e1a60 --- /dev/null +++ b/openhantek/src/docks/DebugDock.cpp @@ -0,0 +1,156 @@ +// SPDX-License-Identifier: GPL-2.0+ + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include + +#include "DebugDock.h" +#include "dockwindows.h" +#include "hantekdso/devicesettings.h" +#include "hantekdso/enums.h" +#include "hantekdso/dsocontrol.h" +#include "hantekdso/modelspecification.h" +#include "hantekprotocol/codes.h" +#include "iconfont/QtAwesome.h" +#include "scopesettings.h" +#include "utils/debugnotify.h" +#include "utils/enumhelper.h" +#include "utils/printutils.h" +#include "widgets/sispinbox.h" + +#include +#include +#include +#include +#include + +Q_DECLARE_METATYPE(std::vector) +Q_DECLARE_METATYPE(std::vector) + +template struct SELECT { + template static constexpr auto OVERLOAD_OF(R (C::*pmf)(Args...)) -> decltype(pmf) { + return pmf; + } +}; + +using namespace Debug; + +DebugDock::DebugDock(DsoControl *dsocontrol, QWidget *parent, Qt::WindowFlags flags) + : QDockWidget(tr("Debug"), parent, flags) { + + QWidget *dockWidget = new QWidget(this); + QVBoxLayout *dockLayout = new QVBoxLayout; + + QHBoxLayout *manualCommandLayout = new QHBoxLayout; + QHBoxLayout *manualCommandLayout2 = new QHBoxLayout; + QComboBox *manualCommandType = new QComboBox(this); + QComboBox *controlCodes = new QComboBox(this); + QComboBox *bulkCodes = new QComboBox(this); + QLineEdit *commandEdit = new QLineEdit(this); + QPushButton *actionManualCommand = new QPushButton; + + commandEdit->setPlaceholderText(tr("0a ca (hex values)")); + + actionManualCommand->setIcon(iconFont->icon(fa::edit)); + manualCommandType->addItems(QStringList() << tr("Control") << tr("Bulk")); + + auto bulkMeta = QMetaEnum::fromType(); + for (int i = 0; i < bulkMeta.keyCount(); ++i) { + int v = bulkMeta.value(i); + if (dsocontrol->isCommandSupported((Hantek::BulkCode)v)) bulkCodes->addItem(bulkMeta.key(i), v); + } + bulkCodes->hide(); + + auto controlMeta = QMetaEnum::fromType(); + for (int i = 0; i < controlMeta.keyCount(); ++i) { + int v = controlMeta.value(i); + if (dsocontrol->isCommandSupported((Hantek::ControlCode)v)) controlCodes->addItem(controlMeta.key(i), v); + } + + connect(manualCommandType, SELECT::OVERLOAD_OF(&QComboBox::currentIndexChanged), this, + [controlCodes, bulkCodes](unsigned index) { + controlCodes->setVisible(index == 0); + bulkCodes->setVisible(index == 1); + }); + + manualCommandLayout->addWidget(manualCommandType); + manualCommandLayout->addWidget(controlCodes); + manualCommandLayout->addWidget(bulkCodes); + manualCommandLayout2->addWidget(commandEdit, 1); + manualCommandLayout2->addWidget(actionManualCommand); + + connect(this, &DebugDock::manualCommand, dsocontrol, &DsoControl::manualCommand); + + auto fnManualCommand = [this, commandEdit, manualCommandType, controlCodes, bulkCodes, dsocontrol]() { + if (commandEdit->text().trimmed().isEmpty()) return; + if (manualCommandType->currentIndex() == 1) { + if (bulkCodes->currentIndex() < 0) return; + } else if (controlCodes->currentIndex() < 0) + return; + QByteArray data(100, 0); + data.resize((int)hexParse(commandEdit->text(), (unsigned char *)data.data(), (unsigned)data.size())); + if (data.isEmpty()) return; + + // Use a signal instead of a direct function call to archive thread-safety + emit manualCommand(manualCommandType->currentIndex() == 1, + (Hantek::BulkCode)bulkCodes->currentData(Qt::UserRole).toInt(), + (Hantek::ControlCode)controlCodes->currentData(Qt::UserRole).toInt(), data); + commandEdit->clear(); + }; + + connect(actionManualCommand, &QPushButton::toggled, this, fnManualCommand); + connect(commandEdit, &QLineEdit::returnPressed, this, fnManualCommand); + + QTableView *logTable = new QTableView(this); + logTable->setSelectionMode(QAbstractItemView::SelectionMode::SingleSelection); + logTable->setSelectionBehavior(QAbstractItemView::SelectionBehavior::SelectRows); + logTable->horizontalHeader()->hide(); + logTable->horizontalHeader()->setStretchLastSection(true); + logTable->setColumnWidth(0, 60); + logTable->setColumnWidth(1, 60); + logTable->verticalHeader()->hide(); + m_model = new Debug::LogModel(this); + logTable->setModel(m_model); + connect(dsocontrol, &DsoControl::debugMessage, m_model, &LogModel::addEntry); + + QCheckBox *showLoopLog = new QCheckBox("Verbose loop log", this); + showLoopLog->setChecked(false); + m_model->addToFilter(Debug::NotificationType::DSOLoop); + connect(showLoopLog, &QCheckBox::toggled, this, [this](bool enable) { + if (enable) + m_model->clearFilter(); + else + m_model->addToFilter(Debug::NotificationType::DSOLoop); + }); + + QPushButton *clearLog = new QPushButton; + clearLog->setIcon(iconFont->icon(fa::remove)); + connect(clearLog, &QPushButton::clicked, m_model, &LogModel::removeAll); + + QHBoxLayout *clearLogLayout = new QHBoxLayout; + clearLogLayout->addWidget(new QLabel(tr("Logs"), this), 1); + clearLogLayout->addWidget(clearLog); + + dockLayout->addLayout(clearLogLayout); + dockLayout->addWidget(logTable, 1); + dockLayout->addWidget(showLoopLog); + dockLayout->addWidget(new QLabel(tr("Manual command"), this)); + dockLayout->addLayout(manualCommandLayout); + dockLayout->addLayout(manualCommandLayout2); + SetupDockWidget(this, dockWidget, dockLayout, QSizePolicy::Expanding); +} + +/// \brief Don't close the dock, just hide it. +/// \param event The close event that should be handled. +void DebugDock::closeEvent(QCloseEvent *event) { + this->hide(); + event->accept(); +} diff --git a/openhantek/src/docks/DebugDock.h b/openhantek/src/docks/DebugDock.h new file mode 100644 index 00000000..3855460c --- /dev/null +++ b/openhantek/src/docks/DebugDock.h @@ -0,0 +1,37 @@ +// SPDX-License-Identifier: GPL-2.0+ + +#pragma once + +#include +#include + +#include "hantekprotocol/codes.h" +#include "utils/debugnotify.h" + +namespace Settings { +class Scope; +} +class DsoControl; +namespace Debug { +class LogModel; +} + +/// \brief Dock window with a log view and manual command +/// It contains the settings for the timebase and the display format. +class DebugDock : public QDockWidget { + Q_OBJECT + + public: + /// \brief Initializes the horizontal axis docking window. + /// \param settings The target settings object. + /// \param parent The parent widget. + /// \param flags Flags for the window manager. + DebugDock(DsoControl *dsocontrol, QWidget *parent, Qt::WindowFlags flags = 0); + + protected: + void closeEvent(QCloseEvent *event); + + Debug::LogModel *m_model; + signals: + void manualCommand(bool isBulk, Hantek::BulkCode bulkCode, Hantek::ControlCode controlCode, const QByteArray &data); +}; diff --git a/openhantek/src/docks/HorizontalDock.cpp b/openhantek/src/docks/HorizontalDock.cpp index 81af81eb..180098f9 100644 --- a/openhantek/src/docks/HorizontalDock.cpp +++ b/openhantek/src/docks/HorizontalDock.cpp @@ -3,205 +3,240 @@ #include #include #include +#include #include +#include #include #include -#include +#include #include #include "HorizontalDock.h" #include "dockwindows.h" - +#include "hantekdso/devicesettings.h" +#include "hantekdso/dsocontrol.h" +#include "hantekdso/enums.h" +#include "hantekdso/modelspecification.h" #include "scopesettings.h" -#include "sispinbox.h" +#include "utils/enumhelper.h" #include "utils/printutils.h" +#include "widgets/sispinbox.h" + +Q_DECLARE_METATYPE(std::vector) +Q_DECLARE_METATYPE(std::vector) -template struct SELECT { - template - static constexpr auto OVERLOAD_OF( R (C::*pmf)(Args...) ) -> decltype(pmf) { +template struct SELECT { + template static constexpr auto OVERLOAD_OF(R (C::*pmf)(Args...)) -> decltype(pmf) { return pmf; } }; -HorizontalDock::HorizontalDock(DsoSettingsScope *scope, QWidget *parent, Qt::WindowFlags flags) - : QDockWidget(tr("Horizontal"), parent, flags), scope(scope) { - - // Initialize elements - this->samplerateLabel = new QLabel(tr("Samplerate")); - this->samplerateSiSpinBox = new SiSpinBox(UNIT_SAMPLES); - this->samplerateSiSpinBox->setMinimum(1); - this->samplerateSiSpinBox->setMaximum(1e8); - this->samplerateSiSpinBox->setUnitPostfix("/s"); - - timebaseSteps << 1.0 << 2.0 << 4.0 << 10.0; - - this->timebaseLabel = new QLabel(tr("Timebase")); - this->timebaseSiSpinBox = new SiSpinBox(UNIT_SECONDS); - this->timebaseSiSpinBox->setSteps(timebaseSteps); - this->timebaseSiSpinBox->setMinimum(1e-9); - this->timebaseSiSpinBox->setMaximum(3.6e3); - - this->frequencybaseLabel = new QLabel(tr("Frequencybase")); - this->frequencybaseSiSpinBox = new SiSpinBox(UNIT_HERTZ); - this->frequencybaseSiSpinBox->setMinimum(1.0); - this->frequencybaseSiSpinBox->setMaximum(100e6); - - this->recordLengthLabel = new QLabel(tr("Record length")); - this->recordLengthComboBox = new QComboBox(); - - this->formatLabel = new QLabel(tr("Format")); - this->formatComboBox = new QComboBox(); - for (Dso::GraphFormat format: Dso::GraphFormatEnum) - this->formatComboBox->addItem(Dso::graphFormatString(format)); - - this->dockLayout = new QGridLayout(); - this->dockLayout->setColumnMinimumWidth(0, 64); - this->dockLayout->setColumnStretch(1, 1); - this->dockLayout->addWidget(this->samplerateLabel, 0, 0); - this->dockLayout->addWidget(this->samplerateSiSpinBox, 0, 1); - this->dockLayout->addWidget(this->timebaseLabel, 1, 0); - this->dockLayout->addWidget(this->timebaseSiSpinBox, 1, 1); - this->dockLayout->addWidget(this->frequencybaseLabel, 2, 0); - this->dockLayout->addWidget(this->frequencybaseSiSpinBox, 2, 1); - this->dockLayout->addWidget(this->recordLengthLabel, 3, 0); - this->dockLayout->addWidget(this->recordLengthComboBox, 3, 1); - this->dockLayout->addWidget(this->formatLabel, 4, 0); - this->dockLayout->addWidget(this->formatComboBox, 4, 1); - - this->dockWidget = new QWidget(); - SetupDockWidget(this, dockWidget, dockLayout); - - // Connect signals and slots - connect(this->samplerateSiSpinBox, SELECT::OVERLOAD_OF(&QDoubleSpinBox::valueChanged), this, &HorizontalDock::samplerateSelected); - connect(this->timebaseSiSpinBox, SELECT::OVERLOAD_OF(&QDoubleSpinBox::valueChanged), this, &HorizontalDock::timebaseSelected); - connect(this->frequencybaseSiSpinBox, SELECT::OVERLOAD_OF(&QDoubleSpinBox::valueChanged), this, &HorizontalDock::frequencybaseSelected); - connect(this->recordLengthComboBox, SELECT::OVERLOAD_OF(&QComboBox::currentIndexChanged), this, &HorizontalDock::recordLengthSelected); - connect(this->formatComboBox, SELECT::OVERLOAD_OF(&QComboBox::currentIndexChanged), this, &HorizontalDock::formatSelected); - - // Set values - this->setSamplerate(scope->horizontal.samplerate); - this->setTimebase(scope->horizontal.timebase); - this->setFrequencybase(scope->horizontal.frequencybase); - // this->setRecordLength(scope->horizontal.recordLength); - this->setFormat(scope->horizontal.format); -} - -/// \brief Don't close the dock, just hide it. -/// \param event The close event that should be handled. -void HorizontalDock::closeEvent(QCloseEvent *event) { - this->hide(); - - event->accept(); -} - -void HorizontalDock::setFrequencybase(double frequencybase) { - QSignalBlocker blocker(frequencybaseSiSpinBox); - frequencybaseSiSpinBox->setValue(frequencybase); -} - -void HorizontalDock::setSamplerate(double samplerate) { - QSignalBlocker blocker(samplerateSiSpinBox); - samplerateSiSpinBox->setValue(samplerate); -} - -double HorizontalDock::setTimebase(double timebase) { - QSignalBlocker blocker(timebaseSiSpinBox); - // timebaseSteps are repeated in each decade - double decade = pow(10, floor(log10(timebase))); - double vNorm = timebase / decade; - for (int i = 0; i < timebaseSteps.size() - 1; ++i) { - if (timebaseSteps.at(i) <= vNorm && vNorm < timebaseSteps.at(i + 1)) { - timebaseSiSpinBox->setValue(decade * timebaseSteps.at(i)); - break; +/// A simple Qt Model with the fixed samplerates as display values +class FixedSamplerateModel : public QAbstractListModel { + Q_OBJECT + public: + FixedSamplerateModel(const std::vector &steps, QObject *parent = nullptr) + : QAbstractListModel(parent), steps(steps) { + for (const Dso::FixedSampleRate &v : steps) { + stepStrings.push_back(valueToString(v.samplerate, Unit::SAMPLES, 3)); } } - return timebaseSiSpinBox->value(); -} - -int addRecordLength(QComboBox *recordLengthComboBox, unsigned recordLength) { - recordLengthComboBox->addItem( - recordLength == UINT_MAX ? QCoreApplication::translate("HorizontalDock","Roll") : valueToString(recordLength, UNIT_SAMPLES, 3), recordLength); - return recordLengthComboBox->count()-1; -} -void HorizontalDock::setRecordLength(unsigned int recordLength) { - QSignalBlocker blocker(recordLengthComboBox); - int index = recordLengthComboBox->findData(recordLength); - scope->horizontal.recordLength = recordLength; + private: + const std::vector steps; + std::vector stepStrings; - if (index == -1) { - index = addRecordLength(recordLengthComboBox, recordLength); - } - recordLengthComboBox->setCurrentIndex(index); -} - -int HorizontalDock::setFormat(Dso::GraphFormat format) { - QSignalBlocker blocker(formatComboBox); - if (format >= Dso::GraphFormat::TY && format <= Dso::GraphFormat::XY) { - formatComboBox->setCurrentIndex(format); - return format; + // QAbstractItemModel interface + public: + virtual int rowCount(const QModelIndex &) const override { return (int)steps.size(); } + virtual QVariant data(const QModelIndex &index, int role) const override { + if (role == Qt::DisplayRole) { return stepStrings[(unsigned)index.row()]; } + return QVariant(); } +}; +#include "HorizontalDock.moc" + +HorizontalDock::HorizontalDock(Settings::Scope *scope, DsoControl *dsocontrol, QWidget *parent, Qt::WindowFlags flags) + : QDockWidget(tr("Horizontal"), parent, flags) { + + QGridLayout *dockLayout; ///< The main layout for the dock window + QWidget *dockWidget; ///< The main widget for the dock window + QLabel *samplerateLabel; ///< The label for the samplerate spinbox + QLabel *timebaseLabel; ///< The label for the timebase spinbox + QLabel *frequencybaseLabel; ///< The label for the frequencybase spinbox + QLabel *recordLengthLabel; ///< The label for the record length combobox + QLabel *formatLabel; ///< The label for the format combobox + SiSpinBox *samplerateSiSpinBox; ///< Selects the samplerate for aquisitions + QComboBox *fixedSamplerateBox; ///< Selects the samplerate for aquisitions (fixed samplerrates) + SiSpinBox *timebaseSiSpinBox; ///< Selects the timebase for voltage graphs + SiSpinBox *frequencybaseSiSpinBox; ///< Selects the frequencybase for spectrum graphs + QComboBox *recordLengthComboBox; ///< Selects the record length for aquisitions + QComboBox *formatComboBox; ///< Selects the way the sampled data is + /// interpreted and shown - return -1; -} + // Initialize elements + samplerateLabel = new QLabel(tr("Samplerate"), this); + samplerateSiSpinBox = new SiSpinBox(Unit::SAMPLES, this); + samplerateSiSpinBox->setRange(0, 0); + samplerateSiSpinBox->setUnitPostfix("/s"); + + fixedSamplerateBox = new QComboBox(this); + + std::vector timebaseSteps = {1.0, 2.0, 4.0, 10.0}; + + timebaseLabel = new QLabel(tr("Timebase")); + timebaseSiSpinBox = new SiSpinBox(Unit::SECONDS, this); + timebaseSiSpinBox->setSteps(timebaseSteps); + + frequencybaseLabel = new QLabel(tr("Frequencybase")); + frequencybaseSiSpinBox = new SiSpinBox(Unit::HERTZ, this); + frequencybaseSiSpinBox->setMinimum(1.0); + frequencybaseSiSpinBox->setMaximum(100e6); + frequencybaseSiSpinBox->setToolTip( + tr("From %1 to %2") + .arg(frequencybaseSiSpinBox->textFromValue(frequencybaseSiSpinBox->minimum())) + .arg(frequencybaseSiSpinBox->textFromValue(frequencybaseSiSpinBox->maximum()))); + + recordLengthLabel = new QLabel(tr("Record length"), this); + recordLengthComboBox = new QComboBox(this); + + formatLabel = new QLabel(tr("Format"), this); + formatComboBox = new QComboBox(this); + for (Dso::GraphFormat format : Enum()) formatComboBox->addItem(Dso::graphFormatString(format)); + + dockLayout = new QGridLayout; + dockLayout->setColumnMinimumWidth(0, 64); + dockLayout->setColumnStretch(1, 1); + dockLayout->addWidget(samplerateLabel, 0, 0); + dockLayout->addWidget(samplerateSiSpinBox, 0, 1); + dockLayout->addWidget(fixedSamplerateBox, 0, 1); + dockLayout->addWidget(timebaseLabel, 1, 0); + dockLayout->addWidget(timebaseSiSpinBox, 1, 1); + dockLayout->addWidget(frequencybaseLabel, 2, 0); + dockLayout->addWidget(frequencybaseSiSpinBox, 2, 1); + dockLayout->addWidget(recordLengthLabel, 3, 0); + dockLayout->addWidget(recordLengthComboBox, 3, 1); + dockLayout->addWidget(formatLabel, 4, 0); + dockLayout->addWidget(formatComboBox, 4, 1); + + dockWidget = new QWidget(this); + SetupDockWidget(this, dockWidget, dockLayout); -void HorizontalDock::setAvailableRecordLengths(const std::vector &recordLengths) { - QSignalBlocker blocker(recordLengthComboBox); + auto deviceSettings = dsocontrol->deviceSettings().get(); - recordLengthComboBox->clear(); - for (auto recordLength : recordLengths) { - addRecordLength(recordLengthComboBox, recordLength); + // Set values + if (dsocontrol->specification()->isFixedSamplerateDevice) { + samplerateSiSpinBox->setVisible(false); + fixedSamplerateBox->setVisible(true); + fixedSamplerateBox->setModel(new FixedSamplerateModel(dsocontrol->specification()->fixedSampleRates, this)); + fixedSamplerateBox->setCurrentIndex((int)deviceSettings->samplerate().fixedSamperateId); + } else { + samplerateSiSpinBox->setVisible(true); + fixedSamplerateBox->setVisible(false); + samplerateSiSpinBox->setMinimum(dsocontrol->minSamplerate()); + samplerateSiSpinBox->setMaximum(dsocontrol->maxSamplerate()); + samplerateSiSpinBox->setValue(deviceSettings->samplerate().samplerate); } + formatComboBox->setCurrentIndex((int)scope->format()); + frequencybaseSiSpinBox->setValue(scope->frequencybase()); + timebaseSiSpinBox->setValue(deviceSettings->samplerate().timebase); // /DIVS_TIME + for (auto &entry : deviceSettings->limits->recordLengths) { + recordLengthComboBox->addItem( + entry.recordLength == UINT_MAX ? tr("Roll") : valueToString(entry.recordLength, Unit::SAMPLES, 3), + entry.recordLength); + } + recordLengthComboBox->setCurrentIndex((int)deviceSettings->recordLengthId()); - setRecordLength(scope->horizontal.recordLength); -} - -void HorizontalDock::setSamplerateLimits(double minimum, double maximum) { - QSignalBlocker blocker(samplerateSiSpinBox); - this->samplerateSiSpinBox->setMinimum(minimum); - this->samplerateSiSpinBox->setMaximum(maximum); -} - -void HorizontalDock::setSamplerateSteps(int mode, QList steps) { - QSignalBlocker blocker(samplerateSiSpinBox); - this->samplerateSiSpinBox->setMode(mode); - this->samplerateSiSpinBox->setSteps(steps); -} - -/// \brief Called when the frequencybase spinbox changes its value. -/// \param frequencybase The frequencybase in hertz. -void HorizontalDock::frequencybaseSelected(double frequencybase) { - scope->horizontal.frequencybase = frequencybase; - emit frequencybaseChanged(frequencybase); -} - -/// \brief Called when the samplerate spinbox changes its value. -/// \param samplerate The samplerate in samples/second. -void HorizontalDock::samplerateSelected(double samplerate) { - scope->horizontal.samplerate = samplerate; - scope->horizontal.samplerateSource = DsoSettingsScopeHorizontal::Samplerrate; - emit samplerateChanged(samplerate); -} - -/// \brief Called when the timebase spinbox changes its value. -/// \param timebase The timebase in seconds. -void HorizontalDock::timebaseSelected(double timebase) { - scope->horizontal.timebase = timebase; - scope->horizontal.samplerateSource = DsoSettingsScopeHorizontal::Duration; - emit timebaseChanged(timebase); -} - -/// \brief Called when the record length combo box changes its value. -/// \param index The index of the combo box item. -void HorizontalDock::recordLengthSelected(int index) { - scope->horizontal.recordLength = this->recordLengthComboBox->itemData(index).toUInt(); - emit recordLengthChanged(index); + // Connect signals and slots + connect(samplerateSiSpinBox, SELECT::OVERLOAD_OF(&QDoubleSpinBox::valueChanged), this, + [this, dsocontrol](double samplerate) { dsocontrol->setSamplerate(samplerate); }); + connect(fixedSamplerateBox, SELECT::OVERLOAD_OF(&QComboBox::currentIndexChanged), this, + [this, dsocontrol](int index) { dsocontrol->setFixedSamplerate((unsigned)index); }); + connect(timebaseSiSpinBox, SELECT::OVERLOAD_OF(&QDoubleSpinBox::valueChanged), this, + [this, dsocontrol, timebaseSiSpinBox, deviceSettings](double recordTime) { + QSignalBlocker timebaseBlocker(timebaseSiSpinBox); + dsocontrol->setRecordTime(recordTime); /* *DIVS_TIME */ + }); + connect(frequencybaseSiSpinBox, SELECT::OVERLOAD_OF(&QDoubleSpinBox::valueChanged), this, + [this, scope](double frequencybase) { scope->setFrequencybase(frequencybase); }); + connect(recordLengthComboBox, SELECT::OVERLOAD_OF(&QComboBox::currentIndexChanged), this, + [this, dsocontrol](int index) { dsocontrol->setRecordLengthByIndex(index); }); + connect(formatComboBox, SELECT::OVERLOAD_OF(&QComboBox::currentIndexChanged), this, + [this, scope](int index) { scope->setFormat((Dso::GraphFormat)index); }); + + connect(deviceSettings, &Dso::DeviceSettings::samplerateLimitsChanged, this, + [samplerateSiSpinBox, fixedSamplerateBox, timebaseSiSpinBox](double minimum, double maximum) { + QSignalBlocker blocker(samplerateSiSpinBox); + QSignalBlocker blocker2(fixedSamplerateBox); + QSignalBlocker timebaseBlocker(timebaseSiSpinBox); + samplerateSiSpinBox->setVisible(true); + fixedSamplerateBox->setVisible(false); + samplerateSiSpinBox->setMinimum(minimum); + samplerateSiSpinBox->setMaximum(maximum); + timebaseSiSpinBox->setMinimum(1e-9); + timebaseSiSpinBox->setMaximum(3.6e3); + timebaseSiSpinBox->setToolTip(tr("From %1 to %2") + .arg(timebaseSiSpinBox->textFromValue(timebaseSiSpinBox->minimum())) + .arg(timebaseSiSpinBox->textFromValue(timebaseSiSpinBox->maximum()))); + }); + connect(deviceSettings, &Dso::DeviceSettings::fixedSampleratesChanged, this, + [samplerateSiSpinBox, fixedSamplerateBox, timebaseSiSpinBox, + deviceSettings](const std::vector &sampleSteps) { + QSignalBlocker blocker(samplerateSiSpinBox); + QSignalBlocker blocker2(fixedSamplerateBox); + samplerateSiSpinBox->setVisible(false); + fixedSamplerateBox->setVisible(true); + fixedSamplerateBox->setModel(new FixedSamplerateModel(sampleSteps, fixedSamplerateBox)); + QSignalBlocker timebaseBlocker(timebaseSiSpinBox); + const double reducedRecLen = + deviceSettings->getRecordLength() - deviceSettings->trigger.swSampleMargin(); + timebaseSiSpinBox->setMinimum(reducedRecLen / sampleSteps.back().samplerate); + timebaseSiSpinBox->setMaximum(reducedRecLen / sampleSteps.front().samplerate); + timebaseSiSpinBox->setToolTip(tr("From %1 to %2") + .arg(timebaseSiSpinBox->textFromValue(timebaseSiSpinBox->minimum())) + .arg(timebaseSiSpinBox->textFromValue(timebaseSiSpinBox->maximum()))); + }); + connect(deviceSettings, &Dso::DeviceSettings::availableRecordLengthsChanged, this, [recordLengthComboBox, + deviceSettings]() { + QSignalBlocker blocker(recordLengthComboBox); + recordLengthComboBox->clear(); + for (auto entry : deviceSettings->limits->recordLengths) { + recordLengthComboBox->addItem( + entry.recordLength == UINT_MAX ? tr("Roll") : valueToString(entry.recordLength, Unit::SAMPLES, 3), + entry.recordLength); + } + }); + + connect(deviceSettings, &Dso::DeviceSettings::samplerateChanged, this, + [samplerateSiSpinBox, timebaseSiSpinBox, fixedSamplerateBox](Dso::Samplerate samplerate) { + QSignalBlocker blocker(samplerateSiSpinBox); + QSignalBlocker blocker2(timebaseSiSpinBox); + QSignalBlocker blocker3(fixedSamplerateBox); + samplerateSiSpinBox->setValue(samplerate.samplerate); + timebaseSiSpinBox->setValue(samplerate.timebase); // /DIVS_TIME + fixedSamplerateBox->setCurrentIndex(samplerate.fixedSamperateId); + }); + connect(deviceSettings, &Dso::DeviceSettings::recordLengthChanged, this, + [recordLengthComboBox](unsigned recordLengthId) { + QSignalBlocker blocker(recordLengthComboBox); + recordLengthComboBox->setCurrentIndex((int)recordLengthId); + }); + + connect(scope, &Settings::Scope::frequencybaseChanged, this, + [frequencybaseSiSpinBox](const Settings::Scope *scope) { + QSignalBlocker blocker(frequencybaseSiSpinBox); + frequencybaseSiSpinBox->setValue(scope->frequencybase()); + }); + connect(scope, &Settings::Scope::formatChanged, this, [formatComboBox](const Settings::Scope *scope) { + QSignalBlocker blocker(formatComboBox); + formatComboBox->setCurrentIndex((int)scope->format()); + }); } -/// \brief Called when the format combo box changes its value. -/// \param index The index of the combo box item. -void HorizontalDock::formatSelected(int index) { - scope->horizontal.format = (Dso::GraphFormat)index; - emit formatChanged(scope->horizontal.format); +/// \brief Don't close the dock, just hide it. +/// \param event The close event that should be handled. +void HorizontalDock::closeEvent(QCloseEvent *event) { + hide(); + event->accept(); } diff --git a/openhantek/src/docks/HorizontalDock.h b/openhantek/src/docks/HorizontalDock.h index da1b16d6..a755d559 100644 --- a/openhantek/src/docks/HorizontalDock.h +++ b/openhantek/src/docks/HorizontalDock.h @@ -7,18 +7,10 @@ #include -#include "hantekdso/enums.h" - -class QLabel; -class QCheckBox; -class QComboBox; - -class SiSpinBox; - -struct DsoSettingsScope; - -Q_DECLARE_METATYPE(std::vector) -Q_DECLARE_METATYPE(std::vector) +namespace Settings { +class Scope; +} +class DsoControl; /// \brief Dock window for the horizontal axis. /// It contains the settings for the timebase and the display format. @@ -30,69 +22,9 @@ class HorizontalDock : public QDockWidget { /// \param settings The target settings object. /// \param parent The parent widget. /// \param flags Flags for the window manager. - HorizontalDock(DsoSettingsScope *scope, QWidget *parent, Qt::WindowFlags flags = 0); - - /// \brief Changes the frequencybase. - /// \param frequencybase The frequencybase in hertz. - void setFrequencybase(double timebase); - /// \brief Changes the samplerate. - /// \param samplerate The samplerate in seconds. - void setSamplerate(double samplerate); - /// \brief Changes the timebase. - /// \param timebase The timebase in seconds. - double setTimebase(double timebase); - /// \brief Changes the record length if the new value is supported. - /// \param recordLength The record length in samples. - void setRecordLength(unsigned int recordLength); - /// \brief Changes the format if the new value is supported. - /// \param format The format for the horizontal axis. - /// \return Index of format-value, -1 on error. - int setFormat(Dso::GraphFormat format); - /// \brief Updates the available record lengths in the combo box. - /// \param recordLengths The available record lengths for the combo box. - void setAvailableRecordLengths(const std::vector &recordLengths); - /// \brief Updates the minimum and maximum of the samplerate spin box. - /// \param minimum The minimum value the spin box should accept. - /// \param maximum The minimum value the spin box should accept. - void setSamplerateLimits(double minimum, double maximum); - /// \brief Updates the mode and steps of the samplerate spin box. - /// \param mode The mode value the spin box should accept. - /// \param steps The steps value the spin box should accept. - void setSamplerateSteps(int mode, QList sampleSteps); + HorizontalDock(Settings::Scope *scope, DsoControl *dsocontrol, QWidget *parent, + Qt::WindowFlags flags = 0); protected: void closeEvent(QCloseEvent *event); - - QGridLayout *dockLayout; ///< The main layout for the dock window - QWidget *dockWidget; ///< The main widget for the dock window - QLabel *samplerateLabel; ///< The label for the samplerate spinbox - QLabel *timebaseLabel; ///< The label for the timebase spinbox - QLabel *frequencybaseLabel; ///< The label for the frequencybase spinbox - QLabel *recordLengthLabel; ///< The label for the record length combobox - QLabel *formatLabel; ///< The label for the format combobox - SiSpinBox *samplerateSiSpinBox; ///< Selects the samplerate for aquisitions - SiSpinBox *timebaseSiSpinBox; ///< Selects the timebase for voltage graphs - SiSpinBox *frequencybaseSiSpinBox; ///< Selects the frequencybase for spectrum graphs - QComboBox *recordLengthComboBox; ///< Selects the record length for aquisitions - QComboBox *formatComboBox; ///< Selects the way the sampled data is - /// interpreted and shown - - DsoSettingsScope *scope; ///< The settings provided by the parent class - QList timebaseSteps; ///< Steps for the timebase spinbox - - QStringList formatStrings; ///< Strings for the formats - - protected slots: - void frequencybaseSelected(double frequencybase); - void samplerateSelected(double samplerate); - void timebaseSelected(double timebase); - void recordLengthSelected(int index); - void formatSelected(int index); - - signals: - void frequencybaseChanged(double frequencybase); ///< The frequencybase has been changed - void samplerateChanged(double samplerate); ///< The samplerate has been changed - void timebaseChanged(double timebase); ///< The timebase has been changed - void recordLengthChanged(unsigned long recordLength); ///< The recordd length has been changed - void formatChanged(Dso::GraphFormat format); ///< The viewing format has been changed }; diff --git a/openhantek/src/docks/SpectrumDock.cpp b/openhantek/src/docks/SpectrumDock.cpp deleted file mode 100644 index 2d53607b..00000000 --- a/openhantek/src/docks/SpectrumDock.cpp +++ /dev/null @@ -1,100 +0,0 @@ -// SPDX-License-Identifier: GPL-2.0+ - -#include -#include -#include -#include -#include -#include - -#include - -#include "SpectrumDock.h" -#include "dockwindows.h" - -#include "settings.h" -#include "sispinbox.h" -#include "utils/printutils.h" - -template struct SELECT { - template - static constexpr auto OVERLOAD_OF( R (C::*pmf)(Args...) ) -> decltype(pmf) { - return pmf; - } -}; - -SpectrumDock::SpectrumDock(DsoSettingsScope *scope, QWidget *parent, Qt::WindowFlags flags) - : QDockWidget(tr("Spectrum"), parent, flags), scope(scope) { - - // Initialize lists for comboboxes - this->magnitudeSteps = { 1e0 , 2e0 , 3e0 , 6e0 , 1e1 , 2e1 , 3e1 , 6e1 , 1e2 , 2e2 , 3e2, 6e2 }; - for (const auto& magnitude: magnitudeSteps) - this->magnitudeStrings << valueToString(magnitude, UNIT_DECIBEL, 0); - - this->dockLayout = new QGridLayout(); - this->dockLayout->setColumnMinimumWidth(0, 64); - this->dockLayout->setColumnStretch(1, 1); - - // Initialize elements - for (ChannelID channel = 0; channel < scope->voltage.size(); ++channel) { - ChannelBlock b; - b.magnitudeComboBox=(new QComboBox()); - b.usedCheckBox=(new QCheckBox(scope->voltage[channel].name)); - - channelBlocks.push_back(b); - - this->dockLayout->addWidget(b.usedCheckBox, (int)channel, 0); - this->dockLayout->addWidget(b.magnitudeComboBox, (int)channel, 1); - - b.magnitudeComboBox->addItems(this->magnitudeStrings); - this->setMagnitude(channel, scope->spectrum[channel].magnitude); - this->setUsed(channel, scope->spectrum[channel].used); - - // Connect signals and slots - connect(b.usedCheckBox, &QCheckBox::toggled, [this,channel](bool checked) { - // Send signal if it was one of the checkboxes - if (channel < this->scope->voltage.size()) { - this->scope->spectrum[channel].used = checked; - emit usedChanged(channel, checked); - } - }); - - connect(b.magnitudeComboBox, SELECT::OVERLOAD_OF(&QComboBox::currentIndexChanged), [this,channel](unsigned index) { - // Send signal if it was one of the comboboxes - if (channel < this->scope->voltage.size()) { - this->scope->spectrum[channel].magnitude = this->magnitudeSteps.at(index); - emit magnitudeChanged(channel, this->scope->spectrum[channel].magnitude); - } - }); - } - - dockWidget = new QWidget(); - SetupDockWidget(this, dockWidget, dockLayout); -} - -/// \brief Don't close the dock, just hide it -/// \param event The close event that should be handled. -void SpectrumDock::closeEvent(QCloseEvent *event) { - this->hide(); - - event->accept(); -} - -int SpectrumDock::setMagnitude(ChannelID channel, double magnitude) { - if (channel >= scope->voltage.size()) return -1; - QSignalBlocker blocker(channelBlocks[channel].magnitudeComboBox); - - auto indexIt = std::find(magnitudeSteps.begin(),magnitudeSteps.end(),magnitude); - if (indexIt == magnitudeSteps.end()) return -1; - int index = (int)std::distance(magnitudeSteps.begin(), indexIt); - channelBlocks[channel].magnitudeComboBox->setCurrentIndex(index); - return index; -} - -unsigned SpectrumDock::setUsed(ChannelID channel, bool used) { - if (channel >= scope->voltage.size()) return INT_MAX; - QSignalBlocker blocker(channelBlocks[channel].usedCheckBox); - - channelBlocks[channel].usedCheckBox->setChecked(used); - return channel; -} diff --git a/openhantek/src/docks/SpectrumDock.h b/openhantek/src/docks/SpectrumDock.h deleted file mode 100644 index 11b5e12b..00000000 --- a/openhantek/src/docks/SpectrumDock.h +++ /dev/null @@ -1,62 +0,0 @@ -// SPDX-License-Identifier: GPL-2.0+ - -#pragma once - -#include -#include - -#include "scopesettings.h" - -class QLabel; -class QCheckBox; -class QComboBox; - -class SiSpinBox; - -/// \brief Dock window for the spectrum view. -/// It contains the magnitude for all channels and allows to enable/disable the -/// channels. -class SpectrumDock : public QDockWidget { - Q_OBJECT - - public: - /// \brief Initializes the spectrum view docking window. - /// \param settings The target settings object. - /// \param parent The parent widget. - /// \param flags Flags for the window manager. - SpectrumDock(DsoSettingsScope *scope, QWidget *parent, Qt::WindowFlags flags = 0); - - /// \brief Sets the magnitude for a channel. - /// \param channel The channel, whose magnitude should be set. - /// \param magnitude The magnitude in dB. - /// \return Index of magnitude-value, -1 on error. - int setMagnitude(ChannelID channel, double magnitude); - - /// \brief Enables/disables a channel. - /// \param channel The channel, that should be enabled/disabled. - /// \param used True if the channel should be enabled, false otherwise. - /// \return Index of channel, INT_MAX on error. - unsigned setUsed(ChannelID channel, bool used); - - protected: - void closeEvent(QCloseEvent *event); - - QGridLayout *dockLayout; ///< The main layout for the dock window - QWidget *dockWidget; ///< The main widget for the dock window - - struct ChannelBlock { - QCheckBox * usedCheckBox; ///< Enable/disable a specific channel - QComboBox * magnitudeComboBox; ///< Select the vertical magnitude for the spectrums - }; - - std::vector channelBlocks; - - DsoSettingsScope* scope; ///< The settings provided by the parent class - - std::vector magnitudeSteps; ///< The selectable magnitude steps in dB/div - QStringList magnitudeStrings; ///< String representations for the magnitude steps - - signals: - void magnitudeChanged(ChannelID channel, double magnitude); ///< A magnitude has been selected - void usedChanged(ChannelID channel, bool used); ///< A spectrum has been enabled/disabled -}; diff --git a/openhantek/src/docks/TriggerDock.cpp b/openhantek/src/docks/TriggerDock.cpp index 03e9297e..c19ee173 100644 --- a/openhantek/src/docks/TriggerDock.cpp +++ b/openhantek/src/docks/TriggerDock.cpp @@ -12,105 +12,111 @@ #include "TriggerDock.h" #include "dockwindows.h" -#include "hantekdso/controlspecification.h" +#include "hantekdso/devicesettings.h" +#include "hantekdso/dsocontrol.h" +#include "hantekdso/modelspecification.h" #include "settings.h" -#include "sispinbox.h" +#include "utils/enumhelper.h" #include "utils/printutils.h" +#include "widgets/sispinbox.h" -TriggerDock::TriggerDock(DsoSettingsScope *scope, const Dso::ControlSpecification *spec, QWidget *parent, - Qt::WindowFlags flags) - : QDockWidget(tr("Trigger"), parent, flags), scope(scope), mSpec(spec) { +TriggerDock::TriggerDock(Settings::Scope *scope, DsoControl *dsocontrol, QWidget *parent, Qt::WindowFlags flags) + : QDockWidget(tr("Trigger"), parent, flags) { - // Initialize lists for comboboxes - for (ChannelID channel = 0; channel < mSpec->channels; ++channel) - this->sourceStandardStrings << tr("CH%1").arg(channel + 1); - for (const Dso::SpecialTriggerChannel &specialTrigger : mSpec->specialTriggerChannels) - this->sourceSpecialStrings.append(QString::fromStdString(specialTrigger.name)); + QGridLayout *dockLayout; ///< The main layout for the dock window + QWidget *dockWidget; ///< The main widget for the dock window + QLabel *modeLabel; ///< The label for the trigger mode combobox + QLabel *sourceLabel; ///< The label for the trigger source combobox + QLabel *slopeLabel; ///< The label for the trigger slope combobox + QComboBox *modeComboBox; ///< Select the triggering mode + QComboBox *sourceComboBox; ///< Select the source for triggering + QComboBox *slopeComboBox; ///< Select the slope that causes triggering + + auto spec = dsocontrol->deviceSettings()->spec; // Initialize elements - this->modeLabel = new QLabel(tr("Mode")); - this->modeComboBox = new QComboBox(); - for (Dso::TriggerMode mode : mSpec->triggerModes) this->modeComboBox->addItem(Dso::triggerModeString(mode)); - - this->slopeLabel = new QLabel(tr("Slope")); - this->slopeComboBox = new QComboBox(); - for (Dso::Slope slope : Dso::SlopeEnum) this->slopeComboBox->addItem(Dso::slopeString(slope)); - - this->sourceLabel = new QLabel(tr("Source")); - this->sourceComboBox = new QComboBox(); - this->sourceComboBox->addItems(this->sourceStandardStrings); - this->sourceComboBox->addItems(this->sourceSpecialStrings); - - this->dockLayout = new QGridLayout(); - this->dockLayout->setColumnMinimumWidth(0, 64); - this->dockLayout->setColumnStretch(1, 1); - this->dockLayout->addWidget(this->modeLabel, 0, 0); - this->dockLayout->addWidget(this->modeComboBox, 0, 1); - this->dockLayout->addWidget(this->sourceLabel, 1, 0); - this->dockLayout->addWidget(this->sourceComboBox, 1, 1); - this->dockLayout->addWidget(this->slopeLabel, 2, 0); - this->dockLayout->addWidget(this->slopeComboBox, 2, 1); - - this->dockWidget = new QWidget(); + modeLabel = new QLabel(tr("Mode")); + modeComboBox = new QComboBox(); + for (Dso::TriggerMode mode : spec->triggerModes) modeComboBox->addItem(Dso::triggerModeString(mode)); + + slopeLabel = new QLabel(tr("Slope")); + slopeComboBox = new QComboBox(); + for (Dso::Slope slope : Enum()) slopeComboBox->addItem(Dso::slopeString(slope)); + + sourceLabel = new QLabel(tr("Source")); + sourceComboBox = new QComboBox(); + for (auto *c : *scope) + if (!c->isMathChannel()) sourceComboBox->addItem(tr("CH%1").arg(c->channelID() + 1), (int)c->channelID()); + int specialID = -1; // Assign negative (beginning with -1) ids for special channels + for (const Dso::SpecialTriggerChannel &specialTrigger : spec->specialTriggerChannels) + sourceComboBox->addItem(QString::fromStdString(specialTrigger.name), (int)specialID--); + + dockLayout = new QGridLayout(); + dockLayout->setColumnMinimumWidth(0, 64); + dockLayout->setColumnStretch(1, 1); + dockLayout->addWidget(modeLabel, 0, 0); + dockLayout->addWidget(modeComboBox, 0, 1); + dockLayout->addWidget(sourceLabel, 1, 0); + dockLayout->addWidget(sourceComboBox, 1, 1); + dockLayout->addWidget(slopeLabel, 2, 0); + dockLayout->addWidget(slopeComboBox, 2, 1); + + dockWidget = new QWidget(); SetupDockWidget(this, dockWidget, dockLayout); + const Dso::DeviceSettings *devicesettings = dsocontrol->deviceSettings().get(); + // Set values - setMode(scope->trigger.mode); - setSlope(scope->trigger.slope); - setSource(scope->trigger.special, scope->trigger.source); - - // Connect signals and slots - connect(this->modeComboBox, static_cast(&QComboBox::currentIndexChanged), - [this, spec](int index) { - this->scope->trigger.mode = mSpec->triggerModes[(unsigned)index]; - emit modeChanged(this->scope->trigger.mode); + modeComboBox->setCurrentIndex(spec->indexOfTriggerMode(devicesettings->trigger.mode())); + slopeComboBox->setCurrentIndex((int)devicesettings->trigger.slope()); + // A special channel is after all real channels + sourceComboBox->setCurrentIndex(devicesettings->trigger.special() ? (int)spec->channels + : 0 + (int)devicesettings->trigger.source()); + + // Connect widgets --> settings + connect(modeComboBox, static_cast(&QComboBox::currentIndexChanged), this, + [spec, dsocontrol, modeComboBox, devicesettings](int index) { + dsocontrol->setTriggerMode(spec->triggerModes[(unsigned)index]); + QSignalBlocker blocker(modeComboBox); + modeComboBox->setCurrentIndex(spec->indexOfTriggerMode(devicesettings->trigger.mode())); + }); + connect(slopeComboBox, static_cast(&QComboBox::currentIndexChanged), this, + [dsocontrol, slopeComboBox, devicesettings](int index) { + dsocontrol->setTriggerSlope((Dso::Slope)index); + QSignalBlocker blocker(slopeComboBox); + slopeComboBox->setCurrentIndex((int)devicesettings->trigger.slope()); }); - connect(this->slopeComboBox, static_cast(&QComboBox::currentIndexChanged), - [this](int index) { - this->scope->trigger.slope = (Dso::Slope)index; - emit slopeChanged(this->scope->trigger.slope); + connect(sourceComboBox, static_cast(&QComboBox::currentIndexChanged), this, + [sourceComboBox, devicesettings, dsocontrol, spec](int index) { + int channelIndex = sourceComboBox->itemData(index, Qt::UserRole).toInt(); + dsocontrol->setTriggerSource(channelIndex < 0, + channelIndex < 0 ? (unsigned)(1 + -channelIndex) : (unsigned)channelIndex); + QSignalBlocker blocker(sourceComboBox); + sourceComboBox->setCurrentIndex(devicesettings->trigger.special() + ? (int)spec->channels + : 0 + (int)devicesettings->trigger.source()); }); - connect(this->sourceComboBox, static_cast(&QComboBox::currentIndexChanged), - [this](int index) { - bool special = false; - - if (index >= this->sourceStandardStrings.count()) { - index -= this->sourceStandardStrings.count(); - special = true; - } - - this->scope->trigger.source = (unsigned)index; - this->scope->trigger.special = special; - emit sourceChanged(special, (unsigned)index); + // Connect settings --> widgets + connect(&devicesettings->trigger, &Dso::Trigger::modeChanged, this, [modeComboBox, spec](Dso::TriggerMode mode) { + QSignalBlocker blocker(modeComboBox); + modeComboBox->setCurrentIndex(spec->indexOfTriggerMode(mode)); + }); + connect(&devicesettings->trigger, &Dso::Trigger::sourceChanged, this, + [this, sourceComboBox, spec](bool special, unsigned int id) { + QSignalBlocker blocker(sourceComboBox); + // A special channel is after all real channels + sourceComboBox->setCurrentIndex(special ? (int)spec->channels : 0 + (int)id); }); + connect(&devicesettings->trigger, &Dso::Trigger::slopeChanged, this, [slopeComboBox](Dso::Slope slope) { + QSignalBlocker blocker(slopeComboBox); + slopeComboBox->setCurrentIndex((int)slope); + }); } /// \brief Don't close the dock, just hide it /// \param event The close event that should be handled. void TriggerDock::closeEvent(QCloseEvent *event) { - this->hide(); + hide(); event->accept(); } - -void TriggerDock::setMode(Dso::TriggerMode mode) { - int index = std::find(mSpec->triggerModes.begin(), mSpec->triggerModes.end(), mode) - mSpec->triggerModes.begin(); - QSignalBlocker blocker(modeComboBox); - modeComboBox->setCurrentIndex(index); -} - -void TriggerDock::setSlope(Dso::Slope slope) { - QSignalBlocker blocker(slopeComboBox); - slopeComboBox->setCurrentIndex((int)slope); -} - -void TriggerDock::setSource(bool special, unsigned int id) { - if ((!special && id >= (unsigned int)this->sourceStandardStrings.count()) || - (special && id >= (unsigned int)this->sourceSpecialStrings.count())) - return; - - int index = (int)id; - if (special) index += this->sourceStandardStrings.count(); - QSignalBlocker blocker(sourceComboBox); - sourceComboBox->setCurrentIndex(index); -} diff --git a/openhantek/src/docks/TriggerDock.h b/openhantek/src/docks/TriggerDock.h index e361aa16..00f90721 100644 --- a/openhantek/src/docks/TriggerDock.h +++ b/openhantek/src/docks/TriggerDock.h @@ -2,19 +2,22 @@ #pragma once +#include +#include #include #include #include -#include -#include #include "hantekdso/enums.h" class SiSpinBox; -struct DsoSettingsScope; +namespace Settings { +class Scope; +} namespace Dso { -struct ControlSpecification; +struct ModelSpec; } +class DsoControl; /// \brief Dock window for the trigger settings. /// It contains the settings for the trigger mode, source and slope. @@ -27,40 +30,8 @@ class TriggerDock : public QDockWidget { /// \param spec /// \param parent The parent widget. /// \param flags Flags for the window manager. - TriggerDock(DsoSettingsScope *scope, const Dso::ControlSpecification* mSpec, QWidget *parent, Qt::WindowFlags flags = 0); - - /// \brief Changes the trigger mode if the new mode is supported. - /// \param mode The trigger mode. - void setMode(Dso::TriggerMode mode); - - /// \brief Changes the trigger source if the new source is supported. - /// \param special true for a special channel (EXT, ...) as trigger source. - /// \param id The number of the channel, that should be used as trigger. - void setSource(bool special, unsigned int id); - - /// \brief Changes the trigger slope if the new slope is supported. - /// \param slope The trigger slope. - void setSlope(Dso::Slope slope); + TriggerDock(Settings::Scope *scope, DsoControl *dsocontrol, QWidget *parent, Qt::WindowFlags flags = 0); protected: void closeEvent(QCloseEvent *event); - - QGridLayout *dockLayout; ///< The main layout for the dock window - QWidget *dockWidget; ///< The main widget for the dock window - QLabel *modeLabel; ///< The label for the trigger mode combobox - QLabel *sourceLabel; ///< The label for the trigger source combobox - QLabel *slopeLabel; ///< The label for the trigger slope combobox - QComboBox *modeComboBox; ///< Select the triggering mode - QComboBox *sourceComboBox; ///< Select the source for triggering - QComboBox *slopeComboBox; ///< Select the slope that causes triggering - - DsoSettingsScope *scope; ///< The settings provided by the parent class - const Dso::ControlSpecification* mSpec; - - QStringList sourceStandardStrings; ///< Strings for the standard trigger sources - QStringList sourceSpecialStrings; ///< Strings for the special trigger sources - signals: - void modeChanged(Dso::TriggerMode); ///< The trigger mode has been changed - void sourceChanged(bool special, unsigned int id); ///< The trigger source has been changed - void slopeChanged(Dso::Slope); ///< The trigger slope has been changed }; diff --git a/openhantek/src/docks/VoltageDock.cpp b/openhantek/src/docks/VoltageDock.cpp deleted file mode 100644 index 5cfb7ad0..00000000 --- a/openhantek/src/docks/VoltageDock.cpp +++ /dev/null @@ -1,129 +0,0 @@ -// SPDX-License-Identifier: GPL-2.0+ - -#include -#include -#include -#include -#include -#include - -#include - -#include "VoltageDock.h" -#include "dockwindows.h" - -#include "settings.h" -#include "sispinbox.h" -#include "utils/printutils.h" - -template struct SELECT { - template - static constexpr auto OVERLOAD_OF( R (C::*pmf)(Args...) ) -> decltype(pmf) { - return pmf; - } -}; - -VoltageDock::VoltageDock(DsoSettingsScope *scope, const Dso::ControlSpecification *spec, QWidget *parent, Qt::WindowFlags flags) - : QDockWidget(tr("Voltage"), parent, flags), scope(scope), spec(spec) { - - // Initialize lists for comboboxes - for (Dso::Coupling c: spec->couplings) - couplingStrings.append(Dso::couplingString(c)); - - for( auto e: Dso::MathModeEnum ) { - modeStrings.append(Dso::mathModeString(e)); - } - - for (double gainStep: scope->gainSteps) - gainStrings << valueToString(gainStep, UNIT_VOLTS, 0); - - dockLayout = new QGridLayout(); - dockLayout->setColumnMinimumWidth(0, 64); - dockLayout->setColumnStretch(1, 1); - - // Initialize elements - for (ChannelID channel = 0; channel < scope->voltage.size(); ++channel) { - ChannelBlock b; - - b.miscComboBox=(new QComboBox()); - b.gainComboBox=(new QComboBox()); - b.invertCheckBox=(new QCheckBox(tr("Invert"))); - b.usedCheckBox=(new QCheckBox(scope->voltage[channel].name)); - - channelBlocks.push_back(std::move(b)); - - if (channel < spec->channels) - b.miscComboBox->addItems(couplingStrings); - else - b.miscComboBox->addItems(modeStrings); - - b.gainComboBox->addItems(gainStrings); - - dockLayout->addWidget(b.usedCheckBox, (int)channel * 3, 0); - dockLayout->addWidget(b.gainComboBox, (int)channel * 3, 1); - dockLayout->addWidget(b.miscComboBox, (int)channel * 3 + 1, 1); - dockLayout->addWidget(b.invertCheckBox, (int)channel * 3 + 2, 1); - - if (channel < spec->channels) - setCoupling(channel, scope->voltage[channel].couplingOrMathIndex); - else - setMode(scope->voltage[channel].couplingOrMathIndex); - setGain(channel, scope->voltage[channel].gainStepIndex); - setUsed(channel, scope->voltage[channel].used); - - connect(b.gainComboBox, SELECT::OVERLOAD_OF(&QComboBox::currentIndexChanged), [this,channel](int index) { - this->scope->voltage[channel].gainStepIndex = (unsigned)index; - emit gainChanged(channel, this->scope->gain(channel)); - }); - connect(b.invertCheckBox, &QAbstractButton::toggled, [this,channel](bool checked) { - this->scope->voltage[channel].inverted = checked; - }); - connect(b.miscComboBox, SELECT::OVERLOAD_OF(&QComboBox::currentIndexChanged), [this,channel,spec,scope](int index){ - this->scope->voltage[channel].couplingOrMathIndex = (unsigned)index; - if (channel < spec->channels) { - emit couplingChanged(channel, scope->coupling(channel, spec)); - } else { - emit modeChanged(Dso::getMathMode(this->scope->voltage[channel])); - } - }); - connect(b.usedCheckBox, &QAbstractButton::toggled, [this,channel](bool checked) { - this->scope->voltage[channel].used = checked; - emit usedChanged(channel, checked); - }); - } - - dockWidget = new QWidget(); - SetupDockWidget(this, dockWidget, dockLayout); -} - -/// \brief Don't close the dock, just hide it -/// \param event The close event that should be handled. -void VoltageDock::closeEvent(QCloseEvent *event) { - hide(); - event->accept(); -} - -void VoltageDock::setCoupling(ChannelID channel, unsigned couplingIndex) { - if (channel >= spec->channels) return; - if (couplingIndex >= spec->couplings.size()) return; - QSignalBlocker blocker(channelBlocks[channel].miscComboBox); - channelBlocks[channel].miscComboBox->setCurrentIndex((int)couplingIndex); -} - -void VoltageDock::setGain(ChannelID channel, unsigned gainStepIndex) { - if (channel >= scope->voltage.size()) return; - if (gainStepIndex >= scope->gainSteps.size()) return; - QSignalBlocker blocker(channelBlocks[channel].gainComboBox); - channelBlocks[channel].gainComboBox->setCurrentIndex((unsigned)gainStepIndex); -} - -void VoltageDock::setMode(unsigned mathModeIndex) { - QSignalBlocker blocker(channelBlocks[spec->channels].miscComboBox); - channelBlocks[spec->channels].miscComboBox->setCurrentIndex((int)mathModeIndex); -} - -void VoltageDock::setUsed(ChannelID channel, bool used) { - if (channel >= scope->voltage.size()) return; - QSignalBlocker blocker(channelBlocks[channel].usedCheckBox); - channelBlocks[channel].usedCheckBox->setChecked(used); -} diff --git a/openhantek/src/docks/VoltageDock.h b/openhantek/src/docks/VoltageDock.h deleted file mode 100644 index 260a84b7..00000000 --- a/openhantek/src/docks/VoltageDock.h +++ /dev/null @@ -1,76 +0,0 @@ -// SPDX-License-Identifier: GPL-2.0+ - -#pragma once - -#include -#include -#include -#include -#include - -#include "scopesettings.h" -#include "hantekdso/controlspecification.h" -#include "post/postprocessingsettings.h" - -class SiSpinBox; - -/// \brief Dock window for the voltage channel settings. -/// It contains the settings for gain and coupling for both channels and -/// allows to enable/disable the channels. -class VoltageDock : public QDockWidget { - Q_OBJECT - - public: - /// \brief Initializes the vertical axis docking window. - /// \param settings The target settings object. - /// \param parent The parent widget. - /// \param flags Flags for the window manager. - VoltageDock(DsoSettingsScope *scope, const Dso::ControlSpecification* spec, QWidget *parent, Qt::WindowFlags flags = 0); - - /// \brief Sets the coupling for a channel. - /// \param channel The channel, whose coupling should be set. - /// \param couplingIndex The coupling-mode index. - void setCoupling(ChannelID channel, unsigned couplingIndex); - - /// \brief Sets the gain for a channel. - /// \param channel The channel, whose gain should be set. - /// \param gain The gain in volts. - void setGain(ChannelID channel, unsigned gainStepIndex); - - /// \brief Sets the mode for the math channel. - /// \param mathModeIndex The math-mode index. - void setMode(unsigned mathModeIndex); - - /// \brief Enables/disables a channel. - /// \param channel The channel, that should be enabled/disabled. - /// \param used True if the channel should be enabled, false otherwise. - void setUsed(ChannelID channel, bool used); - - protected: - void closeEvent(QCloseEvent *event); - - QGridLayout *dockLayout; ///< The main layout for the dock window - QWidget *dockWidget; ///< The main widget for the dock window - - struct ChannelBlock { - QCheckBox * usedCheckBox; ///< Enable/disable a specific channel - QComboBox * gainComboBox; ///< Select the vertical gain for the channels - QComboBox * miscComboBox; ///< Select coupling for real and mode for math channels - QCheckBox * invertCheckBox; ///< Select if the channels should be displayed inverted - }; - - std::vector channelBlocks; - - DsoSettingsScope *scope; ///< The settings provided by the parent class - const Dso::ControlSpecification *spec; - - QStringList couplingStrings; ///< The strings for the couplings - QStringList modeStrings; ///< The strings for the math mode - QStringList gainStrings; ///< String representations for the gain steps - - signals: - void couplingChanged(ChannelID channel, Dso::Coupling coupling); ///< A coupling has been selected - void gainChanged(ChannelID channel, double gain); ///< A gain has been selected - void modeChanged(Dso::MathMode mode); ///< The mode for the math channels has been changed - void usedChanged(ChannelID channel, bool used); ///< A channel has been enabled/disabled -}; diff --git a/openhantek/src/docks/VoltageOrSpectrumDock.cpp b/openhantek/src/docks/VoltageOrSpectrumDock.cpp new file mode 100644 index 00000000..e710909b --- /dev/null +++ b/openhantek/src/docks/VoltageOrSpectrumDock.cpp @@ -0,0 +1,288 @@ +// SPDX-License-Identifier: GPL-2.0+ + +#include +#include +#include +#include +#include +#include +#include +#include + +#include + +#include "VoltageOrSpectrumDock.h" +#include "dockwindows.h" + +#include "hantekdso/dsocontrol.h" +#include "scopesettings.h" +#include "utils/enumhelper.h" +#include "utils/printutils.h" +#include "widgets/sispinbox.h" + +template struct SELECT { + template static constexpr auto OVERLOAD_OF(R (C::*pmf)(Args...)) -> decltype(pmf) { + return pmf; + } +}; + +VoltageOrSpectrumDock::VoltageOrSpectrumDock(bool isSpectrum, Settings::Scope *scope, DsoControl *dsocontrol, + QWidget *parent, Qt::WindowFlags flags) + : QDockWidget(isSpectrum ? tr("Spectrum") : tr("Voltage"), parent, flags), m_isSpectrum(isSpectrum) { + + const Dso::DeviceSettings *deviceSettings = dsocontrol->deviceSettings().get(); + + // Initialize lists for comboboxes + for (Dso::Coupling c : deviceSettings->spec->couplings) couplingStrings.append(Dso::couplingString(c)); + for (auto e : Enum()) { modeStrings.append(PostProcessing::mathModeString(e)); } + magnitudeSteps = {1e0, 2e0, 3e0, 6e0, 1e1, 2e1, 3e1, 6e1, 1e2, 2e2, 3e2, 6e2}; + for (const auto &magnitude : magnitudeSteps) magnitudeStrings << valueToString(magnitude, Unit::DECIBEL, 0); + + QWidget *dockWidget = new QWidget(); + dockLayout = new QVBoxLayout(); + SetupDockWidget(this, dockWidget, dockLayout); + + // Create widgets for each channel and connect to math channels changed signal to recreate if necessary + // Initialize elements + for (Settings::Channel *channel : *scope) { + if (!channel->isMathChannel()) createChannelWidgets(scope, dsocontrol, deviceSettings, channel); + } + + auto btnAdd = new QPushButton(tr("Add math channel"), this); + connect(btnAdd, &QPushButton::clicked, this, [scope, deviceSettings, dsocontrol]() { + scope->addMathChannel(dsocontrol->channelUsage(), deviceSettings); + }); + dockLayout->addWidget(btnAdd); + + for (Settings::Channel *channel : *scope) { + if (channel->isMathChannel()) createChannelWidgets(scope, dsocontrol, deviceSettings, channel); + } + + connect(scope, &Settings::Scope::mathChannelAdded, this, + [this, scope, dsocontrol, deviceSettings](Settings::Channel *channel) { + createChannelWidgets(scope, dsocontrol, deviceSettings, channel); + }); +} + +/// \brief Don't close the dock, just hide it +/// \param event The close event that should be handled. +void VoltageOrSpectrumDock::closeEvent(QCloseEvent *event) { + hide(); + event->accept(); +} + +void VoltageOrSpectrumDock::setMagnitude(QComboBox *magnitudeComboBox, double magnitude) { + QSignalBlocker blocker(magnitudeComboBox); + + auto indexIt = std::find(magnitudeSteps.begin(), magnitudeSteps.end(), magnitude); + if (indexIt == magnitudeSteps.end()) return; + int index = (int)std::distance(magnitudeSteps.begin(), indexIt); + magnitudeComboBox->setCurrentIndex(index); +} + +void VoltageOrSpectrumDock::fillGainBox(QComboBox *gainComboBox, Settings::Scope *scope, DsoControl *dsocontrol, + Settings::Channel *channel) { + QSignalBlocker b(gainComboBox); + gainComboBox->clear(); + if (scope->useHardwareGainSteps()) { + for (auto &gainStep : dsocontrol->specification()->gain) + gainComboBox->addItem(valueToString(gainStep.gain, Unit::VOLTS, 0), gainStep.gainIdentificator); + gainComboBox->setCurrentIndex((int)channel->voltage()->gainStepIndex()); + } else { + int index = -1; + for (unsigned i = 0; i < gainValue.size(); ++i) { + if (channel->gain() >= gainValue[i]) index = (int)i; + gainComboBox->addItem(valueToString(gainValue[i], Unit::VOLTS, 0)); + } + gainComboBox->setToolTip( + tr("Hardware Gain Index: %1").arg(findMatchingHardwareGainId(channel->gain(), dsocontrol))); + gainComboBox->setCurrentIndex(index); + } +} + +int VoltageOrSpectrumDock::findMatchingHardwareGainId(double gain, DsoControl *dsocontrol) { + int matchingHardwareIndex = 0; + auto hwGains = dsocontrol->specification()->gain; + for (unsigned hIndex = 0; hIndex < hwGains.size(); ++hIndex) { + if (gain < hwGains[hIndex].gain) { break; } + matchingHardwareIndex = hIndex; + } + return matchingHardwareIndex; +} + +void VoltageOrSpectrumDock::createChannelWidgets(Settings::Scope *scope, DsoControl *dsocontrol, + const Dso::DeviceSettings *deviceSettings, + Settings::Channel *channel) { + + // Create a common parent, that is deleted when the dock is deleted (due to having the dock as parent) + // as well as getting deleted when the corresponding channel vanishes. + QGroupBox *channelParent = new QGroupBox(this); + channelParent->setTitle(channel->name()); + channelParentWidgets.push_back(channelParent); + channelParent->setCheckable(true); + connect(channel, &QObject::destroyed, channelParent, &QObject::deleteLater); + dockLayout->addWidget(channelParent); + + auto layout = new QVBoxLayout(channelParent); + channelParent->setLayout(layout); + + // Spectrum properties + if (isSpectrum()) { + channelParent->setChecked(channel->spectrum()->visible()); + + QComboBox *magnitudeComboBox = new QComboBox(channelParent); + magnitudeComboBox->addItems(magnitudeStrings); + layout->addWidget(magnitudeComboBox); + setMagnitude(magnitudeComboBox, channel->spectrum()->magnitude()); + + // Connect widgets --> settings + connect(channelParent, &QGroupBox::toggled, + [scope, channel](bool checked) { channel->setSpectrumVisible(checked); }); + + connect( + magnitudeComboBox, SELECT::OVERLOAD_OF(&QComboBox::currentIndexChanged), channelParent, + [scope, channel, this](unsigned index) { channel->spectrum()->setMagnitude(magnitudeSteps.at(index)); }); + // Connect settings --> widgets + connect(channel->spectrum(), &Settings::Spectrum::magnitudeChanged, channelParent, + [this, magnitudeComboBox](const Settings::Spectrum *spectrum) { + setMagnitude(magnitudeComboBox, spectrum->magnitude()); + }); + + connect(channel->spectrum(), &Settings::Spectrum::visibleChanged, channelParent, [channelParent](bool visible) { + QSignalBlocker blocker(channelParent); + channelParent->setChecked(visible); + }); + return; + } + + // Voltage properties + + channelParent->setChecked(channel->visible()); + // Connect widgets --> settings + connect(channelParent, &QGroupBox::toggled, channelParent, + [channel, dsocontrol](bool checked) { channel->setVoltageVisible(checked); }); + // Connect settings --> widgets + connect(channel, &Settings::Channel::visibleChanged, channelParent, [channelParent](bool used) { + QSignalBlocker blocker(channelParent); + channelParent->setChecked(used); + }); + + auto sublayout = new QHBoxLayout; // Invert + Gain + coupling next to each other + layout->addLayout(sublayout); + + /////// Invert /////// + QCheckBox *invertCheckBox = new QCheckBox(tr("INV"), channelParent); + invertCheckBox->setToolTip(tr("Invert channel on x-axes")); + invertCheckBox->setChecked(channel->inverted()); + sublayout->addWidget(invertCheckBox); + connect(invertCheckBox, &QAbstractButton::toggled, channelParent, + [this, channel](bool checked) { channel->setInverted(checked); }); + connect(channel, &Settings::Channel::invertedChanged, channelParent, [invertCheckBox](bool inverted) { + QSignalBlocker blocker(invertCheckBox); + invertCheckBox->setChecked(inverted); + }); + + /////// The voltage gain steps in V /////// + QComboBox *gainComboBox = new QComboBox(channelParent); + fillGainBox(gainComboBox, scope, dsocontrol, channel); + connect(scope, &Settings::Scope::useHardwareGainChanged, this, [this, gainComboBox, scope, dsocontrol, channel]() { + fillGainBox(gainComboBox, scope, dsocontrol, channel); + }); + connect(gainComboBox, SELECT::OVERLOAD_OF(&QComboBox::currentIndexChanged), channelParent, + [channel, dsocontrol, scope, this, gainComboBox](int index) { + double newGain; + if (scope->useHardwareGainSteps()) { + newGain = dsocontrol->specification()->gain[(unsigned)index].gain; + dsocontrol->setGain(channel->channelID(), (unsigned)index); + } else { + newGain = gainValue[index]; + int matchingHardwareIndex = findMatchingHardwareGainId(newGain, dsocontrol); + gainComboBox->setToolTip(tr("Hardware Gain Index: %1").arg(matchingHardwareIndex)); + dsocontrol->setGain(channel->channelID(), (unsigned)matchingHardwareIndex); + } + channel->setGain(newGain); + }); + connect(channel->voltage(), &Dso::Channel::gainStepIndexChanged, channelParent, + [gainComboBox, scope](unsigned gainId) { + if (!scope->useHardwareGainSteps()) return; ///< Do nothing if we are not using hardware gain steps + QSignalBlocker blocker(gainComboBox); + gainComboBox->setCurrentIndex((int)gainId); + }); + sublayout->addWidget(gainComboBox); + + if (channel->isMathChannel()) { + Settings::MathChannel *mathChannel = static_cast(channel); + auto mathlayout = new QHBoxLayout; // Channel selection and mathmode next to each other + layout->addLayout(mathlayout); + + QComboBox *mathChannel1 = new QComboBox(channelParent); + mathlayout->addWidget(mathChannel1); + QComboBox *mathModeComboBox = new QComboBox(channelParent); + mathlayout->addWidget(mathModeComboBox); + QComboBox *mathChannel2 = new QComboBox(channelParent); + mathlayout->addWidget(mathChannel2); + + for (Settings::Channel *c : *scope) { + if (c->isMathChannel()) continue; + mathChannel1->addItem(c->name(), c->channelID()); + mathChannel2->addItem(c->name(), c->channelID()); + if (mathChannel->firstID() == c->channelID()) mathChannel1->setCurrentIndex(mathChannel1->count() - 1); + if (mathChannel->secondID() == c->channelID()) mathChannel2->setCurrentIndex(mathChannel2->count() - 1); + } + + mathModeComboBox->addItems(modeStrings); + mathModeComboBox->setCurrentIndex((int)mathChannel->mathMode()); + + // Connect widgets --> settings + connect(mathModeComboBox, SELECT::OVERLOAD_OF(&QComboBox::currentIndexChanged), channelParent, + [mathChannel](int index) { mathChannel->setMathMode((PostProcessing::MathMode)index); }); + connect(mathChannel1, SELECT::OVERLOAD_OF(&QComboBox::currentIndexChanged), channelParent, + [mathChannel, deviceSettings](int index) { + mathChannel->setFirstChannel((unsigned)index, deviceSettings->voltage[(unsigned)index]); + }); + connect(mathChannel2, SELECT::OVERLOAD_OF(&QComboBox::currentIndexChanged), channelParent, + [mathChannel, deviceSettings](int index) { + mathChannel->setSecondChannel((unsigned)index, deviceSettings->voltage[(unsigned)index]); + }); + // Connect settings --> widgets + connect(mathChannel, &Settings::MathChannel::mathModeChanged, channelParent, + [mathModeComboBox](const Settings::Channel *channel) { + QSignalBlocker blocker(mathModeComboBox); + mathModeComboBox->setCurrentIndex((int)((Settings::MathChannel *)channel)->mathMode()); + }); + connect(mathChannel, &Settings::MathChannel::firstChannelChanged, channelParent, + [mathChannel1](ChannelID channel) { + if (channel == UINT_MAX) return; + QSignalBlocker blocker(mathChannel1); + mathChannel1->setCurrentIndex((int)channel); + }); + connect(mathChannel, &Settings::MathChannel::firstChannelChanged, channelParent, + [mathChannel2](ChannelID channel) { + if (channel == UINT_MAX) return; + QSignalBlocker blocker(mathChannel2); + mathChannel2->setCurrentIndex((int)channel); + }); + + auto btnRemove = new QPushButton(tr("Remove"), channelParent); + connect(btnRemove, &QPushButton::clicked, channelParent, + [channel, scope]() { scope->removeMathChannel(channel->channelID()); }); + layout->addWidget(btnRemove); + connect(channelParent, &QGroupBox::toggled, this, [btnRemove](bool) { btnRemove->setEnabled(true); }); + btnRemove->setEnabled(true); + } else { + QComboBox *couplingComboBox = new QComboBox(channelParent); + couplingComboBox->addItems(couplingStrings); + couplingComboBox->setCurrentIndex((int)channel->voltage()->couplingIndex()); + // Connect widgets --> settings + connect( + couplingComboBox, SELECT::OVERLOAD_OF(&QComboBox::currentIndexChanged), channelParent, + [channel, dsocontrol](int index) { dsocontrol->setCoupling(channel->channelID(), (Dso::Coupling)index); }); + // Connect settings --> widgets + connect(channel->voltage(), &Dso::Channel::couplingIndexChanged, channelParent, + [couplingComboBox](unsigned couplingIndex) { + QSignalBlocker blocker(couplingComboBox); + couplingComboBox->setCurrentIndex((int)couplingIndex); + }); + sublayout->addWidget(couplingComboBox); + } +} diff --git a/openhantek/src/docks/VoltageOrSpectrumDock.h b/openhantek/src/docks/VoltageOrSpectrumDock.h new file mode 100644 index 00000000..5c31f01e --- /dev/null +++ b/openhantek/src/docks/VoltageOrSpectrumDock.h @@ -0,0 +1,53 @@ +// SPDX-License-Identifier: GPL-2.0+ + +#pragma once + +#include +#include +#include +#include +#include + +#include "hantekdso/modelspecification.h" +#include "post/postprocessingsettings.h" +#include "scopesettings.h" + +class SiSpinBox; +class DsoControl; + +/// \brief Dock window for the voltage channel settings. +/// It contains the settings for gain and coupling for both channels and +/// allows to enable/disable the channels. +class VoltageOrSpectrumDock : public QDockWidget { + Q_OBJECT + + public: + /// \brief Initializes the vertical axis docking window. + /// \param settings The target settings object. + /// \param parent The parent widget. + /// \param flags Flags for the window manager. + VoltageOrSpectrumDock(bool isSpectrum, Settings::Scope *scope, DsoControl *dsocontrol, QWidget *parent, + Qt::WindowFlags flags = 0); + inline bool isSpectrum() const { return m_isSpectrum; } + + protected: + void closeEvent(QCloseEvent *event); + void createChannelWidgets(Settings::Scope *scope, DsoControl *dsocontrol, + const Dso::DeviceSettings *deviceSettings, Settings::Channel *channel); + void setMagnitude(QComboBox *magnitudeComboBox, double magnitude); + void fillGainBox(QComboBox *gainComboBox, Settings::Scope *scope, DsoControl *dsocontrol, + Settings::Channel *channel); + int findMatchingHardwareGainId(double gain,DsoControl *dsocontrol); + + QVBoxLayout *dockLayout; + std::list channelParentWidgets; + const bool m_isSpectrum; + + /// Selectable voltage gain steps in V, if use-hardwareGainSteps is disabled + std::vector gainValue = {1e-2, 2e-2, 5e-2, 1e-1, 2e-1, 5e-1, 1e0, 2e0, 5e0}; + + std::vector magnitudeSteps; ///< The selectable magnitude steps in dB/div + QStringList magnitudeStrings; ///< String representations for the magnitude steps + QStringList couplingStrings; ///< The strings for the couplings + QStringList modeStrings; ///< The strings for the math mode +}; diff --git a/openhantek/src/docks/dockwindows.cpp b/openhantek/src/docks/dockwindows.cpp index 37fff337..5599f697 100644 --- a/openhantek/src/docks/dockwindows.cpp +++ b/openhantek/src/docks/dockwindows.cpp @@ -8,29 +8,29 @@ #include -#include "post/postprocessingsettings.h" +#include "dockwindows.h" #include "hantekdso/enums.h" #include "hantekprotocol/types.h" -#include "dockwindows.h" +#include "post/postprocessingsettings.h" -void SetupDockWidget(QDockWidget *dockWindow, QWidget *dockWidget, QLayout *layout) { +void SetupDockWidget(QDockWidget *dockWindow, QWidget *dockWidget, QLayout *layout, QSizePolicy::Policy vPolicy) { dockWindow->setObjectName(dockWindow->windowTitle()); dockWindow->setAllowedAreas(Qt::LeftDockWidgetArea | Qt::RightDockWidgetArea); dockWidget->setLayout(layout); - dockWidget->setSizePolicy(QSizePolicy(QSizePolicy::Minimum, QSizePolicy::Fixed, QSizePolicy::DefaultType)); + dockWidget->setSizePolicy(QSizePolicy(QSizePolicy::Minimum, vPolicy, QSizePolicy::DefaultType)); dockWindow->setWidget(dockWidget); } void registerDockMetaTypes() { qRegisterMetaType(); - qRegisterMetaType(); + qRegisterMetaType(); qRegisterMetaType(); qRegisterMetaType(); qRegisterMetaType(); qRegisterMetaType(); - qRegisterMetaType(); + qRegisterMetaType(); qRegisterMetaType(); - qRegisterMetaType >(); - qRegisterMetaType >(); + qRegisterMetaType>(); + qRegisterMetaType>(); qRegisterMetaType("ChannelID"); } diff --git a/openhantek/src/docks/dockwindows.h b/openhantek/src/docks/dockwindows.h index 3a479b44..2913ee49 100644 --- a/openhantek/src/docks/dockwindows.h +++ b/openhantek/src/docks/dockwindows.h @@ -6,4 +6,5 @@ #include void registerDockMetaTypes(); -void SetupDockWidget(QDockWidget *dockWindow, QWidget *dockWidget, QLayout *layout); +void SetupDockWidget(QDockWidget *dockWindow, QWidget *dockWidget, QLayout *layout, + QSizePolicy::Policy vPolicy = QSizePolicy::Fixed); diff --git a/openhantek/src/docks/gainAdjustDock.cpp b/openhantek/src/docks/gainAdjustDock.cpp new file mode 100644 index 00000000..34eb3e79 --- /dev/null +++ b/openhantek/src/docks/gainAdjustDock.cpp @@ -0,0 +1,253 @@ +// SPDX-License-Identifier: GPL-2.0+ + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include + +#include "gainAdjustDock.h" + +#include "dockwindows.h" +#include "hantekdso/devicesettings.h" +#include "hantekdso/dsocontrol.h" +#include "hantekdso/enums.h" +#include "hantekdso/modelspecification.h" +#include "hantekprotocol/codes.h" +#include "iconfont/QtAwesome.h" +#include "post/selfcalibration.h" +#include "scopesettings.h" +#include "utils/debugnotify.h" +#include "utils/enumhelper.h" +#include "utils/printutils.h" +#include "widgets/sispinbox.h" + +#include +#include +#include +#include +#include +#include +#include + +Q_DECLARE_METATYPE(std::vector) +Q_DECLARE_METATYPE(std::vector) + +template struct SELECT { + template static constexpr auto OVERLOAD_OF(R (C::*pmf)(Args...)) -> decltype(pmf) { + return pmf; + } +}; + +using namespace Debug; + +GainAdjustDock::GainAdjustDock(DsoControl *dsocontrol, SelfCalibration *selfCalibration, QWidget *parent, + Qt::WindowFlags flags) + : QDockWidget(tr("Calibration"), parent, flags) { + + Dso::ModelSpec *spec = const_cast(dsocontrol->deviceSettings()->spec); + + QVBoxLayout *dockLayout = new QVBoxLayout; + QScrollArea *scroll = new QScrollArea(this); + QGridLayout *grid = new QGridLayout; + QWidget *dockWidget = new QWidget(this); + + QWidget *scrollWidget = new QWidget(this); + scrollWidget->setSizePolicy(QSizePolicy(QSizePolicy::MinimumExpanding, QSizePolicy::MinimumExpanding)); + grid->setSizeConstraint(QLayout::SetFixedSize); + scrollWidget->setLayout(grid); + scroll->setWidget(scrollWidget); + scroll->setSizePolicy(QSizePolicy(QSizePolicy::MinimumExpanding, QSizePolicy::MinimumExpanding)); + dockLayout->addWidget(scroll, 1); + + QHBoxLayout *btns = new QHBoxLayout; + QPushButton *btnCalibrationStart = new QPushButton(tr("Self-calibration")); + btnCalibrationStart->setIcon(iconFont->icon(fa::warning)); + + QPushButton *btnHelp = new QPushButton(); + btnHelp->setIcon(iconFont->icon(fa::info)); + + QPushButton *btnApply = new QPushButton(this); + btnApply->setIcon(iconFont->icon(fa::check)); + btnApply->setText(tr("Apply")); + btnApply->setEnabled(false); + dockLayout->addWidget(btnApply); + + ///// Dialog for self calibration ///// + QDialog *calibrationDialog = new QDialog(this); + calibrationDialog->setWindowTitle(tr("Self-calibration")); + calibrationDialog->setModal(true); + QVBoxLayout *dialogLayout = new QVBoxLayout(calibrationDialog); + QLabel *dialogLabel = new QLabel(calibrationDialog); + QProgressBar *dialogProgress = new QProgressBar(calibrationDialog); + dialogProgress->setRange(0, 100); + connect(selfCalibration, &SelfCalibration::progress, this, + [dialogLabel, dialogProgress](double progress, const QString &task) { + dialogProgress->setValue(progress * 100); + dialogLabel->setText(task); + }); + QPushButton *btnCalibrationCancel = new QPushButton(tr("Cancel"), calibrationDialog); + dialogLayout->addWidget(dialogLabel); + dialogLayout->addWidget(dialogProgress); + dialogLayout->addWidget(btnCalibrationCancel); + + QComboBox *selfCalibChannels = new QComboBox(this); + for (ChannelID c = 0; c < spec->channels; ++c) selfCalibChannels->addItem(tr("Channel %1").arg(c + 1)); + selfCalibChannels->setCurrentIndex(0); + + connect(btnCalibrationCancel, &QPushButton::clicked, selfCalibration, &SelfCalibration::cancel); + connect(btnCalibrationStart, &QPushButton::clicked, selfCalibration, [selfCalibration, selfCalibChannels]() { + selfCalibration->start((unsigned)selfCalibChannels->currentIndex()); + }); + connect(selfCalibration, &SelfCalibration::runningChanged, this, + [btnCalibrationStart, calibrationDialog, this](bool running) { + calibrationDialog->setVisible(running); + btnCalibrationStart->setDisabled(running); + if (!running) emit selfCalibrationFinished(); + }); + + connect(btnHelp, &QPushButton::clicked, this, [this, selfCalibChannels, spec]() { + QMessageBox::information( + this, tr("Self-calibration"), + tr("Please connect the %1 probe of your oscilloscope to GND and the test signal " + "generator. Self-calibration will adjust the gain values to match the amplitude " + "of %2V. This may be inaccurate for low gain values, because of clipping in " + "the signal.\n\nThe new values are not permanent and will be discarded on exit. If the " + "new values are an improvement in your opinion, please visit our github page " + "(Help->About) and post them in a new Issue.") + .arg(selfCalibChannels->currentText()) + .arg(spec->testSignalAmplitude)); + }); + + btns->addWidget(selfCalibChannels); + btns->addWidget(btnCalibrationStart); + btns->addStretch(1); + btns->addWidget(btnHelp); + + dockLayout->addLayout(btns); + + int row = 0; + + // Header row + QLabel *l = new QLabel(tr("Gain\nFactor*"), this); + l->setToolTip(tr("The formula is 1V=Voltage=(RawSamplePoint/gainFactor-offset)*hardwareGainVoltage to archive " + "a 1V amplitude with the DSO included test signal.")); + grid->addWidget(l, row, 1); + l = new QLabel(tr("Offset\nStart"), this); + l->setToolTip(tr("Some models allow to set a hardware offset. That value is usually limited by 8, 10 or 16bits or " + "any value up to 16bits. To compute an accurate sample set, the offset range need to be known.")); + grid->addWidget(l, row, 2); + grid->addWidget(new QLabel(tr("Offset\nEnd"), this), row, 3); + l = new QLabel(tr("GND\nCorrection"), this); + l->setToolTip(tr("The signal ground offset is usually auto-calibrated, but some models do not do that. " + "Adjust these values if the ground level is not correct for you.")); + grid->addWidget(l, row, 4); + grid->setColumnStretch(0, 0); ///< Columns are fixed size + grid->setColumnStretch(1, 0); + grid->setColumnStretch(2, 0); + grid->setColumnStretch(3, 0); + + ++row; + + auto *copy = new std::vector(spec->calibration); + connect(this, &QObject::destroyed, [copy]() { delete copy; }); // delete copy when this is destroyed + + QSpinBox *edit; + QDoubleSpinBox *editD; + for (unsigned gainId = 0; gainId < spec->gain.size(); ++gainId) { + for (ChannelID channelID = 0; channelID < spec->channels; ++channelID) { + auto *l = new QLabel( + tr("%1 CH%2").arg(valueToString(spec->gain[gainId].gain, Unit::VOLTS, -1)).arg(channelID + 1), this); + grid->addWidget(l, row, 0); + + editD = new QDoubleSpinBox(this); + editD->setSizePolicy(QSizePolicy(QSizePolicy::Fixed, QSizePolicy::Fixed)); + editD->setRange(1, 2000); + editD->setSingleStep(0.1); + editD->setValue(copy->at(channelID)[gainId].voltageLimit); + editD->setToolTip(tr("Original value is: %1").arg(editD->value())); + grid->addWidget(editD, row, 1); + connect(this, &GainAdjustDock::selfCalibrationFinished, editD, [editD, spec, channelID, gainId]() { + editD->setValue(spec->calibration[channelID][gainId].voltageLimit); + }); + connect(editD, SELECT::OVERLOAD_OF(&QDoubleSpinBox::valueChanged), this, + [btnApply, copy, channelID, gainId](double value) { + btnApply->setEnabled(true); + (*copy)[channelID][gainId].voltageLimit = value; + }); + + edit = new QSpinBox(this); + edit->setSizePolicy(QSizePolicy(QSizePolicy::Fixed, QSizePolicy::Fixed)); + edit->setRange(0, UINT16_MAX); + edit->setSingleStep(1); + edit->setValue(copy->at(channelID)[gainId].offsetStart); + edit->setToolTip(tr("Original value is: %1").arg(edit->value())); + grid->addWidget(edit, row, 2); + connect(this, &GainAdjustDock::selfCalibrationFinished, edit, [edit, spec, channelID, gainId]() { + edit->setValue(spec->calibration[channelID][gainId].offsetStart); + }); + connect(edit, SELECT::OVERLOAD_OF(&QSpinBox::valueChanged), this, + [btnApply, copy, channelID, gainId](int value) { + btnApply->setEnabled(true); + (*copy)[channelID][gainId].offsetStart = (unsigned short)value; + }); + + edit = new QSpinBox(this); + edit->setSizePolicy(QSizePolicy(QSizePolicy::Fixed, QSizePolicy::Fixed)); + edit->setRange(0, UINT16_MAX); + edit->setSingleStep(1); + edit->setValue(copy->at(channelID)[gainId].offsetEnd); + edit->setToolTip(tr("Original value is: %1").arg(edit->value())); + grid->addWidget(edit, row, 3); + connect(this, &GainAdjustDock::selfCalibrationFinished, edit, [edit, spec, channelID, gainId]() { + edit->setValue(spec->calibration[channelID][gainId].offsetEnd); + }); + connect(edit, SELECT::OVERLOAD_OF(&QSpinBox::valueChanged), this, + [btnApply, copy, channelID, gainId](int value) { + btnApply->setEnabled(true); + (*copy)[channelID][gainId].offsetEnd = (unsigned short)value; + }); + + editD = new QDoubleSpinBox(this); + editD->setSizePolicy(QSizePolicy(QSizePolicy::Fixed, QSizePolicy::Fixed)); + editD->setSingleStep(0.01); + editD->setRange(-1, 1); + editD->setValue(copy->at(channelID)[gainId].offsetCorrection); + editD->setToolTip(tr("Original value is: %1").arg(editD->value())); + grid->addWidget(editD, row, 4); + connect(this, &GainAdjustDock::selfCalibrationFinished, editD, [editD, spec, channelID, gainId]() { + editD->setValue(spec->calibration[channelID][gainId].offsetCorrection); + }); + connect(editD, SELECT::OVERLOAD_OF(&QDoubleSpinBox::valueChanged), this, + [btnApply, copy, channelID, gainId](double value) { + btnApply->setEnabled(true); + (*copy)[channelID][gainId].offsetCorrection = value; + }); + + ++row; + } + } + + connect(btnApply, &QPushButton::clicked, this, [dsocontrol, spec, copy, btnApply]() { + spec->calibration = *copy; + for (ChannelID channelID = 0; channelID < spec->channels; ++channelID) + dsocontrol->setOffset(channelID, dsocontrol->deviceSettings()->voltage[channelID]->offset(), true); + btnApply->setEnabled(false); + }); + + SetupDockWidget(this, dockWidget, dockLayout, QSizePolicy::Expanding); +} + +/// \brief Don't close the dock, just hide it. +/// \param event The close event that should be handled. +void GainAdjustDock::closeEvent(QCloseEvent *event) { + this->hide(); + event->accept(); +} diff --git a/openhantek/src/docks/gainAdjustDock.h b/openhantek/src/docks/gainAdjustDock.h new file mode 100644 index 00000000..1ea9cadf --- /dev/null +++ b/openhantek/src/docks/gainAdjustDock.h @@ -0,0 +1,37 @@ +// SPDX-License-Identifier: GPL-2.0+ + +#pragma once + +#include +#include + +#include "hantekprotocol/codes.h" +#include "utils/debugnotify.h" + +namespace Dso { +struct ModelSpec; +} +class DsoControl; +namespace Debug { +class LogModel; +} +class SelfCalibration; + +/// \brief Dock window for adjusting the gain factor +/// It contains the settings for the timebase and the display format. +class GainAdjustDock : public QDockWidget { + Q_OBJECT + + public: + /// \brief Initializes the dock + /// \param settings The target settings object. + /// \param parent The parent widget. + /// \param flags Flags for the window manager. + GainAdjustDock(DsoControl *dsocontrol, SelfCalibration *selfCalibration, QWidget *parent, + Qt::WindowFlags flags = 0); + + protected: + void closeEvent(QCloseEvent *event); + signals: + void selfCalibrationFinished(); +}; diff --git a/openhantek/src/dsowidget.cpp b/openhantek/src/dsowidget.cpp deleted file mode 100644 index 54f58ad5..00000000 --- a/openhantek/src/dsowidget.cpp +++ /dev/null @@ -1,614 +0,0 @@ -// SPDX-License-Identifier: GPL-2.0+ - -#include - -#include -#include -#include -#include -#include - -#include "dsowidget.h" - -#include "post/postprocessingsettings.h" -#include "post/graphgenerator.h" -#include "post/ppresult.h" - -#include "utils/printutils.h" - -#include "glscope.h" -#include "scopesettings.h" -#include "viewconstants.h" -#include "viewsettings.h" -#include "widgets/levelslider.h" - -static int zoomScopeRow = 0; - -DsoWidget::DsoWidget(DsoSettingsScope *scope, DsoSettingsView *view, const Dso::ControlSpecification *spec, - QWidget *parent, Qt::WindowFlags flags) - : QWidget(parent, flags), scope(scope), view(view), spec(spec), mainScope(GlScope::createNormal(scope, view)), - zoomScope(GlScope::createZoomed(scope, view)) { - - // Palette for this widget - QPalette palette; - palette.setColor(QPalette::Background, view->screen.background); - palette.setColor(QPalette::WindowText, view->screen.text); - - setupSliders(mainSliders); - setupSliders(zoomSliders); - - connect(mainScope, &GlScope::markerMoved, [this](int marker, double position) { - double value = std::round(position / MARKER_STEP) * MARKER_STEP; - - this->scope->horizontal.marker[marker] = value; - this->mainSliders.markerSlider->setValue(marker, value); - this->mainScope->markerUpdated(); - }); - - // The table for the settings - settingsTriggerLabel = new QLabel(); - settingsTriggerLabel->setMinimumWidth(160); - settingsTriggerLabel->setIndent(5); - settingsRecordLengthLabel = new QLabel(); - settingsRecordLengthLabel->setAlignment(Qt::AlignRight); - settingsRecordLengthLabel->setPalette(palette); - settingsSamplerateLabel = new QLabel(); - settingsSamplerateLabel->setAlignment(Qt::AlignRight); - settingsSamplerateLabel->setPalette(palette); - settingsTimebaseLabel = new QLabel(); - settingsTimebaseLabel->setAlignment(Qt::AlignRight); - settingsTimebaseLabel->setPalette(palette); - settingsFrequencybaseLabel = new QLabel(); - settingsFrequencybaseLabel->setAlignment(Qt::AlignRight); - settingsFrequencybaseLabel->setPalette(palette); - swTriggerStatus = new QLabel(); - swTriggerStatus->setMinimumWidth(30); - swTriggerStatus->setText(tr("TR")); - swTriggerStatus->setAlignment(Qt::AlignCenter); - swTriggerStatus->setAutoFillBackground(true); - swTriggerStatus->setVisible(false); - settingsLayout = new QHBoxLayout(); - settingsLayout->addWidget(swTriggerStatus); - settingsLayout->addWidget(settingsTriggerLabel); - settingsLayout->addWidget(settingsRecordLengthLabel, 1); - settingsLayout->addWidget(settingsSamplerateLabel, 1); - settingsLayout->addWidget(settingsTimebaseLabel, 1); - settingsLayout->addWidget(settingsFrequencybaseLabel, 1); - - // The table for the marker details - markerInfoLabel = new QLabel(); - markerInfoLabel->setMinimumWidth(160); - markerInfoLabel->setPalette(palette); - markerTimeLabel = new QLabel(); - markerTimeLabel->setAlignment(Qt::AlignRight); - markerTimeLabel->setPalette(palette); - markerFrequencyLabel = new QLabel(); - markerFrequencyLabel->setAlignment(Qt::AlignRight); - markerFrequencyLabel->setPalette(palette); - markerTimebaseLabel = new QLabel(); - markerTimebaseLabel->setAlignment(Qt::AlignRight); - markerTimebaseLabel->setPalette(palette); - markerFrequencybaseLabel = new QLabel(); - markerFrequencybaseLabel->setAlignment(Qt::AlignRight); - markerFrequencybaseLabel->setPalette(palette); - markerLayout = new QHBoxLayout(); - markerLayout->addWidget(markerInfoLabel); - markerLayout->addWidget(markerTimeLabel, 1); - markerLayout->addWidget(markerFrequencyLabel, 1); - markerLayout->addWidget(markerTimebaseLabel, 1); - markerLayout->addWidget(markerFrequencybaseLabel, 1); - - // The table for the measurements - QPalette tablePalette = palette; - measurementLayout = new QGridLayout(); - measurementLayout->setColumnMinimumWidth(0, 64); - measurementLayout->setColumnMinimumWidth(1, 32); - measurementLayout->setColumnStretch(2, 2); - measurementLayout->setColumnStretch(3, 2); - measurementLayout->setColumnStretch(4, 3); - measurementLayout->setColumnStretch(5, 3); - for (ChannelID channel = 0; channel < scope->voltage.size(); ++channel) { - tablePalette.setColor(QPalette::WindowText, view->screen.voltage[channel]); - measurementNameLabel.push_back(new QLabel(scope->voltage[channel].name)); - measurementNameLabel[channel]->setPalette(tablePalette); - measurementMiscLabel.push_back(new QLabel()); - measurementMiscLabel[channel]->setPalette(tablePalette); - measurementGainLabel.push_back(new QLabel()); - measurementGainLabel[channel]->setAlignment(Qt::AlignRight); - measurementGainLabel[channel]->setPalette(tablePalette); - tablePalette.setColor(QPalette::WindowText, view->screen.spectrum[channel]); - measurementMagnitudeLabel.push_back(new QLabel()); - measurementMagnitudeLabel[channel]->setAlignment(Qt::AlignRight); - measurementMagnitudeLabel[channel]->setPalette(tablePalette); - measurementAmplitudeLabel.push_back(new QLabel()); - measurementAmplitudeLabel[channel]->setAlignment(Qt::AlignRight); - measurementAmplitudeLabel[channel]->setPalette(palette); - measurementFrequencyLabel.push_back(new QLabel()); - measurementFrequencyLabel[channel]->setAlignment(Qt::AlignRight); - measurementFrequencyLabel[channel]->setPalette(palette); - setMeasurementVisible(channel); - measurementLayout->addWidget(measurementNameLabel[channel], (int)channel, 0); - measurementLayout->addWidget(measurementMiscLabel[channel], (int)channel, 1); - measurementLayout->addWidget(measurementGainLabel[channel], (int)channel, 2); - measurementLayout->addWidget(measurementMagnitudeLabel[channel], (int)channel, 3); - measurementLayout->addWidget(measurementAmplitudeLabel[channel], (int)channel, 4); - measurementLayout->addWidget(measurementFrequencyLabel[channel], (int)channel, 5); - if ((unsigned)channel < spec->channels) - updateVoltageCoupling((unsigned)channel); - else - updateMathMode(); - updateVoltageDetails((unsigned)channel); - updateSpectrumDetails((unsigned)channel); - } - - // The layout for the widgets - mainLayout = new QGridLayout(); - mainLayout->setColumnStretch(2, 1); // Scopes increase their size - // Bars around the scope, needed because the slider-drawing-area is outside - // the scope at min/max - mainLayout->setColumnMinimumWidth(1, mainSliders.triggerPositionSlider->preMargin()); - mainLayout->setColumnMinimumWidth(3, mainSliders.triggerPositionSlider->postMargin()); - mainLayout->setSpacing(0); - int row = 0; - mainLayout->addLayout(settingsLayout, row++, 0, 1, 5); - // 5x5 box for mainScope & mainSliders - mainLayout->setRowMinimumHeight(row + 1, mainSliders.offsetSlider->preMargin()); - mainLayout->setRowMinimumHeight(row + 3, mainSliders.offsetSlider->postMargin()); - mainLayout->setRowStretch(row + 2, 1); - mainLayout->addWidget(mainScope, row + 2, 2); - mainLayout->addWidget(mainSliders.offsetSlider, row + 1, 0, 3, 2, Qt::AlignRight); - mainLayout->addWidget(mainSliders.triggerPositionSlider, row, 1, 2, 3, Qt::AlignBottom); - mainLayout->addWidget(mainSliders.triggerLevelSlider, row + 1, 3, 3, 2, Qt::AlignLeft); - mainLayout->addWidget(mainSliders.markerSlider, row + 3, 1, 2, 3, Qt::AlignTop); - row += 5; - // Separators and markerLayout - mainLayout->setRowMinimumHeight(row++, 4); - mainLayout->addLayout(markerLayout, row++, 0, 1, 5); - mainLayout->setRowMinimumHeight(row++, 4); - // 5x5 box for zoomScope & zoomSliders - zoomScopeRow = row + 2; - mainLayout->addWidget(zoomScope, zoomScopeRow, 2); - mainLayout->addWidget(zoomSliders.offsetSlider, row + 1, 0, 3, 2, Qt::AlignRight); - mainLayout->addWidget(zoomSliders.triggerPositionSlider, row, 1, 2, 3, Qt::AlignBottom); - mainLayout->addWidget(zoomSliders.triggerLevelSlider, row + 1, 3, 3, 2, Qt::AlignLeft); - row += 5; - // Separator and embedded measurementLayout - mainLayout->setRowMinimumHeight(row++, 8); - mainLayout->addLayout(measurementLayout, row++, 0, 1, 5); - - // The widget itself - setPalette(palette); - setBackgroundRole(QPalette::Background); - setAutoFillBackground(true); - setLayout(mainLayout); - - // Connect change-signals of sliders - connect(mainSliders.offsetSlider, &LevelSlider::valueChanged, this, &DsoWidget::updateOffset); - connect(zoomSliders.offsetSlider, &LevelSlider::valueChanged, this, &DsoWidget::updateOffset); - - connect(mainSliders.triggerPositionSlider, &LevelSlider::valueChanged, this, &DsoWidget::updateTriggerPosition); - zoomSliders.triggerPositionSlider->setEnabled(false); - - connect(mainSliders.triggerLevelSlider, &LevelSlider::valueChanged, this, &DsoWidget::updateTriggerLevel); - connect(zoomSliders.triggerLevelSlider, &LevelSlider::valueChanged, this, &DsoWidget::updateTriggerLevel); - - connect(mainSliders.markerSlider, &LevelSlider::valueChanged, [this](int index, double value) { - updateMarker(index, value); - mainScope->update(); - zoomScope->update(); - }); - zoomSliders.markerSlider->setEnabled(false); -} - -void DsoWidget::setupSliders(DsoWidget::Sliders &sliders) { - // The offset sliders for all possible channels - sliders.offsetSlider = new LevelSlider(Qt::RightArrow); - for (ChannelID channel = 0; channel < scope->voltage.size(); ++channel) { - sliders.offsetSlider->addSlider(scope->voltage[channel].name, channel); - sliders.offsetSlider->setColor(channel, view->screen.voltage[channel]); - sliders.offsetSlider->setLimits(channel, -DIVS_VOLTAGE / 2, DIVS_VOLTAGE / 2); - sliders.offsetSlider->setStep(channel, 0.2); - sliders.offsetSlider->setValue(channel, scope->voltage[channel].offset); - sliders.offsetSlider->setIndexVisible(channel, scope->voltage[channel].used); - } - for (ChannelID channel = 0; channel < scope->voltage.size(); ++channel) { - sliders.offsetSlider->addSlider(scope->spectrum[channel].name, scope->voltage.size() + channel); - sliders.offsetSlider->setColor(scope->voltage.size() + channel, view->screen.spectrum[channel]); - sliders.offsetSlider->setLimits(scope->voltage.size() + channel, -DIVS_VOLTAGE / 2, DIVS_VOLTAGE / 2); - sliders.offsetSlider->setStep(scope->voltage.size() + channel, 0.2); - sliders.offsetSlider->setValue(scope->voltage.size() + channel, scope->spectrum[channel].offset); - sliders.offsetSlider->setIndexVisible(scope->voltage.size() + channel, scope->spectrum[channel].used); - } - - // The triggerPosition slider - sliders.triggerPositionSlider = new LevelSlider(Qt::DownArrow); - sliders.triggerPositionSlider->addSlider(); - sliders.triggerPositionSlider->setLimits(0, 0.0, 1.0); - sliders.triggerPositionSlider->setStep(0, 0.2 / (double)DIVS_TIME); - sliders.triggerPositionSlider->setValue(0, scope->trigger.position); - sliders.triggerPositionSlider->setIndexVisible(0, true); - - // The sliders for the trigger levels - sliders.triggerLevelSlider = new LevelSlider(Qt::LeftArrow); - for (ChannelID channel = 0; channel < spec->channels; ++channel) { - sliders.triggerLevelSlider->addSlider((int)channel); - sliders.triggerLevelSlider->setColor(channel, - (!scope->trigger.special && channel == scope->trigger.source) - ? view->screen.voltage[channel] - : view->screen.voltage[channel].darker()); - adaptTriggerLevelSlider(sliders, channel); - sliders.triggerLevelSlider->setValue(channel, scope->voltage[channel].trigger); - sliders.triggerLevelSlider->setIndexVisible(channel, scope->voltage[channel].used); - } - - // The marker slider - sliders.markerSlider = new LevelSlider(Qt::UpArrow); - for (int marker = 0; marker < MARKER_COUNT; ++marker) { - sliders.markerSlider->addSlider(QString::number(marker + 1), marker); - sliders.markerSlider->setLimits(marker, -DIVS_TIME / 2, DIVS_TIME / 2); - sliders.markerSlider->setStep(marker, MARKER_STEP); - sliders.markerSlider->setValue(marker, scope->horizontal.marker[marker]); - sliders.markerSlider->setIndexVisible(marker, true); - } -} - -/// \brief Set the trigger level sliders minimum and maximum to the new values. -void DsoWidget::adaptTriggerLevelSlider(DsoWidget::Sliders &sliders, ChannelID channel) { - sliders.triggerLevelSlider->setLimits((int)channel, - (-DIVS_VOLTAGE / 2 - scope->voltage[channel].offset) * scope->gain(channel), - (DIVS_VOLTAGE / 2 - scope->voltage[channel].offset) * scope->gain(channel)); - sliders.triggerLevelSlider->setStep((int)channel, scope->gain(channel) * 0.05); -} - -/// \brief Show/Hide a line of the measurement table. -void DsoWidget::setMeasurementVisible(ChannelID channel) { - - bool visible = scope->voltage[channel].used || scope->spectrum[channel].used; - - measurementNameLabel[channel]->setVisible(visible); - measurementMiscLabel[channel]->setVisible(visible); - - measurementAmplitudeLabel[channel]->setVisible(visible); - measurementFrequencyLabel[channel]->setVisible(visible); - if (!visible) { - measurementGainLabel[channel]->setText(QString()); - measurementAmplitudeLabel[channel]->setText(QString()); - measurementFrequencyLabel[channel]->setText(QString()); - } - - measurementGainLabel[channel]->setVisible(scope->voltage[channel].used); - if (!scope->voltage[channel].used) { measurementGainLabel[channel]->setText(QString()); } - - measurementMagnitudeLabel[channel]->setVisible(scope->spectrum[channel].used); - if (!scope->spectrum[channel].used) { measurementMagnitudeLabel[channel]->setText(QString()); } -} - -/// \brief Update the label about the marker measurements -void DsoWidget::updateMarkerDetails() { - double divs = fabs(scope->horizontal.marker[1] - scope->horizontal.marker[0]); - double time = divs * scope->horizontal.timebase; - - QString infoLabelPrefix(tr("Markers")); - if (view->zoom) { - infoLabelPrefix = tr("Zoom x%L1").arg(DIVS_TIME / divs, -1, 'g', 3); - markerTimebaseLabel->setText(valueToString(time / DIVS_TIME, UNIT_SECONDS, 3) + tr("/div")); - markerFrequencybaseLabel->setText( - valueToString(divs * scope->horizontal.frequencybase / DIVS_TIME, UNIT_HERTZ, 4) + tr("/div")); - } - markerInfoLabel->setText( - infoLabelPrefix.append(": %1 %2") - .arg( - valueToString(0.5 + scope->horizontal.marker[0] / DIVS_TIME - scope->trigger.position, UNIT_SECONDS, 4)) - .arg(valueToString(0.5 + scope->horizontal.marker[1] / DIVS_TIME - scope->trigger.position, UNIT_SECONDS, - 4))); - - markerTimeLabel->setText(valueToString(time, UNIT_SECONDS, 4)); - markerFrequencyLabel->setText(valueToString(1.0 / time, UNIT_HERTZ, 4)); -} - -/// \brief Update the label about the trigger settings -void DsoWidget::updateSpectrumDetails(ChannelID channel) { - setMeasurementVisible(channel); - - if (scope->spectrum[channel].used) - measurementMagnitudeLabel[channel]->setText(valueToString(scope->spectrum[channel].magnitude, UNIT_DECIBEL, 3) + - tr("/div")); - else - measurementMagnitudeLabel[channel]->setText(QString()); -} - -/// \brief Update the label about the trigger settings -void DsoWidget::updateTriggerDetails() { - // Update the trigger details - QPalette tablePalette = palette(); - tablePalette.setColor(QPalette::WindowText, view->screen.voltage[scope->trigger.source]); - settingsTriggerLabel->setPalette(tablePalette); - QString levelString = valueToString(scope->voltage[scope->trigger.source].trigger, UNIT_VOLTS, 3); - QString pretriggerString = tr("%L1%").arg((int)(scope->trigger.position * 100 + 0.5)); - settingsTriggerLabel->setText(tr("%1 %2 %3 %4") - .arg(scope->voltage[scope->trigger.source].name, - Dso::slopeString(scope->trigger.slope), levelString, pretriggerString)); - - /// \todo This won't work for special trigger sources -} - -/// \brief Update the label about the trigger settings -void DsoWidget::updateVoltageDetails(ChannelID channel) { - if (channel >= scope->voltage.size()) return; - - setMeasurementVisible(channel); - - if (scope->voltage[channel].used) - measurementGainLabel[channel]->setText(valueToString(scope->gain(channel), UNIT_VOLTS, 3) + tr("/div")); - else - measurementGainLabel[channel]->setText(QString()); -} - -/// \brief Handles frequencybaseChanged signal from the horizontal dock. -/// \param frequencybase The frequencybase used for displaying the trace. -void DsoWidget::updateFrequencybase(double frequencybase) { - settingsFrequencybaseLabel->setText(valueToString(frequencybase, UNIT_HERTZ, 4) + tr("/div")); -} - -/// \brief Updates the samplerate field after changing the samplerate. -/// \param samplerate The samplerate set in the oscilloscope. -void DsoWidget::updateSamplerate(double samplerate) { - settingsSamplerateLabel->setText(valueToString(samplerate, UNIT_SAMPLES, 4) + tr("/s")); -} - -/// \brief Handles timebaseChanged signal from the horizontal dock. -/// \param timebase The timebase used for displaying the trace. -void DsoWidget::updateTimebase(double timebase) { - settingsTimebaseLabel->setText(valueToString(timebase, UNIT_SECONDS, 4) + tr("/div")); - - updateMarkerDetails(); -} - -/// \brief Handles magnitudeChanged signal from the spectrum dock. -/// \param channel The channel whose magnitude was changed. -void DsoWidget::updateSpectrumMagnitude(ChannelID channel) { updateSpectrumDetails(channel); } - -/// \brief Handles usedChanged signal from the spectrum dock. -/// \param channel The channel whose used-state was changed. -/// \param used The new used-state for the channel. -void DsoWidget::updateSpectrumUsed(ChannelID channel, bool used) { - if (channel >= (unsigned int)scope->voltage.size()) return; - - mainSliders.offsetSlider->setIndexVisible(scope->voltage.size() + channel, used); - zoomSliders.offsetSlider->setIndexVisible(scope->voltage.size() + channel, used); - - updateSpectrumDetails(channel); -} - -/// \brief Handles modeChanged signal from the trigger dock. -void DsoWidget::updateTriggerMode() { updateTriggerDetails(); } - -/// \brief Handles slopeChanged signal from the trigger dock. -void DsoWidget::updateTriggerSlope() { updateTriggerDetails(); } - -/// \brief Handles sourceChanged signal from the trigger dock. -void DsoWidget::updateTriggerSource() { - // Change the colors of the trigger sliders - if (scope->trigger.special || scope->trigger.source >= spec->channels) { - mainSliders.triggerPositionSlider->setColor(0, view->screen.border); - zoomSliders.triggerPositionSlider->setColor(0, view->screen.border); - } else { - mainSliders.triggerPositionSlider->setColor(0, view->screen.voltage[scope->trigger.source]); - zoomSliders.triggerPositionSlider->setColor(0, view->screen.voltage[scope->trigger.source]); - } - - for (ChannelID channel = 0; channel < spec->channels; ++channel) { - QColor color = (!scope->trigger.special && channel == scope->trigger.source) - ? view->screen.voltage[channel] - : view->screen.voltage[channel].darker(); - mainSliders.triggerLevelSlider->setColor(channel, color); - zoomSliders.triggerLevelSlider->setColor(channel, color); - } - - updateTriggerDetails(); -} - -/// \brief Handles couplingChanged signal from the voltage dock. -/// \param channel The channel whose coupling was changed. -void DsoWidget::updateVoltageCoupling(ChannelID channel) { - if (channel >= (unsigned int)scope->voltage.size()) return; - - measurementMiscLabel[channel]->setText(Dso::couplingString(scope->coupling(channel, spec))); -} - -/// \brief Handles modeChanged signal from the voltage dock. -void DsoWidget::updateMathMode() { - measurementMiscLabel[spec->channels]->setText( - Dso::mathModeString(Dso::getMathMode(scope->voltage[spec->channels]))); -} - -/// \brief Handles gainChanged signal from the voltage dock. -/// \param channel The channel whose gain was changed. -void DsoWidget::updateVoltageGain(ChannelID channel) { - if (channel >= (unsigned int)scope->voltage.size()) return; - - if (channel < spec->channels) { - adaptTriggerLevelSlider(mainSliders, channel); - adaptTriggerLevelSlider(zoomSliders, channel); - } - - updateVoltageDetails(channel); -} - -/// \brief Handles usedChanged signal from the voltage dock. -/// \param channel The channel whose used-state was changed. -/// \param used The new used-state for the channel. -void DsoWidget::updateVoltageUsed(ChannelID channel, bool used) { - if (channel >= (unsigned int)scope->voltage.size()) return; - - mainSliders.offsetSlider->setIndexVisible(channel, used); - zoomSliders.offsetSlider->setIndexVisible(channel, used); - - mainSliders.triggerLevelSlider->setIndexVisible(channel, used); - zoomSliders.triggerLevelSlider->setIndexVisible(channel, used); - - setMeasurementVisible(channel); - updateVoltageDetails(channel); -} - -/// \brief Change the record length. -void DsoWidget::updateRecordLength(unsigned long size) { - settingsRecordLengthLabel->setText(valueToString(size, UNIT_SAMPLES, 4)); -} - -/// \brief Show/hide the zoom view. -void DsoWidget::updateZoom(bool enabled) { - mainLayout->setRowStretch(zoomScopeRow, enabled ? 1 : 0); - zoomScope->setVisible(enabled); - - if (enabled) { - zoomSliders.offsetSlider->show(); - zoomSliders.triggerPositionSlider->show(); - zoomSliders.triggerLevelSlider->show(); - } else { - zoomSliders.offsetSlider->hide(); - zoomSliders.triggerPositionSlider->hide(); - zoomSliders.triggerLevelSlider->hide(); - } - - // Show time-/frequencybase and zoom factor if the magnified scope is shown - markerLayout->setStretch(3, enabled ? 1 : 0); - markerTimebaseLabel->setVisible(enabled); - markerLayout->setStretch(4, enabled ? 1 : 0); - markerFrequencybaseLabel->setVisible(enabled); - updateMarkerDetails(); - - repaint(); -} - -/// \brief Prints analyzed data. -void DsoWidget::showNew(std::shared_ptr data) { - mainScope->showData(data); - zoomScope->showData(data); - - if (spec->isSoftwareTriggerDevice) { - QPalette triggerLabelPalette = palette(); - triggerLabelPalette.setColor(QPalette::WindowText, Qt::black); - triggerLabelPalette.setColor(QPalette::Background, data->softwareTriggerTriggered ? Qt::green : Qt::red); - swTriggerStatus->setPalette(triggerLabelPalette); - swTriggerStatus->setVisible(true); - } - - updateRecordLength(data.get()->sampleCount()); - - for (ChannelID channel = 0; channel < scope->voltage.size(); ++channel) { - if (scope->voltage[channel].used && data.get()->data(channel)) { - // Amplitude string representation (4 significant digits) - measurementAmplitudeLabel[channel]->setText( - valueToString(data.get()->data(channel)->computeAmplitude(), UNIT_VOLTS, 4)); - // Frequency string representation (5 significant digits) - measurementFrequencyLabel[channel]->setText( - valueToString(data.get()->data(channel)->frequency, UNIT_HERTZ, 5)); - } - } -} - -void DsoWidget::showEvent(QShowEvent *event) { - QWidget::showEvent(event); - // Apply settings and update measured values - updateTriggerDetails(); - updateRecordLength(scope->horizontal.recordLength); - updateFrequencybase(scope->horizontal.frequencybase); - updateSamplerate(scope->horizontal.samplerate); - updateTimebase(scope->horizontal.timebase); - updateZoom(view->zoom); - - updateTriggerSource(); - adaptTriggerPositionSlider(); -} - -/// \brief Handles valueChanged signal from the offset sliders. -/// \param channel The channel whose offset was changed. -/// \param value The new offset for the channel. -void DsoWidget::updateOffset(ChannelID channel, double value) { - if (channel < scope->voltage.size()) { - scope->voltage[channel].offset = value; - - if (channel < spec->channels) { - adaptTriggerLevelSlider(mainSliders, channel); - adaptTriggerLevelSlider(zoomSliders, channel); - } - } else if (channel < scope->voltage.size() * 2) - scope->spectrum[channel - scope->voltage.size()].offset = value; - - if (channel < scope->voltage.size() * 2) { - if (mainSliders.offsetSlider->value(channel) != value) { - QSignalBlocker blocker(mainSliders.offsetSlider); - mainSliders.offsetSlider->setValue(channel, value); - } - if (zoomSliders.offsetSlider->value(channel) != value) { - QSignalBlocker blocker(zoomSliders.offsetSlider); - zoomSliders.offsetSlider->setValue(channel, value); - } - } - - emit offsetChanged(channel, value); -} - -/// \brief Handles signals affecting trigger position in the zoom view. -void DsoWidget::adaptTriggerPositionSlider() { - double m1 = scope->horizontal.marker[0]; - double m2 = scope->horizontal.marker[1]; - - if (m1 > m2) std::swap(m1, m2); - double value = (scope->trigger.position - 0.5) * DIVS_TIME; - if (m1 != m2 && m1 <= value && value <= m2) { - zoomSliders.triggerPositionSlider->setIndexVisible(0, true); - zoomSliders.triggerPositionSlider->setValue(0, (value - m1) / (m2 - m1)); - } else { - zoomSliders.triggerPositionSlider->setIndexVisible(0, false); - } -} - -/// \brief Handles valueChanged signal from the triggerPosition slider. -/// \param index The index of the slider. -/// \param value The new triggerPosition in seconds relative to the first -/// sample. -void DsoWidget::updateTriggerPosition(int index, double value) { - if (index != 0) return; - - scope->trigger.position = value; - adaptTriggerPositionSlider(); - - updateTriggerDetails(); - updateMarkerDetails(); - - emit triggerPositionChanged(value * scope->horizontal.timebase * DIVS_TIME); -} - -/// \brief Handles valueChanged signal from the trigger level slider. -/// \param channel The index of the slider. -/// \param value The new trigger level. -void DsoWidget::updateTriggerLevel(ChannelID channel, double value) { - scope->voltage[channel].trigger = value; - - if (mainSliders.triggerLevelSlider->value(channel) != value) { - QSignalBlocker blocker(mainSliders.triggerLevelSlider); - mainSliders.triggerLevelSlider->setValue(channel, value); - } - if (zoomSliders.triggerLevelSlider->value(channel) != value) { - QSignalBlocker blocker(zoomSliders.triggerLevelSlider); - zoomSliders.triggerLevelSlider->setValue(channel, value); - } - - updateTriggerDetails(); - - emit triggerLevelChanged(channel, value); -} - -/// \brief Handles valueChanged signal from the marker slider. -/// \param marker The index of the slider. -/// \param value The new marker position. -void DsoWidget::updateMarker(int marker, double value) { - scope->horizontal.marker[marker] = value; - - adaptTriggerPositionSlider(); - updateMarkerDetails(); - - emit markerChanged(marker, value); -} diff --git a/openhantek/src/dsowidget.h b/openhantek/src/dsowidget.h deleted file mode 100644 index 6b63b93b..00000000 --- a/openhantek/src/dsowidget.h +++ /dev/null @@ -1,130 +0,0 @@ -// SPDX-License-Identifier: GPL-2.0+ - -#pragma once - -#include -#include -#include -#include -#include - -#include "glscope.h" -#include "levelslider.h" -#include "hantekdso/controlspecification.h" - -class SpectrumGenerator; -struct DsoSettingsScope; -struct DsoSettingsView; - -/// \brief The widget for the oszilloscope-screen -/// This widget contains the scopes and all level sliders. -class DsoWidget : public QWidget { - Q_OBJECT - - struct Sliders { - LevelSlider *offsetSlider; ///< The sliders for the graph offsets - LevelSlider *triggerPositionSlider; ///< The slider for the pretrigger - LevelSlider *triggerLevelSlider; ///< The sliders for the trigger level - LevelSlider *markerSlider; ///< The sliders for the markers - }; - - public: - /// \brief Initializes the components of the oszilloscope-screen. - /// \param settings The settings object containing the oscilloscope settings. - /// \param dataAnalyzer The data analyzer that should be used as data source. - /// \param parent The parent widget. - /// \param flags Flags for the window manager. - DsoWidget(DsoSettingsScope* scope, DsoSettingsView* view, const Dso::ControlSpecification* spec, QWidget *parent = 0, Qt::WindowFlags flags = 0); - - // Data arrived - void showNew(std::shared_ptr data); - - protected: - virtual void showEvent(QShowEvent *event); - void setupSliders(Sliders &sliders); - void adaptTriggerLevelSlider(DsoWidget::Sliders &sliders, ChannelID channel); - void adaptTriggerPositionSlider(); - void setMeasurementVisible(ChannelID channel); - void updateMarkerDetails(); - void updateSpectrumDetails(ChannelID channel); - void updateTriggerDetails(); - void updateVoltageDetails(ChannelID channel); - - Sliders mainSliders; - Sliders zoomSliders; - - QGridLayout *mainLayout; ///< The main layout for this widget - - QHBoxLayout *settingsLayout; ///< The table for the settings info - QLabel *settingsTriggerLabel; ///< The trigger details - QLabel *settingsRecordLengthLabel; ///< The record length - QLabel *settingsSamplerateLabel; ///< The samplerate - QLabel *settingsTimebaseLabel; ///< The timebase of the main scope - QLabel *settingsFrequencybaseLabel; ///< The frequencybase of the main scope - - QLabel *swTriggerStatus; ///< The status of SW trigger - - QHBoxLayout *markerLayout; ///< The table for the marker details - QLabel *markerInfoLabel; ///< The info about the zoom factor - QLabel *markerTimeLabel; ///< The time period between the markers - QLabel *markerFrequencyLabel; ///< The frequency for the time period - QLabel *markerTimebaseLabel; ///< The timebase for the zoomed scope - QLabel *markerFrequencybaseLabel; ///< The frequencybase for the zoomed scope - - QGridLayout *measurementLayout; ///< The table for the signal details - std::vector measurementNameLabel; ///< The name of the channel - std::vector measurementGainLabel; ///< The gain for the voltage (V/div) - std::vector measurementMagnitudeLabel; ///< The magnitude for the spectrum (dB/div) - std::vector measurementMiscLabel; ///< Coupling or math mode - std::vector measurementAmplitudeLabel; ///< Amplitude of the signal (V) - std::vector measurementFrequencyLabel; ///< Frequency of the signal (Hz) - - DsoSettingsScope* scope; - DsoSettingsView* view; - const Dso::ControlSpecification* spec; - - GlScope *mainScope; ///< The main scope screen - GlScope *zoomScope; ///< The optional magnified scope screen - - public slots: - // Horizontal axis - // void horizontalFormatChanged(HorizontalFormat format); - void updateFrequencybase(double frequencybase); - void updateSamplerate(double samplerate); - void updateTimebase(double timebase); - - // Trigger - void updateTriggerMode(); - void updateTriggerSlope(); - void updateTriggerSource(); - - // Spectrum - void updateSpectrumMagnitude(ChannelID channel); - void updateSpectrumUsed(ChannelID channel, bool used); - - // Vertical axis - void updateVoltageCoupling(ChannelID channel); - void updateMathMode(); - void updateVoltageGain(ChannelID channel); - void updateVoltageUsed(ChannelID channel, bool used); - - // Menus - void updateRecordLength(unsigned long size); - - // Scope control - void updateZoom(bool enabled); - - private slots: - // Sliders - void updateOffset(ChannelID channel, double value); - void updateTriggerPosition(int index, double value); - void updateTriggerLevel(ChannelID channel, double value); - void updateMarker(int marker, double value); - - signals: - // Sliders - void offsetChanged(ChannelID channel, double value); ///< A graph offset has been changed - void triggerPositionChanged(double value); ///< The pretrigger has been changed - void triggerLevelChanged(ChannelID channel, double value); ///< A trigger level has been changed - void markerChanged(unsigned int marker, double value); ///< A marker position has been changed -}; diff --git a/openhantek/src/exporting/exportcsv.cpp b/openhantek/src/exporting/exportcsv.cpp index d543bbf6..5807b6b9 100644 --- a/openhantek/src/exporting/exportcsv.cpp +++ b/openhantek/src/exporting/exportcsv.cpp @@ -2,31 +2,30 @@ #include "exportcsv.h" #include "exporterregistry.h" +#include "iconfont/QtAwesome.h" #include "post/ppresult.h" #include "settings.h" -#include "iconfont/QtAwesome.h" #include -#include #include #include +#include -ExporterCSV::ExporterCSV() {} +namespace Exporter { +CSV::CSV() {} -void ExporterCSV::create(ExporterRegistry *registry) { this->registry = registry; data.reset(); } +QIcon CSV::icon() { return iconFont->icon(fa::filetexto); } -QIcon ExporterCSV::icon() { return iconFont->icon(fa::filetexto); } +QString CSV::name() { return QCoreApplication::tr("Export CSV"); } -QString ExporterCSV::name() { return QCoreApplication::tr("Export CSV"); } +ExporterInterface::Type CSV::type() { return Type::SnapshotExport; } -ExporterInterface::Type ExporterCSV::type() { return Type::SnapshotExport; } +float CSV::samples(const std::shared_ptr) { return 1; } -bool ExporterCSV::samples(const std::shared_ptr data) { - this->data = std::move(data); - return false; -} +bool CSV::exportNow(Registry *registry) { + auto data = registry->lastDataSet(); + if (!data) return false; -bool ExporterCSV::save() { QStringList filters; filters << QCoreApplication::tr("Comma-Separated Values (*.csv)"); @@ -42,63 +41,49 @@ bool ExporterCSV::save() { csvStream.setRealNumberNotation(QTextStream::FixedNotation); csvStream.setRealNumberPrecision(10); - size_t chCount = registry->settings->scope.voltage.size(); - std::vector voltageData(size_t(chCount), nullptr); - std::vector spectrumData(size_t(chCount), nullptr); + std::map *> voltageData; + std::map *> spectrumData; size_t maxRow = 0; - bool isSpectrumUsed = false; double timeInterval = 0; double freqInterval = 0; - for (ChannelID channel = 0; channel < chCount; ++channel) { - if (data->data(channel)) { - if (registry->settings->scope.voltage[channel].used) { - voltageData[channel] = &(data->data(channel)->voltage); - maxRow = std::max(maxRow, voltageData[channel]->sample.size()); - timeInterval = data->data(channel)->voltage.interval; - } - if (registry->settings->scope.spectrum[channel].used) { - spectrumData[channel] = &(data->data(channel)->spectrum); - maxRow = std::max(maxRow, spectrumData[channel]->sample.size()); - freqInterval = data->data(channel)->spectrum.interval; - isSpectrumUsed = true; - } + const Settings::Scope *scope = ®istry->settings->scope; + for (const Settings::Channel *c : *scope) { + const DataChannel *dataChannel = data->data(c->channelID()); + if (!dataChannel) continue; + if (c->visible()) { + const std::vector *samples = &(dataChannel->voltage.sample); + voltageData[c->channelID()] = samples; + maxRow = std::max(maxRow, samples->size()); + timeInterval = dataChannel->voltage.interval; + } + if (c->spectrum()->visible()) { + const std::vector *samples = &(dataChannel->spectrum.sample); + spectrumData[c->channelID()] = samples; + maxRow = std::max(maxRow, samples->size()); + freqInterval = dataChannel->spectrum.interval; } } // Start with channel names csvStream << "\"t\""; - for (ChannelID channel = 0; channel < chCount; ++channel) { - if (voltageData[channel] != nullptr) { csvStream << ",\"" << registry->settings->scope.voltage[channel].name << "\""; } - } - if (isSpectrumUsed) { - csvStream << ",\"f\""; - for (ChannelID channel = 0; channel < chCount; ++channel) { - if (spectrumData[channel] != nullptr) { - csvStream << ",\"" << registry->settings->scope.spectrum[channel].name << "\""; - } - } - } + for (auto &c : voltageData) { csvStream << ",\"" << scope->channel(c.first)->name() << "\""; } + csvStream << ",\"f\""; + for (auto &c : spectrumData) { csvStream << ",\"" << scope->channel(c.first)->name() << "\""; } csvStream << "\n"; for (unsigned int row = 0; row < maxRow; ++row) { csvStream << timeInterval * row; - for (ChannelID channel = 0; channel < chCount; ++channel) { - if (voltageData[channel] != nullptr) { - csvStream << ","; - if (row < voltageData[channel]->sample.size()) { csvStream << voltageData[channel]->sample[row]; } - } + for (auto &c : voltageData) { + csvStream << ","; + if (row < c.second->size()) { csvStream << (*c.second)[row]; } } - if (isSpectrumUsed) { - csvStream << "," << freqInterval * row; - for (ChannelID channel = 0; channel < chCount; ++channel) { - if (spectrumData[channel] != nullptr) { - csvStream << ","; - if (row < spectrumData[channel]->sample.size()) { csvStream << spectrumData[channel]->sample[row]; } - } - } + csvStream << "," << freqInterval * row; + for (auto &c : spectrumData) { + csvStream << ","; + if (row < c.second->size()) { csvStream << (*c.second)[row]; } } csvStream << "\n"; } @@ -108,4 +93,5 @@ bool ExporterCSV::save() { return true; } -float ExporterCSV::progress() { return data ? 1.0f : 0; } +QKeySequence CSV::shortcut() { return QKeySequence(); } +} diff --git a/openhantek/src/exporting/exportcsv.h b/openhantek/src/exporting/exportcsv.h index 09a2278c..7c15a091 100644 --- a/openhantek/src/exporting/exportcsv.h +++ b/openhantek/src/exporting/exportcsv.h @@ -3,17 +3,15 @@ #pragma once #include "exporterinterface.h" -class ExporterCSV : public ExporterInterface -{ -public: - ExporterCSV(); - virtual void create(ExporterRegistry *registry) override; +namespace Exporter { +class CSV : public ExporterInterface { + public: + CSV(); + virtual bool exportNow(Registry *registry) override; virtual QIcon icon() override; virtual QString name() override; virtual Type type() override; - virtual bool samples(const std::shared_ptrdata) override; - virtual bool save() override; - virtual float progress() override; -private: - std::shared_ptr data; + virtual float samples(const std::shared_ptr data) override; + virtual QKeySequence shortcut() override; }; +} diff --git a/openhantek/src/exporting/exporterinterface.h b/openhantek/src/exporting/exporterinterface.h index 2720d102..f777ca70 100644 --- a/openhantek/src/exporting/exporterinterface.h +++ b/openhantek/src/exporting/exporterinterface.h @@ -7,25 +7,17 @@ #include -class ExporterRegistry; class PPresult; +namespace Exporter { +class Registry; + /** * Implement this interface and register your Exporter to the ExporterRegistry instance * in the main routine to make an Exporter available. */ class ExporterInterface { -public: - - /** - * Starts up this exporter. Aquires resources etc. Do not call this directly, it - * will be called by the exporter registry at some point. Release your resources in the - * destructor as usual. - * @param registry The exporter registry instance. This is used to obtain a reference - * to the settings. - */ - virtual void create(ExporterRegistry *registry) = 0; - + public: /** * @return Return the icon representation of this exporter. Will be used in graphical * interfaces. @@ -38,6 +30,12 @@ class ExporterInterface { */ virtual QString name() = 0; + /** + * @return Return a shortcut for this exporter. Will be used in graphical + * interfaces. + */ + virtual QKeySequence shortcut() = 0; + /** * Exporters can save only a single sample set or save data continously. */ @@ -50,29 +48,37 @@ class ExporterInterface { virtual Type type() = 0; /** - * A new sample set from the ExporterRegistry. The exporter needs to be active to receive samples. - * If it is a snapshot exporter, only one set of samples will be received. - * @return Return true if you want to receive another sample or false if you are done (progres()==1). + * A new sample set arrived at the ExporterRegistry. The exporter needs to be a continous exporter: See type(). + * This method is called in the thread context of the ExporterRegistry. + * + * \return Report the used memory / reservered memory ratio here. If float>=1.0 then this exporter will be + * deactivated again and will not receive anymore sample data. */ - virtual bool samples(const std::shared_ptr) = 0; + virtual float samples(const std::shared_ptr) = 0; /** - * Exporter: Save your received data and perform any conversions necessary. - * This method will be called in the - * GUI thread context and can create and show dialogs if required. - * @return Return true if saving succedded otherwise false. + * Start the export process. If you are a snapshot exporter, now is the time to access the last sampleset + * from the ExporterRegistry, convert it, ask for a filename and save the data. + * + * If you are a continous exporter, you should show a dialog to the user to inform about the progress and provide + * a cancel option. + * + * This method will be called in the GUI thread context and can create and show dialogs if required. + * + * @param registry The exporter registry instance. This is used to obtain a reference + * to the settings and sample data. + * @return Return true if saving or setting everything up succedded otherwise false. If this is a continous + * exporter and false is returned, then no sample data will be send via samples(...). */ - virtual bool save() = 0; + virtual bool exportNow(Registry *registry) = 0; /** - * @brief The progress of receiving and processing samples. If the exporter returns 1, it will - * be called back by the GUI via the save() method. - * - * @return A number between 0..1 indicating the used capacity of this exporter. If this is a - * snapshot exporter, only 0 for "no samples processed yet" or 1 for "finished" will be returned. - * A continous exporter may report the used memory / reservered memory ratio here. + * Implement this if you are a continous exporter and want the user to be able to stop the export process + * via export checkbox in the main window. */ - virtual float progress() = 0; -protected: - ExporterRegistry *registry; + virtual void stopContinous() {} + + protected: + Registry *registry; }; +} diff --git a/openhantek/src/exporting/exporterprocessor.cpp b/openhantek/src/exporting/exporterprocessor.cpp deleted file mode 100644 index 7e435500..00000000 --- a/openhantek/src/exporting/exporterprocessor.cpp +++ /dev/null @@ -1,6 +0,0 @@ -#include "exporterprocessor.h" -#include "exporterregistry.h" - -ExporterProcessor::ExporterProcessor(ExporterRegistry *registry) : registry(registry) {} - -void ExporterProcessor::process(PPresult *data) { registry->addRawSamples(data); } diff --git a/openhantek/src/exporting/exporterprocessor.h b/openhantek/src/exporting/exporterprocessor.h deleted file mode 100644 index a920ac9f..00000000 --- a/openhantek/src/exporting/exporterprocessor.h +++ /dev/null @@ -1,14 +0,0 @@ -#pragma once - -#include "post/processor.h" - -class ExporterRegistry; - -class ExporterProcessor : public Processor -{ -public: - ExporterProcessor(ExporterRegistry* registry); - virtual void process(PPresult *) override; -private: - ExporterRegistry* registry; -}; diff --git a/openhantek/src/exporting/exporterregistry.cpp b/openhantek/src/exporting/exporterregistry.cpp index 249eba36..f209d619 100644 --- a/openhantek/src/exporting/exporterregistry.cpp +++ b/openhantek/src/exporting/exporterregistry.cpp @@ -4,81 +4,54 @@ #include "exporterinterface.h" #include +#include -#include "controlspecification.h" +#include "hantekdso/modelspecification.h" #include "post/ppresult.h" #include "settings.h" -ExporterRegistry::ExporterRegistry(const Dso::ControlSpecification *deviceSpecification, DsoSettings *settings, - QObject *parent) - : QObject(parent), deviceSpecification(deviceSpecification), settings(settings) {} - -bool ExporterRegistry::processData(std::shared_ptr &data, ExporterInterface *const &exporter) { - if (!exporter->samples(data)) { - waitToSaveExporters.insert(exporter); - emit exporterProgressChanged(); - return true; +template +void discard_if(std::set &c, Predicate pred) { + for (auto it{c.begin()}, end{c.end()}; it != end;) { + if (pred(*it)) { + it = c.erase(it); + } else { + ++it; + } } - return false; -} - -void ExporterRegistry::addRawSamples(PPresult *d) { - if (settings->exporting.useProcessedSamples) return; - std::shared_ptr data(d); - enabledExporters.remove_if([&data, this](ExporterInterface *const &i) { return processData(data, i); }); } -void ExporterRegistry::input(std::shared_ptr data) { - if (!settings->exporting.useProcessedSamples) return; - enabledExporters.remove_if([&data, this](ExporterInterface *const &i) { return processData(data, i); }); +namespace Exporter { +Registry::Registry(const Dso::ModelSpec *deviceSpecification, Settings::DsoSettings *settings, QObject *parent) + : QObject(parent), deviceSpecification(deviceSpecification), settings(settings) { } -void ExporterRegistry::registerExporter(ExporterInterface *exporter) { - exporters.push_back(exporter); - exporter->create(this); +void Registry::input(std::shared_ptr data) { + m_lastDataset = data; + discard_if(continousActiveExporters, + [&data, this](ExporterInterface *const &i) { return i->samples(data) >= 1.0; }); } -void ExporterRegistry::setExporterEnabled(ExporterInterface *exporter, bool enabled) { - bool wasInList = false; - enabledExporters.remove_if([exporter, &wasInList](ExporterInterface *inlist) { - if (inlist == exporter) { - wasInList = true; - return true; - } else - return false; - }); +void Registry::registerExporter(ExporterInterface *exporter) { exporters.push_back(exporter); } - if (enabled) { - // If the exporter was waiting for the user save confirmation, - // reset it instead. - auto localFind = waitToSaveExporters.find(exporter); - if (localFind != waitToSaveExporters.end()) { - waitToSaveExporters.erase(localFind); - exporter->create(this); - } - enabledExporters.push_back(exporter); - } else if (wasInList) { - // If exporter made some progress: Add to waiting for GUI list - if (exporter->progress() > 0) { - waitToSaveExporters.insert(exporter); - emit exporterProgressChanged(); - } else // Reset exporter - exporter->create(this); +void Registry::exportNow(ExporterInterface *exporter) { + if (exporter->exportNow(this) && exporter->type() == ExporterInterface::Type::ContinousExport) { + auto localFind = continousActiveExporters.find(exporter); + if (localFind == continousActiveExporters.end()) { continousActiveExporters.insert(exporter); } } } -void ExporterRegistry::checkForWaitingExporters() { - for (ExporterInterface *exporter : waitToSaveExporters) { - if (exporter->save()) { - emit exporterStatusChanged(exporter->name(), tr("Data saved")); - } else { - emit exporterStatusChanged(exporter->name(), tr("No data exported")); +void Registry::stopContinous(ExporterInterface *exporter) { + if (exporter->type() == ExporterInterface::Type::ContinousExport) { + auto localFind = continousActiveExporters.find(exporter); + if (localFind != continousActiveExporters.end()) { + continousActiveExporters.erase(localFind); + exporter->stopContinous(); } - exporter->create(this); } - waitToSaveExporters.clear(); } -std::vector::const_iterator ExporterRegistry::begin() { return exporters.begin(); } +std::vector::const_iterator Registry::begin() { return exporters.begin(); } -std::vector::const_iterator ExporterRegistry::end() { return exporters.end(); } +std::vector::const_iterator Registry::end() { return exporters.end(); } +} diff --git a/openhantek/src/exporting/exporterregistry.h b/openhantek/src/exporting/exporterregistry.h index fe9fae14..ec3c9969 100644 --- a/openhantek/src/exporting/exporterregistry.h +++ b/openhantek/src/exporting/exporterregistry.h @@ -12,53 +12,54 @@ class Processor; class PPresult; // Settings forwards +namespace Settings { class DsoSettings; +} namespace Dso { -struct ControlSpecification; +struct ModelSpec; } -// Exporter forwards +namespace Exporter { class ExporterInterface; -class ExporterRegistry : public QObject { +class Registry : public QObject { Q_OBJECT public: - explicit ExporterRegistry(const Dso::ControlSpecification *deviceSpecification, DsoSettings *settings, - QObject *parent = nullptr); + explicit Registry(const Dso::ModelSpec *deviceSpecification, Settings::DsoSettings *settings, + QObject *parent = nullptr); // Sample input. This will proably be performed in the post processing // thread context. Do not open GUI dialogs or interrupt the control flow. - void addRawSamples(PPresult *data); void input(std::shared_ptr data); void registerExporter(ExporterInterface *exporter); - void setExporterEnabled(ExporterInterface *exporter, bool enabled); - void checkForWaitingExporters(); + /** + * Called from the GUI thread to start an export process. + * @param exporter The exporter to use. + */ + void exportNow(ExporterInterface *exporter); + + void stopContinous(ExporterInterface *exporter); + + inline std::shared_ptr lastDataSet() const { return m_lastDataset; } // Iterate over this class object std::vector::const_iterator begin(); std::vector::const_iterator end(); /// Device specifications - const Dso::ControlSpecification *deviceSpecification; - const DsoSettings *settings; + const Dso::ModelSpec *deviceSpecification; + const Settings::DsoSettings *settings; private: /// List of all available exporters std::vector exporters; /// List of exporters that collect samples at the moment - std::list enabledExporters; - /// List of exporters that wait to be called back by the user to save their work - std::set waitToSaveExporters; + std::set continousActiveExporters; - /// Process data from addRawSamples() or input() in the given exporter. Add the - /// exporter to waitToSaveExporters if it finishes. - /// - /// @return Return true if the exporter has finished and want to be removed from the - /// enabledExporters list. - bool processData(std::shared_ptr &data, ExporterInterface *const &exporter); + std::shared_ptr m_lastDataset; signals: void exporterStatusChanged(const QString &exporterName, const QString &status); - void exporterProgressChanged(); }; +} diff --git a/openhantek/src/exporting/exportimage.cpp b/openhantek/src/exporting/exportimage.cpp index 9139251d..e175bde9 100644 --- a/openhantek/src/exporting/exportimage.cpp +++ b/openhantek/src/exporting/exportimage.cpp @@ -9,61 +9,62 @@ #include "viewsettings.h" #include +#include #include #include +namespace Exporter { +Image::Image() {} -ExporterImage::ExporterImage() {} +QIcon Image::icon() { return iconFont->icon(fa::image); } -void ExporterImage::create(ExporterRegistry *registry) { this->registry = registry; data.reset(); } +QString Image::name() { return QCoreApplication::tr("Export Image/PDF"); } -QIcon ExporterImage::icon() { return iconFont->icon(fa::image); } +ExporterInterface::Type Image::type() { return Type::SnapshotExport; } -QString ExporterImage::name() { return QCoreApplication::tr("Export Image/PDF"); } +float Image::samples(const std::shared_ptr) { return 1; } -ExporterInterface::Type ExporterImage::type() { return Type::SnapshotExport; } - -bool ExporterImage::samples(const std::shared_ptr data) { - this->data = std::move(data); - return false; -} - -bool ExporterImage::save() { +bool Image::exportNow(Registry *registry) { + auto data = registry->lastDataSet(); + if (!data) return false; QStringList filters; - filters << QCoreApplication::tr("Portable Document Format (*.pdf)") - << QCoreApplication::tr("Image (*.png *.xpm *.jpg)"); + filters << QCoreApplication::tr("Image (*.png *.xpm *.jpg *.bmp)") + << QCoreApplication::tr("Portable Document Format (*.pdf)"); QFileDialog fileDialog(nullptr, QCoreApplication::tr("Export file..."), QString(), filters.join(";;")); + fileDialog.selectFile(QDateTime::currentDateTime().toString(Qt::ISODate) + ".png"); fileDialog.setFileMode(QFileDialog::AnyFile); fileDialog.setAcceptMode(QFileDialog::AcceptSave); if (fileDialog.exec() != QDialog::Accepted) return false; - bool isPdf = filters.indexOf(fileDialog.selectedNameFilter()) == 0; + bool isPdf = filters.indexOf(fileDialog.selectedNameFilter()) == 1; + const QString filename = fileDialog.selectedFiles().first(); std::unique_ptr paintDevice; - const DsoSettingsColorValues *colorValues = &(registry->settings->view.print); + const Settings::Colors *colorValues = &(registry->settings->view.print); if (!isPdf) { // We need a QPixmap for image-export QPixmap *qPixmap = new QPixmap(registry->settings->exporting.imageSize); - qPixmap->fill(colorValues->background); + qPixmap->fill(colorValues->background()); paintDevice = std::unique_ptr(qPixmap); } else { // We need a QPrinter for printing, pdf- and ps-export std::unique_ptr printer = std::unique_ptr(new QPrinter(QPrinter::HighResolution)); - printer->setOrientation(registry->settings->view.zoom ? QPrinter::Portrait : QPrinter::Landscape); + printer->setOrientation(QPrinter::Landscape); printer->setPageMargins(20, 20, 20, 20, QPrinter::Millimeter); - printer->setOutputFileName(fileDialog.selectedFiles().first()); + printer->setOutputFileName(filename); printer->setOutputFormat(QPrinter::PdfFormat); paintDevice = std::move(printer); } if (!paintDevice) return false; - LegacyExportDrawer::exportSamples(data.get(), paintDevice.get(), registry->deviceSpecification, registry->settings, - false, colorValues); + LegacyExportDrawer::exportSamples(data, paintDevice.get(), registry->deviceSpecification, registry->settings, + colorValues); - if (!isPdf) static_cast(paintDevice.get())->save(fileDialog.selectedFiles().first()); + if (!isPdf) static_cast(paintDevice.get())->save(filename); return true; } -float ExporterImage::progress() { return data ? 1.0f : 0; } +QKeySequence Image::shortcut() { return QKeySequence(Qt::CTRL | Qt::Key_E); } +} diff --git a/openhantek/src/exporting/exportimage.h b/openhantek/src/exporting/exportimage.h index b8dd2f26..ef1003a0 100644 --- a/openhantek/src/exporting/exportimage.h +++ b/openhantek/src/exporting/exportimage.h @@ -3,17 +3,15 @@ #pragma once #include "exporterinterface.h" -class ExporterImage : public ExporterInterface -{ -public: - ExporterImage(); - virtual void create(ExporterRegistry *registry) override; +namespace Exporter { +class Image : public ExporterInterface { + public: + Image(); + virtual bool exportNow(Registry *registry) override; virtual QIcon icon() override; virtual QString name() override; virtual Type type() override; - virtual bool samples(const std::shared_ptrdata) override; - virtual bool save() override; - virtual float progress() override; -private: - std::shared_ptr data; + virtual float samples(const std::shared_ptr data) override; + virtual QKeySequence shortcut() override; }; +} diff --git a/openhantek/src/exporting/exportprint.cpp b/openhantek/src/exporting/exportprint.cpp index 4b4b81b7..eb421c15 100644 --- a/openhantek/src/exporting/exportprint.cpp +++ b/openhantek/src/exporting/exportprint.cpp @@ -12,25 +12,23 @@ #include #include -ExporterPrint::ExporterPrint() {} +namespace Exporter { +Print::Print() {} -void ExporterPrint::create(ExporterRegistry *registry) { this->registry = registry; data.reset(); } +QIcon Print::icon() { return iconFont->icon(fa::print); } -QIcon ExporterPrint::icon() { return iconFont->icon(fa::print); } +QString Print::name() { return QCoreApplication::tr("Print"); } -QString ExporterPrint::name() { return QCoreApplication::tr("Print"); } +ExporterInterface::Type Print::type() { return Type::SnapshotExport; } -ExporterInterface::Type ExporterPrint::type() { return Type::SnapshotExport; } +float Print::samples(const std::shared_ptr) { return 1; } -bool ExporterPrint::samples(const std::shared_ptr data) { - this->data = std::move(data); - return false; -} - -bool ExporterPrint::save() { +bool Print::exportNow(Registry *registry) { + auto data = registry->lastDataSet(); + if (!data) return false; // We need a QPrinter for printing, pdf- and ps-export std::unique_ptr printer = std::unique_ptr(new QPrinter(QPrinter::HighResolution)); - printer->setOrientation(registry->settings->view.zoom ? QPrinter::Portrait : QPrinter::Landscape); + printer->setOrientation(QPrinter::Landscape); printer->setPageMargins(20, 20, 20, 20, QPrinter::Millimeter); // Show the printing dialog @@ -38,12 +36,13 @@ bool ExporterPrint::save() { dialog.setWindowTitle(QCoreApplication::tr("Print oscillograph")); if (dialog.exec() != QDialog::Accepted) { return false; } - const DsoSettingsColorValues *colorValues = &(registry->settings->view.print); + const Settings::Colors *colorValues = &(registry->settings->view.print); - LegacyExportDrawer::exportSamples(data.get(), printer.get(), registry->deviceSpecification, registry->settings, - true, colorValues); + LegacyExportDrawer::exportSamples(data, printer.get(), registry->deviceSpecification, registry->settings, + colorValues); return true; } -float ExporterPrint::progress() { return data ? 1.0f : 0; } +QKeySequence Print::shortcut() { return QKeySequence(Qt::CTRL | Qt::Key_P); } +} diff --git a/openhantek/src/exporting/exportprint.h b/openhantek/src/exporting/exportprint.h index 51fcd135..1a967e3e 100644 --- a/openhantek/src/exporting/exportprint.h +++ b/openhantek/src/exporting/exportprint.h @@ -3,17 +3,15 @@ #pragma once #include "exporterinterface.h" -class ExporterPrint : public ExporterInterface -{ -public: - ExporterPrint(); - virtual void create(ExporterRegistry *registry) override; +namespace Exporter { +class Print : public ExporterInterface { + public: + Print(); + virtual bool exportNow(Registry *registry) override; virtual QIcon icon() override; virtual QString name() override; virtual Type type() override; - virtual bool samples(const std::shared_ptrdata) override; - virtual bool save() override; - virtual float progress() override; -private: - std::shared_ptr data; + virtual float samples(const std::shared_ptr data) override; + virtual QKeySequence shortcut() override; }; +} diff --git a/openhantek/src/exporting/exportsettings.cpp b/openhantek/src/exporting/exportsettings.cpp new file mode 100644 index 00000000..6e73df74 --- /dev/null +++ b/openhantek/src/exporting/exportsettings.cpp @@ -0,0 +1,22 @@ +// SPDX-License-Identifier: GPL-2.0+ + +#include +#include +#include +#include + +#include "exportsettings.h" + +void Settings::DsoExportIO::read(QSettings *store, DsoExport &exporting) { + store->beginGroup("exporting"); + if (store->contains("imageSize")) exporting.imageSize = store->value("imageSize").toSize(); + if (store->contains("exportSizeBytes")) exporting.exportSizeBytes = store->value("exportSizeBytes").toUInt(); + store->endGroup(); +} + +void Settings::DsoExportIO::write(QSettings *store, const DsoExport &exporting) { + store->beginGroup("exporting"); + store->setValue("imageSize", exporting.imageSize); + store->setValue("exportSizeBytes", exporting.exportSizeBytes); + store->endGroup(); +} diff --git a/openhantek/src/exporting/exportsettings.h b/openhantek/src/exporting/exportsettings.h index 996873ed..0496cc23 100644 --- a/openhantek/src/exporting/exportsettings.h +++ b/openhantek/src/exporting/exportsettings.h @@ -3,10 +3,17 @@ #pragma once #include +class QSettings; +namespace Settings { /// \brief Holds the export options of the program. -struct DsoSettingsExport { - QSize imageSize = QSize(640, 480); ///< Size of exported images in pixels - unsigned exportSizeBytes = 1024*1024*10; ///< For exporters that save a continous stream. Default: 10 Megabytes - bool useProcessedSamples = true; ///< Export raw or processed samples +struct DsoExport { + QSize imageSize = QSize(640, 480); ///< Size of exported images in pixels + unsigned exportSizeBytes = 1024 * 1024 * 10; ///< For exporters that save a continous stream. Default: 10 Megabytes }; + +struct DsoExportIO { + static void read(QSettings *io, DsoExport &exportStruct); + static void write(QSettings *io, const DsoExport &exportStruct); +}; +} diff --git a/openhantek/src/exporting/legacyexportdrawer.cpp b/openhantek/src/exporting/legacyexportdrawer.cpp index 094619c3..04194e09 100644 --- a/openhantek/src/exporting/legacyexportdrawer.cpp +++ b/openhantek/src/exporting/legacyexportdrawer.cpp @@ -5,12 +5,16 @@ #include #include +#include #include #include +#include + +#include "scopeview/glscope.h" #include "legacyexportdrawer.h" -#include "controlspecification.h" +#include "hantekdso/modelspecification.h" #include "post/graphgenerator.h" #include "post/ppresult.h" #include "settings.h" @@ -19,11 +23,39 @@ #define tr(msg) QCoreApplication::translate("Exporter", msg) -bool LegacyExportDrawer::exportSamples(const PPresult *result, QPaintDevice* paintDevice, - const Dso::ControlSpecification *deviceSpecification, - const DsoSettings *settings, bool isPrinter, const DsoSettingsColorValues *colorValues) { +namespace Exporter { +bool LegacyExportDrawer::exportSamples(std::shared_ptr result, QPaintDevice *paintDevice, + const Dso::ModelSpec *deviceSpecification, const Settings::DsoSettings *settings, + const Settings::Colors *colorValues) { // Create a painter for our device - QPainter painter(paintDevice); + QPainter painter; + painter.begin(paintDevice); + + // Draw grid and graphs with GlScope + GlScope *scope = + new GlScope(nullptr, &settings->view, colorValues, QSize(paintDevice->width() - 1, paintDevice->height() - 1)); + scope->initWithoutWindow(); + scope->showData(result); + QEventLoop loop; + QTimer timer; + timer.setSingleShot(true); + QObject::connect(&timer, &QTimer::timeout, &loop, &QEventLoop::quit); + { // Discard first frame + QPointer localCapture = scope->capture(); + QObject::connect(localCapture.data(), &Qt3DRender::QRenderCaptureReply::completed, &loop, + [&loop]() { loop.quit(); }); + timer.start(300); + if (!localCapture->isComplete()) loop.exec(); + } + { + QPointer localCapture = scope->capture(); + QObject::connect(localCapture.data(), &Qt3DRender::QRenderCaptureReply::completed, &loop, + [&loop]() { loop.quit(); }); + timer.start(300); + if (!localCapture->isComplete()) loop.exec(); + painter.drawImage(QPointF(1, 1), localCapture->image()); + } + delete scope; // Get line height QFont font; @@ -33,310 +65,85 @@ bool LegacyExportDrawer::exportSamples(const PPresult *result, QPaintDevice* pai painter.setBrush(Qt::SolidPattern); // Draw the settings table - double stretchBase = (double)(paintDevice->width() - lineHeight * 10) / 4; + double stretchBase = (double)paintDevice->width() / 5; // Print trigger details - painter.setPen(colorValues->voltage[settings->scope.trigger.source]); - QString levelString = valueToString(settings->scope.voltage[settings->scope.trigger.source].trigger, UNIT_VOLTS, 3); - QString pretriggerString = tr("%L1%").arg((int)(settings->scope.trigger.position * 100 + 0.5)); - painter.drawText(QRectF(0, 0, lineHeight * 10, lineHeight), - tr("%1 %2 %3 %4") - .arg(settings->scope.voltage[settings->scope.trigger.source].name, - Dso::slopeString(settings->scope.trigger.slope), levelString, pretriggerString)); - - double scopeHeight; - - { // DataAnalyser mutex lock - // Print sample count - painter.setPen(colorValues->text); - painter.drawText(QRectF(lineHeight * 10, 0, stretchBase, lineHeight), tr("%1 S").arg(result->sampleCount()), - QTextOption(Qt::AlignRight)); - // Print samplerate - painter.drawText(QRectF(lineHeight * 10 + stretchBase, 0, stretchBase, lineHeight), - valueToString(settings->scope.horizontal.samplerate, UNIT_SAMPLES) + tr("/s"), - QTextOption(Qt::AlignRight)); - // Print timebase - painter.drawText(QRectF(lineHeight * 10 + stretchBase * 2, 0, stretchBase, lineHeight), - valueToString(settings->scope.horizontal.timebase, UNIT_SECONDS, 0) + tr("/div"), - QTextOption(Qt::AlignRight)); - // Print frequencybase - painter.drawText(QRectF(lineHeight * 10 + stretchBase * 3, 0, stretchBase, lineHeight), - valueToString(settings->scope.horizontal.frequencybase, UNIT_HERTZ, 0) + tr("/div"), - QTextOption(Qt::AlignRight)); - - // Draw the measurement table - stretchBase = (double)(paintDevice->width() - lineHeight * 6) / 10; - int channelCount = 0; - for (int channel = settings->scope.voltage.size() - 1; channel >= 0; channel--) { - if ((settings->scope.voltage[channel].used || settings->scope.spectrum[channel].used) && - result->data(channel)) { - ++channelCount; - double top = (double)paintDevice->height() - channelCount * lineHeight; - - // Print label - painter.setPen(colorValues->voltage[channel]); - painter.drawText(QRectF(0, top, lineHeight * 4, lineHeight), settings->scope.voltage[channel].name); - // Print coupling/math mode - if ((unsigned int)channel < deviceSpecification->channels) - painter.drawText(QRectF(lineHeight * 4, top, lineHeight * 2, lineHeight), - Dso::couplingString(settings->scope.coupling(channel, deviceSpecification))); - else - painter.drawText( - QRectF(lineHeight * 4, top, lineHeight * 2, lineHeight), - Dso::mathModeString(Dso::getMathMode(settings->scope.voltage[channel]))); + ChannelID triggerSource = settings->deviceSettings->trigger.source(); - // Print voltage gain - painter.drawText(QRectF(lineHeight * 6, top, stretchBase * 2, lineHeight), - valueToString(settings->scope.gain(channel), UNIT_VOLTS, 0) + tr("/div"), - QTextOption(Qt::AlignRight)); - // Print spectrum magnitude - if (settings->scope.spectrum[channel].used) { - painter.setPen(colorValues->spectrum[channel]); - painter.drawText(QRectF(lineHeight * 6 + stretchBase * 2, top, stretchBase * 2, lineHeight), - valueToString(settings->scope.spectrum[channel].magnitude, UNIT_DECIBEL, 0) + - tr("/div"), - QTextOption(Qt::AlignRight)); - } + painter.setPen(colorValues->voltage(triggerSource)); + QString levelString = + valueToString(result->data(triggerSource)->channelSettings->voltage()->triggerLevel(), Unit::VOLTS, 3); + QString pretriggerString = tr("%L1%").arg((int)(settings->deviceSettings->trigger.position() * 100 + 0.5)); - // Amplitude string representation (4 significant digits) - painter.setPen(colorValues->text); - painter.drawText(QRectF(lineHeight * 6 + stretchBase * 4, top, stretchBase * 3, lineHeight), - valueToString(result->data(channel)->computeAmplitude(), UNIT_VOLTS, 4), - QTextOption(Qt::AlignRight)); - // Frequency string representation (5 significant digits) - painter.drawText(QRectF(lineHeight * 6 + stretchBase * 7, top, stretchBase * 3, lineHeight), - valueToString(result->data(channel)->frequency, UNIT_HERTZ, 5), - QTextOption(Qt::AlignRight)); - } - } - - // Draw the marker table - stretchBase = (double)(paintDevice->width() - lineHeight * 10) / 4; - painter.setPen(colorValues->text); - - // Calculate variables needed for zoomed scope - double divs = fabs(settings->scope.horizontal.marker[1] - settings->scope.horizontal.marker[0]); - double time = divs * settings->scope.horizontal.timebase; - double zoomFactor = DIVS_TIME / divs; - double zoomOffset = (settings->scope.horizontal.marker[0] + settings->scope.horizontal.marker[1]) / 2; + // Print sample count + painter.setPen(colorValues->text()); - if (settings->view.zoom) { - scopeHeight = (double)(paintDevice->height() - (channelCount + 5) * lineHeight) / 2; - double top = 2.5 * lineHeight + scopeHeight; + double top = 5; - painter.drawText(QRectF(0, top, stretchBase, lineHeight), - tr("Zoom x%L1").arg(DIVS_TIME / divs, -1, 'g', 3)); - - painter.drawText(QRectF(lineHeight * 10, top, stretchBase, lineHeight), - valueToString(time, UNIT_SECONDS, 4), QTextOption(Qt::AlignRight)); - painter.drawText(QRectF(lineHeight * 10 + stretchBase, top, stretchBase, lineHeight), - valueToString(1.0 / time, UNIT_HERTZ, 4), QTextOption(Qt::AlignRight)); - - painter.drawText(QRectF(lineHeight * 10 + stretchBase * 2, top, stretchBase, lineHeight), - valueToString(time / DIVS_TIME, UNIT_SECONDS, 3) + tr("/div"), - QTextOption(Qt::AlignRight)); - painter.drawText(QRectF(lineHeight * 10 + stretchBase * 3, top, stretchBase, lineHeight), - valueToString(divs * settings->scope.horizontal.frequencybase / DIVS_TIME, UNIT_HERTZ, 3) + - tr("/div"), + painter.drawText(QRectF(0 * stretchBase, top, stretchBase, lineHeight), + tr("%1 %2 %3 %4") + .arg(settings->scope.channel(settings->deviceSettings->trigger.source())->name(), + Dso::slopeString(settings->deviceSettings->trigger.slope()), levelString, + pretriggerString)); + + painter.drawText(QRectF(1 * stretchBase, top, stretchBase, lineHeight), tr("%1 S").arg(result->sampleCount()), + QTextOption(Qt::AlignRight)); + // Print samplerate + painter.drawText(QRectF(2 * stretchBase, top, stretchBase, lineHeight), + valueToString(settings->deviceSettings->samplerate().samplerate, Unit::SAMPLES) + tr("/s"), + QTextOption(Qt::AlignRight)); + // Print timebase + painter.drawText(QRectF(3 * stretchBase, top, stretchBase, lineHeight), + valueToString(settings->deviceSettings->samplerate().timebase, Unit::SECONDS, 0) + tr("/div"), + QTextOption(Qt::AlignRight)); + // Print frequencybase + painter.drawText(QRectF(4 * stretchBase, top, stretchBase, lineHeight), + valueToString(settings->scope.frequencybase(), Unit::HERTZ, 0) + tr("/div"), + QTextOption(Qt::AlignRight)); + + // Draw the measurement table + stretchBase = (double)(paintDevice->width() - lineHeight * 6) / 10; + unsigned channelCount = result->channelCount(); + for (const DataChannel &c : *result) { + const Settings::Channel *channelSettings = c.channelSettings.get(); + ChannelID channel = channelSettings->channelID(); + if (!channelSettings->anyVisible() || !result->data(channel)) continue; + double top = (double)paintDevice->height() - channelCount-- * lineHeight; + + // Print label + painter.setPen(colorValues->voltage(channel)); + painter.drawText(QRectF(2, top, lineHeight * 4, lineHeight), channelSettings->name()); + // Print coupling/math mode + if (!channelSettings->isMathChannel()) + painter.drawText(QRectF(lineHeight * 4, top, lineHeight * 2, lineHeight), + Dso::couplingString(channelSettings->voltage()->coupling(deviceSpecification))); + else + painter.drawText(QRectF(lineHeight * 4, top, lineHeight * 2, lineHeight), + PostProcessing::mathModeString(((Settings::MathChannel *)channelSettings)->mathMode())); + + // Print voltage gain + painter.drawText(QRectF(lineHeight * 6, top, stretchBase * 2, lineHeight), + valueToString(channelSettings->gain(), Unit::VOLTS, 0) + tr("/div"), + QTextOption(Qt::AlignRight)); + // Print spectrum magnitude + if (channelSettings->spectrum()->visible()) { + painter.setPen(colorValues->spectrum(channel)); + painter.drawText(QRectF(lineHeight * 6 + stretchBase * 2, top, stretchBase * 2, lineHeight), + valueToString(channelSettings->spectrum()->magnitude(), Unit::DECIBEL, 0) + tr("/div"), QTextOption(Qt::AlignRight)); - } else { - scopeHeight = (double)paintDevice->height() - (channelCount + 4) * lineHeight; - double top = 2.5 * lineHeight + scopeHeight; - - painter.drawText(QRectF(0, top, stretchBase, lineHeight), tr("Marker 1/2")); - - painter.drawText(QRectF(lineHeight * 10, top, stretchBase * 2, lineHeight), - valueToString(time, UNIT_SECONDS, 4), QTextOption(Qt::AlignRight)); - painter.drawText(QRectF(lineHeight * 10 + stretchBase * 2, top, stretchBase * 2, lineHeight), - valueToString(1.0 / time, UNIT_HERTZ, 4), QTextOption(Qt::AlignRight)); } - // Set DIVS_TIME x DIVS_VOLTAGE matrix for oscillograph - painter.setMatrix(QMatrix((paintDevice->width() - 1) / DIVS_TIME, 0, 0, -(scopeHeight - 1) / DIVS_VOLTAGE, - (double)(paintDevice->width() - 1) / 2, (scopeHeight - 1) / 2 + lineHeight * 1.5), - false); - - // Draw the graphs - painter.setRenderHint(QPainter::Antialiasing); - painter.setBrush(Qt::NoBrush); - - for (int zoomed = 0; zoomed < (settings->view.zoom ? 2 : 1); ++zoomed) { - switch (settings->scope.horizontal.format) { - case Dso::GraphFormat::TY: - // Add graphs for channels - for (ChannelID channel = 0; channel < settings->scope.voltage.size(); ++channel) { - if (settings->scope.voltage[channel].used && result->data(channel)) { - painter.setPen(QPen(colorValues->voltage[channel], 0)); - - // What's the horizontal distance between sampling points? - double horizontalFactor = - result->data(channel)->voltage.interval / settings->scope.horizontal.timebase; - // How many samples are visible? - double centerPosition, centerOffset; - if (zoomed) { - centerPosition = (zoomOffset + DIVS_TIME / 2) / horizontalFactor; - centerOffset = DIVS_TIME / horizontalFactor / zoomFactor / 2; - } else { - centerPosition = DIVS_TIME / 2 / horizontalFactor; - centerOffset = DIVS_TIME / horizontalFactor / 2; - } - unsigned int firstPosition = qMax((int)(centerPosition - centerOffset), 0); - unsigned int lastPosition = qMin((int)(centerPosition + centerOffset), - (int)result->data(channel)->voltage.sample.size() - 1); - - // Draw graph - QPointF *graph = new QPointF[lastPosition - firstPosition + 1]; - - for (unsigned int position = firstPosition; position <= lastPosition; ++position) - graph[position - firstPosition] = QPointF(position * horizontalFactor - DIVS_TIME / 2, - result->data(channel)->voltage.sample[position] / - settings->scope.gain(channel) + - settings->scope.voltage[channel].offset); - - painter.drawPolyline(graph, lastPosition - firstPosition + 1); - delete[] graph; - } - } - - // Add spectrum graphs - for (ChannelID channel = 0; channel < settings->scope.spectrum.size(); ++channel) { - if (settings->scope.spectrum[channel].used && result->data(channel)) { - painter.setPen(QPen(colorValues->spectrum[channel], 0)); - - // What's the horizontal distance between sampling points? - double horizontalFactor = - result->data(channel)->spectrum.interval / settings->scope.horizontal.frequencybase; - // How many samples are visible? - double centerPosition, centerOffset; - if (zoomed) { - centerPosition = (zoomOffset + DIVS_TIME / 2) / horizontalFactor; - centerOffset = DIVS_TIME / horizontalFactor / zoomFactor / 2; - } else { - centerPosition = DIVS_TIME / 2 / horizontalFactor; - centerOffset = DIVS_TIME / horizontalFactor / 2; - } - unsigned int firstPosition = qMax((int)(centerPosition - centerOffset), 0); - unsigned int lastPosition = qMin((int)(centerPosition + centerOffset), - (int)result->data(channel)->spectrum.sample.size() - 1); - - // Draw graph - QPointF *graph = new QPointF[lastPosition - firstPosition + 1]; - - for (unsigned int position = firstPosition; position <= lastPosition; ++position) - graph[position - firstPosition] = - QPointF(position * horizontalFactor - DIVS_TIME / 2, - result->data(channel)->spectrum.sample[position] / - settings->scope.spectrum[channel].magnitude + - settings->scope.spectrum[channel].offset); - - painter.drawPolyline(graph, lastPosition - firstPosition + 1); - delete[] graph; - } - } - break; - - case Dso::GraphFormat::XY: - break; - - default: - break; - } - - // Set DIVS_TIME / zoomFactor x DIVS_VOLTAGE matrix for zoomed - // oscillograph - painter.setMatrix(QMatrix((paintDevice->width() - 1) / DIVS_TIME * zoomFactor, 0, 0, - -(scopeHeight - 1) / DIVS_VOLTAGE, - (double)(paintDevice->width() - 1) / 2 - - zoomOffset * zoomFactor * (paintDevice->width() - 1) / DIVS_TIME, - (scopeHeight - 1) * 1.5 + lineHeight * 4), - false); - } - } // dataanalyser mutex release + // Amplitude string representation (4 significant digits) + painter.setPen(colorValues->text()); + painter.drawText(QRectF(lineHeight * 6 + stretchBase * 4, top, stretchBase * 3, lineHeight), + valueToString(result->data(channel)->amplitude(), Unit::VOLTS, 4), QTextOption(Qt::AlignRight)); + // Frequency string representation (5 significant digits) + painter.drawText(QRectF(lineHeight * 6 + stretchBase * 7, top, stretchBase * 3, lineHeight), + valueToString(result->data(channel)->frequency, Unit::HERTZ, 5), QTextOption(Qt::AlignRight)); + } - drawGrids(painter, colorValues, lineHeight, scopeHeight, paintDevice->width(), - isPrinter, settings->view.zoom); painter.end(); return true; } - -void LegacyExportDrawer::drawGrids(QPainter &painter, const DsoSettingsColorValues *colorValues, double lineHeight, double scopeHeight, - int scopeWidth, bool isPrinter, bool zoom) { - painter.setRenderHint(QPainter::Antialiasing, false); - for (int zoomed = 0; zoomed < (zoom ? 2 : 1); ++zoomed) { - // Set DIVS_TIME x DIVS_VOLTAGE matrix for oscillograph - painter.setMatrix(QMatrix((scopeWidth - 1) / DIVS_TIME, 0, 0, -(scopeHeight - 1) / DIVS_VOLTAGE, - (double)(scopeWidth - 1) / 2, - (scopeHeight - 1) * (zoomed + 0.5) + lineHeight * 1.5 + lineHeight * 2.5 * zoomed), - false); - - // Grid lines - painter.setPen(QPen(colorValues->grid, 0)); - - if (isPrinter) { - // Draw vertical lines - for (int div = 1; div < DIVS_TIME / 2; ++div) { - for (int dot = 1; dot < DIVS_VOLTAGE / 2 * 5; ++dot) { - painter.drawLine(QPointF((double)-div - 0.02, (double)-dot / 5), - QPointF((double)-div + 0.02, (double)-dot / 5)); - painter.drawLine(QPointF((double)-div - 0.02, (double)dot / 5), - QPointF((double)-div + 0.02, (double)dot / 5)); - painter.drawLine(QPointF((double)div - 0.02, (double)-dot / 5), - QPointF((double)div + 0.02, (double)-dot / 5)); - painter.drawLine(QPointF((double)div - 0.02, (double)dot / 5), - QPointF((double)div + 0.02, (double)dot / 5)); - } - } - // Draw horizontal lines - for (int div = 1; div < DIVS_VOLTAGE / 2; ++div) { - for (int dot = 1; dot < DIVS_TIME / 2 * 5; ++dot) { - painter.drawLine(QPointF((double)-dot / 5, (double)-div - 0.02), - QPointF((double)-dot / 5, (double)-div + 0.02)); - painter.drawLine(QPointF((double)dot / 5, (double)-div - 0.02), - QPointF((double)dot / 5, (double)-div + 0.02)); - painter.drawLine(QPointF((double)-dot / 5, (double)div - 0.02), - QPointF((double)-dot / 5, (double)div + 0.02)); - painter.drawLine(QPointF((double)dot / 5, (double)div - 0.02), - QPointF((double)dot / 5, (double)div + 0.02)); - } - } - } else { - // Draw vertical lines - for (int div = 1; div < DIVS_TIME / 2; ++div) { - for (int dot = 1; dot < DIVS_VOLTAGE / 2 * 5; ++dot) { - painter.drawPoint(QPointF(-div, (double)-dot / 5)); - painter.drawPoint(QPointF(-div, (double)dot / 5)); - painter.drawPoint(QPointF(div, (double)-dot / 5)); - painter.drawPoint(QPointF(div, (double)dot / 5)); - } - } - // Draw horizontal lines - for (int div = 1; div < DIVS_VOLTAGE / 2; ++div) { - for (int dot = 1; dot < DIVS_TIME / 2 * 5; ++dot) { - if (dot % 5 == 0) continue; // Already done by vertical lines - painter.drawPoint(QPointF((double)-dot / 5, -div)); - painter.drawPoint(QPointF((double)dot / 5, -div)); - painter.drawPoint(QPointF((double)-dot / 5, div)); - painter.drawPoint(QPointF((double)dot / 5, div)); - } - } - } - - // Axes - painter.setPen(QPen(colorValues->axes, 0)); - painter.drawLine(QPointF(-DIVS_TIME / 2, 0), QPointF(DIVS_TIME / 2, 0)); - painter.drawLine(QPointF(0, -DIVS_VOLTAGE / 2), QPointF(0, DIVS_VOLTAGE / 2)); - for (double div = 0.2; div <= DIVS_TIME / 2; div += 0.2) { - painter.drawLine(QPointF(div, -0.05), QPointF(div, 0.05)); - painter.drawLine(QPointF(-div, -0.05), QPointF(-div, 0.05)); - } - for (double div = 0.2; div <= DIVS_VOLTAGE / 2; div += 0.2) { - painter.drawLine(QPointF(-0.05, div), QPointF(0.05, div)); - painter.drawLine(QPointF(-0.05, -div), QPointF(0.05, -div)); - } - - // Borders - painter.setPen(QPen(colorValues->border, 0)); - painter.drawRect(QRectF(-DIVS_TIME / 2, -DIVS_VOLTAGE / 2, DIVS_TIME, DIVS_VOLTAGE)); - } } diff --git a/openhantek/src/exporting/legacyexportdrawer.h b/openhantek/src/exporting/legacyexportdrawer.h index e9d3d3ce..85ed0be0 100644 --- a/openhantek/src/exporting/legacyexportdrawer.h +++ b/openhantek/src/exporting/legacyexportdrawer.h @@ -2,34 +2,30 @@ #pragma once +#include "exportsettings.h" #include #include #include #include -#include "exportsettings.h" +namespace Settings { class DsoSettings; +struct Colors; +} class PPresult; -struct DsoSettingsColorValues; -namespace Dso { struct ControlSpecification; } +namespace Dso { +struct ModelSpec; +} +namespace Exporter { /// \brief Exports the oscilloscope screen to a file or prints it. -/// TODO -/// Rewrite image exporter with OpenGL drawn grid and graphs -/// -/// Sources: -/// http://doc.qt.io/qt-5/qoffscreensurface.html -/// http://doc.qt.io/qt-5/qopenglframebufferobject.html -/// -/// https://dangelog.wordpress.com/2013/02/10/using-fbos-instead-of-pbuffers-in-qt-5-2/ +/// TODO Grab DsoWidget instead of drawing all labels by hand class LegacyExportDrawer { public: /// Draw the graphs coming from source and labels to the destination paintdevice. - static bool exportSamples(const PPresult *source, QPaintDevice* dest, - const Dso::ControlSpecification* deviceSpecification, - const DsoSettings *settings, bool isPrinter, const DsoSettingsColorValues *colorValues); - - private: - static void drawGrids(QPainter &painter, const DsoSettingsColorValues *colorValues, double lineHeight, double scopeHeight, - int scopeWidth, bool isPrinter, bool zoom); + static bool exportSamples(std::shared_ptr source, QPaintDevice *dest, + const Dso::ModelSpec *deviceSpecification, + const Settings::DsoSettings *settings, + const Settings::Colors *colorValues); }; +} diff --git a/openhantek/src/exporting/readme.md b/openhantek/src/exporting/readme.md index 5c81a388..7c3e4409 100644 --- a/openhantek/src/exporting/readme.md +++ b/openhantek/src/exporting/readme.md @@ -8,9 +8,9 @@ This directory contains exporting functionality and exporters, namely All export classes (exportcsv, exportimage, exportprint) implement the ExporterInterface and are registered to the ExporterRegistry in the main.cpp. -Some export classes are still using the legacyExportDrawer class to -draw the grid and paint all the labels, values and graphs. +At the moment export classes are still using the legacyExportDrawer class to +paint all the labels. It is planned to unify this with the widget based solution of DsoWidget. # Dependency * Files in this directory depend on the result class of the post processing directory. -* Classes in here depend on the user settings (../viewsetting.h, ../scopesetting.h) +* Classes in here depend on the user settings (../settings/) diff --git a/openhantek/src/glscope.cpp b/openhantek/src/glscope.cpp deleted file mode 100644 index e5990d1a..00000000 --- a/openhantek/src/glscope.cpp +++ /dev/null @@ -1,475 +0,0 @@ -// SPDX-License-Identifier: GPL-2.0+ - -#include -#include - -#include -#include -#include -#include -#include -#include -#include -#include - -#include - -#include "glscope.h" - -#include "post/graphgenerator.h" -#include "post/ppresult.h" -#include "scopesettings.h" -#include "viewconstants.h" -#include "viewsettings.h" - -GlScope *GlScope::createNormal(DsoSettingsScope *scope, DsoSettingsView *view, QWidget *parent) { - GlScope *s = new GlScope(scope, view, parent); - s->zoomed = false; - return s; -} - -GlScope *GlScope::createZoomed(DsoSettingsScope *scope, DsoSettingsView *view, QWidget *parent) { - GlScope *s = new GlScope(scope, view, parent); - s->zoomed = true; - return s; -} - -void GlScope::fixOpenGLversion(QSurfaceFormat::RenderableType t) { - QCoreApplication::setAttribute(Qt::AA_ShareOpenGLContexts, true); - - // Prefer full desktop OpenGL without fixed pipeline - QSurfaceFormat format; - format.setSamples(4); // Antia-Aliasing, Multisampling - format.setProfile(QSurfaceFormat::CoreProfile); - if (t==QSurfaceFormat::OpenGLES) { - format.setVersion(2, 0); - format.setRenderableType(QSurfaceFormat::OpenGLES); - QCoreApplication::setAttribute(Qt::AA_UseOpenGLES, true); - } else { - format.setVersion(3, 2); - format.setRenderableType(QSurfaceFormat::OpenGL); - } - QSurfaceFormat::setDefaultFormat(format); -} - -GlScope::GlScope(DsoSettingsScope *scope, DsoSettingsView *view, QWidget *parent) - : QOpenGLWidget(parent), scope(scope), view(view) { - vaMarker.resize(MARKER_COUNT); -} - -GlScope::~GlScope() {/* virtual destructor necessary */} - -void GlScope::mousePressEvent(QMouseEvent *event) { - if (!zoomed && event->button() == Qt::LeftButton) { - double position = (double)(event->x() - width() / 2) * DIVS_TIME / (double)width(); - double distance = DIVS_TIME; - selectedMarker = NO_MARKER; - // Capture nearest marker located within snap area (+/- 1% of full scale). - for (unsigned marker = 0; marker < MARKER_COUNT; ++marker) { - if (!scope->horizontal.marker_visible[marker]) continue; - if (fabs(scope->horizontal.marker[marker] - position) < std::min(distance, DIVS_TIME / 100.0)) { - distance = fabs(scope->horizontal.marker[marker] - position); - selectedMarker = marker; - } - } - if (selectedMarker != NO_MARKER) { emit markerMoved(selectedMarker, position); } - } - event->accept(); -} - -void GlScope::mouseMoveEvent(QMouseEvent *event) { - if (!zoomed && (event->buttons() & Qt::LeftButton) != 0) { - double position = (double)(event->x() - width() / 2) * DIVS_TIME / (double)width(); - if (selectedMarker == NO_MARKER) { - // User started draging outside the snap area of any marker: - // move all markers to current position and select last marker in the array. - for (unsigned marker = 0; marker < MARKER_COUNT; ++marker) { - emit markerMoved(marker, position); - selectedMarker = marker; - } - } else if (selectedMarker < MARKER_COUNT) { - emit markerMoved(selectedMarker, position); - } - } - event->accept(); -} - -void GlScope::mouseReleaseEvent(QMouseEvent *event) { - if (!zoomed && event->button() == Qt::LeftButton) { - double position = (double)(event->x() - width() / 2) * DIVS_TIME / (double)width(); - if (selectedMarker < MARKER_COUNT) { emit markerMoved(selectedMarker, position); } - selectedMarker = NO_MARKER; - } - event->accept(); -} - -void GlScope::paintEvent(QPaintEvent *event) { - // Draw error message if OpenGL failed - if (!shaderCompileSuccess) { - QPainter painter(this); - painter.setRenderHint(QPainter::Antialiasing, true); - QFont font = painter.font(); - font.setPointSize(18); - painter.setFont(font); - painter.drawText(rect(), Qt::AlignCenter, errorMessage); - event->accept(); - } else - QOpenGLWidget::paintEvent(event); -} - -void GlScope::initializeGL() { - if (!QOpenGLShaderProgram::hasOpenGLShaderPrograms(context())) { - errorMessage = tr("System does not support OpenGL Shading Language (GLSL)"); - return; - } - if (m_program) { - qWarning() << "OpenGL init called twice!"; - return; - } - - auto program = std::unique_ptr(new QOpenGLShaderProgram(context())); - - const char *vshaderES = R"( - #version 100 - attribute highp vec3 vertex; - uniform mat4 matrix; - void main() - { - gl_Position = matrix * vec4(vertex, 1.0); - gl_PointSize = 1.0; - } - )"; - const char *fshaderES = R"( - #version 100 - uniform highp vec4 colour; - void main() { gl_FragColor = colour; } - )"; - - const char *vshaderDesktop = R"( - #version 150 - in highp vec3 vertex; - uniform mat4 matrix; - void main() - { - gl_Position = matrix * vec4(vertex, 1.0); - gl_PointSize = 1.0; - } - )"; - const char *fshaderDesktop = R"( - #version 150 - uniform highp vec4 colour; - out vec4 flatColor; - void main() { flatColor = colour; } - )"; - - qDebug() << "compile shaders"; - // Compile vertex shader - bool usesOpenGL = QSurfaceFormat::defaultFormat().renderableType()==QSurfaceFormat::OpenGL; - if (!program->addShaderFromSourceCode(QOpenGLShader::Vertex, usesOpenGL ? vshaderDesktop : vshaderES) || - !program->addShaderFromSourceCode(QOpenGLShader::Fragment, usesOpenGL ? fshaderDesktop : fshaderES)) { - errorMessage = "Failed to compile OpenGL shader programs.\n" + program->log(); - return; - } - - // Link shader pipeline - if (!program->link() || !program->bind()) { - errorMessage = "Failed to link/bind OpenGL shader programs\n" + program->log(); - return; - } - - vertexLocation = program->attributeLocation("vertex"); - matrixLocation = program->uniformLocation("matrix"); - colorLocation = program->uniformLocation("colour"); - - if (vertexLocation == -1 || colorLocation == -1 || matrixLocation == -1) { - qWarning() << "Failed to locate shader variable"; - return; - } - - program->bind(); - - auto *gl = context()->functions(); - gl->glDisable(GL_DEPTH_TEST); - gl->glEnable(GL_BLEND); - // Enable depth buffer - gl->glEnable(GL_DEPTH_TEST); - - // Enable back face culling - gl->glEnable(GL_CULL_FACE); - gl->glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA); - - QColor bg = view->screen.background; - gl->glClearColor((GLfloat)bg.redF(), (GLfloat)bg.greenF(), (GLfloat)bg.blueF(), (GLfloat)bg.alphaF()); - - generateGrid(program.get()); - - { - m_vaoMarker.create(); - QOpenGLVertexArrayObject::Binder b(&m_vaoMarker); - m_marker.create(); - m_marker.bind(); - m_marker.setUsagePattern(QOpenGLBuffer::StaticDraw); - m_marker.allocate(int(vaMarker.size() * sizeof(Line))); - program->enableAttributeArray(vertexLocation); - program->setAttributeBuffer(vertexLocation, GL_FLOAT, 0, 3, 0); - } - - markerUpdated(); - - m_program = std::move(program); - shaderCompileSuccess = true; -} - -void GlScope::showData(std::shared_ptr data) { - if (!shaderCompileSuccess) return; - makeCurrent(); - // Remove too much entries - while (view->digitalPhosphorDraws() < m_GraphHistory.size()) m_GraphHistory.pop_back(); - - // Add if missing - if (view->digitalPhosphorDraws() > m_GraphHistory.size()) { m_GraphHistory.resize(m_GraphHistory.size() + 1); } - - // Move last item to front - m_GraphHistory.splice(m_GraphHistory.begin(), m_GraphHistory, std::prev(m_GraphHistory.end())); - - // Add new entry - m_GraphHistory.front().writeData(data.get(), m_program.get(), vertexLocation); - // doneCurrent(); - - update(); -} - -void GlScope::markerUpdated() { - - for (unsigned marker = 0; marker < vaMarker.size(); ++marker) { - if (!scope->horizontal.marker_visible[marker]) continue; - vaMarker[marker] = {QVector3D((GLfloat)scope->horizontal.marker[marker], -DIVS_VOLTAGE, 0.0f), - QVector3D((GLfloat)scope->horizontal.marker[marker], DIVS_VOLTAGE, 0.0f)}; - } - - // Write coordinates to GPU - makeCurrent(); - m_marker.bind(); - m_marker.write(0, vaMarker.data(), vaMarker.size() * sizeof(Line)); -} - -void GlScope::paintGL() { - if (!shaderCompileSuccess) return; - - auto *gl = context()->functions(); - - // Clear OpenGL buffer and configure settings - // TODO Don't clear if view->digitalPhosphorDraws()>1 - gl->glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); - gl->glLineWidth(1); - - m_program->bind(); - - // Apply zoom settings via matrix transformation - if (zoomed) { - QMatrix4x4 m; - m.scale(QVector3D(DIVS_TIME / (GLfloat)fabs(scope->horizontal.marker[1] - scope->horizontal.marker[0]), 1.0f, - 1.0f)); - m.translate((GLfloat) - (scope->horizontal.marker[0] + scope->horizontal.marker[1]) / 2, 0.0f, 0.0f); - m_program->setUniformValue(matrixLocation, pmvMatrix * m); - } - - unsigned historyIndex = 0; - for (Graph &graph : m_GraphHistory) { - for (ChannelID channel = 0; channel < scope->voltage.size(); ++channel) { - if (scope->horizontal.format == Dso::GraphFormat::TY) { - drawSpectrumChannelGraph(channel, graph, (int)historyIndex); - } - drawVoltageChannelGraph(channel, graph, (int)historyIndex); - } - ++historyIndex; - } - - if (zoomed) { m_program->setUniformValue(matrixLocation, pmvMatrix); } - - if (!this->zoomed) drawMarkers(); - - drawGrid(); - m_program->release(); -} - -void GlScope::resizeGL(int width, int height) { - if (!shaderCompileSuccess) return; - auto *gl = context()->functions(); - gl->glViewport(0, 0, (GLint)width, (GLint)height); - - // Set axes to div-scale and apply correction for exact pixelization - float pixelizationWidthCorrection = (float)width / (width - 1); - float pixelizationHeightCorrection = (float)height / (height - 1); - - pmvMatrix.setToIdentity(); - pmvMatrix.ortho(-(DIVS_TIME / 2.0f) * pixelizationWidthCorrection, (DIVS_TIME / 2.0f) * pixelizationWidthCorrection, - -(DIVS_VOLTAGE / 2.0f) * pixelizationHeightCorrection, - (DIVS_VOLTAGE / 2.0f) * pixelizationHeightCorrection, -1.0f, 1.0f); - - m_program->bind(); - m_program->setUniformValue(matrixLocation, pmvMatrix); - m_program->release(); -} - -void GlScope::generateGrid(QOpenGLShaderProgram *program) { - gridDrawCounts[0] = 0; - gridDrawCounts[1] = 0; - gridDrawCounts[2] = 0; - - m_grid.create(); - m_grid.bind(); - m_grid.setUsagePattern(QOpenGLBuffer::StaticDraw); - - std::vector vaGrid; - - { // Bind draw vertical lines - m_vaoGrid[0].create(); - QOpenGLVertexArrayObject::Binder b(&m_vaoGrid[0]); - m_grid.bind(); - program->enableAttributeArray(vertexLocation); - program->setAttributeBuffer(vertexLocation, GL_FLOAT, 0, 3, 0); - } - - // Draw vertical lines - for (int div = 1; div < DIVS_TIME / 2; ++div) { - for (int dot = 1; dot < DIVS_VOLTAGE / 2 * DIVS_SUB; ++dot) { - float dotPosition = (float)dot / DIVS_SUB; - gridDrawCounts[0] += 4; - vaGrid.push_back(QVector3D(-div, -dotPosition, 0)); - vaGrid.push_back(QVector3D(-div, dotPosition, 0)); - vaGrid.push_back(QVector3D(div, -dotPosition, 0)); - vaGrid.push_back(QVector3D(div, dotPosition, 0)); - } - } - // Draw horizontal lines - for (int div = 1; div < DIVS_VOLTAGE / 2; ++div) { - for (int dot = 1; dot < DIVS_TIME / 2 * DIVS_SUB; ++dot) { - if (dot % DIVS_SUB == 0) continue; // Already done by vertical lines - float dotPosition = (float)dot / DIVS_SUB; - gridDrawCounts[0] += 4; - vaGrid.push_back(QVector3D(-dotPosition, -div, 0)); - vaGrid.push_back(QVector3D(dotPosition, -div, 0)); - vaGrid.push_back(QVector3D(-dotPosition, div, 0)); - vaGrid.push_back(QVector3D(dotPosition, div, 0)); - } - } - - { // Bind draw axes - m_vaoGrid[1].create(); - QOpenGLVertexArrayObject::Binder b(&m_vaoGrid[1]); - m_grid.bind(); - program->enableAttributeArray(vertexLocation); - program->setAttributeBuffer(vertexLocation, GL_FLOAT, int(vaGrid.size() * sizeof(QVector3D)), 3); - } - - // Axes - // Horizontal axis - gridDrawCounts[1] += 4; - vaGrid.push_back(QVector3D(-DIVS_TIME / 2, 0, 0)); - vaGrid.push_back(QVector3D(DIVS_TIME / 2, 0, 0)); - // Vertical axis - vaGrid.push_back(QVector3D(0, -DIVS_VOLTAGE / 2, 0)); - vaGrid.push_back(QVector3D(0, DIVS_VOLTAGE / 2, 0)); - // Subdiv lines on horizontal axis - for (int line = 1; line < DIVS_TIME / 2 * DIVS_SUB; ++line) { - float linePosition = (float)line / DIVS_SUB; - gridDrawCounts[1] += 4; - vaGrid.push_back(QVector3D(linePosition, -0.05f, 0)); - vaGrid.push_back(QVector3D(linePosition, 0.05f, 0)); - vaGrid.push_back(QVector3D(-linePosition, -0.05f, 0)); - vaGrid.push_back(QVector3D(-linePosition, 0.05f, 0)); - } - // Subdiv lines on vertical axis - for (int line = 1; line < DIVS_VOLTAGE / 2 * DIVS_SUB; ++line) { - float linePosition = (float)line / DIVS_SUB; - gridDrawCounts[1] += 4; - vaGrid.push_back(QVector3D(-0.05f, linePosition, 0)); - vaGrid.push_back(QVector3D(0.05f, linePosition, 0)); - vaGrid.push_back(QVector3D(-0.05f, -linePosition, 0)); - vaGrid.push_back(QVector3D(0.05f, -linePosition, 0)); - } - - { - m_vaoGrid[2].create(); - QOpenGLVertexArrayObject::Binder b(&m_vaoGrid[2]); - m_grid.bind(); - program->enableAttributeArray(vertexLocation); - program->setAttributeBuffer(vertexLocation, GL_FLOAT, int(vaGrid.size() * sizeof(QVector3D)), 3); - } - - // Border - gridDrawCounts[2] += 4; - vaGrid.push_back(QVector3D(-DIVS_TIME / 2, -DIVS_VOLTAGE / 2, 0)); - vaGrid.push_back(QVector3D(DIVS_TIME / 2, -DIVS_VOLTAGE / 2, 0)); - vaGrid.push_back(QVector3D(DIVS_TIME / 2, DIVS_VOLTAGE / 2, 0)); - vaGrid.push_back(QVector3D(-DIVS_TIME / 2, DIVS_VOLTAGE / 2, 0)); - - m_grid.allocate(&vaGrid[0], int(vaGrid.size() * sizeof(QVector3D))); - m_grid.release(); -} - -void GlScope::drawGrid() { - auto *gl = context()->functions(); - gl->glLineWidth(1); - - // Grid - m_vaoGrid[0].bind(); - m_program->setUniformValue(colorLocation, view->screen.grid); - gl->glDrawArrays(GL_POINTS, 0, gridDrawCounts[0]); - m_vaoGrid[0].release(); - - // Axes - m_vaoGrid[1].bind(); - m_program->setUniformValue(colorLocation, view->screen.axes); - gl->glDrawArrays(GL_LINES, 0, gridDrawCounts[1]); - m_vaoGrid[1].release(); - - // Border - m_vaoGrid[2].bind(); - m_program->setUniformValue(colorLocation, view->screen.border); - gl->glDrawArrays(GL_LINE_LOOP, 0, gridDrawCounts[2]); - m_vaoGrid[2].release(); -} - -void GlScope::drawMarkers() { - auto *gl = context()->functions(); - QColor trColor = view->screen.markers; - m_program->setUniformValue(colorLocation, trColor); - - m_vaoMarker.bind(); - - // Draw all - gl->glLineWidth(1); - gl->glDrawArrays(GL_LINES, 0, (GLsizei)vaMarker.size() * 2); - - // Draw selected - if (selectedMarker != NO_MARKER) { - gl->glLineWidth(3); - gl->glDrawArrays(GL_LINES, selectedMarker * 2, (GLsizei)2); - } - - m_vaoMarker.release(); -} - -void GlScope::drawVoltageChannelGraph(ChannelID channel, Graph &graph, int historyIndex) { - if (!scope->voltage[channel].used) return; - - m_program->setUniformValue(colorLocation, view->screen.voltage[channel].darker(100 + 10 * historyIndex)); - Graph::VaoCount &v = graph.vaoVoltage[channel]; - - QOpenGLVertexArrayObject::Binder b(v.first); - const GLenum dMode = (view->interpolation == Dso::INTERPOLATION_OFF) ? GL_POINTS : GL_LINE_STRIP; - context()->functions()->glDrawArrays(dMode, 0, v.second); -} - -void GlScope::drawSpectrumChannelGraph(ChannelID channel, Graph &graph, int historyIndex) { - if (!scope->spectrum[channel].used) return; - - m_program->setUniformValue(colorLocation, view->screen.spectrum[channel].darker(100 + 10 * historyIndex)); - Graph::VaoCount &v = graph.vaoSpectrum[channel]; - - QOpenGLVertexArrayObject::Binder b(v.first); - const GLenum dMode = (view->interpolation == Dso::INTERPOLATION_OFF) ? GL_POINTS : GL_LINE_STRIP; - context()->functions()->glDrawArrays(dMode, 0, v.second); -} diff --git a/openhantek/src/glscope.h b/openhantek/src/glscope.h deleted file mode 100644 index 3f8f2ca7..00000000 --- a/openhantek/src/glscope.h +++ /dev/null @@ -1,117 +0,0 @@ -// SPDX-License-Identifier: GPL-2.0+ - -#pragma once - -#include -#include - -#include -#include -#include -#include -#include -#include - -#include "glscopegraph.h" -#include "hantekdso/enums.h" -#include "hantekprotocol/types.h" - -struct DsoSettingsView; -struct DsoSettingsScope; -class PPresult; - -/// \brief OpenGL accelerated widget that displays the oscilloscope screen. -class GlScope : public QOpenGLWidget { - Q_OBJECT - - public: - static GlScope *createNormal(DsoSettingsScope *scope, DsoSettingsView *view, - QWidget *parent = 0); - static GlScope *createZoomed(DsoSettingsScope *scope, DsoSettingsView *view, - QWidget *parent = 0); - - /** - * We need at least OpenGL 3.2 with shader version 150 or - * OpenGL ES 2.0 with shader version 100. - */ - static void fixOpenGLversion(QSurfaceFormat::RenderableType t=QSurfaceFormat::DefaultRenderableType); - /** - * Show new post processed data - * @param data - */ - void showData(std::shared_ptr data); - void markerUpdated(); - - protected: - /// \brief Initializes the scope widget. - /// \param settings The settings that should be used. - /// \param parent The parent widget. - GlScope(DsoSettingsScope *scope, DsoSettingsView *view, QWidget *parent = 0); - virtual ~GlScope(); - GlScope(const GlScope&) = delete; - - /// \brief Initializes OpenGL output. - virtual void initializeGL() override; - - /// \brief Draw the graphs, marker and the grid. - virtual void paintGL() override; - - /// \brief Resize the widget. - /// \param width The new width of the widget. - /// \param height The new height of the widget. - virtual void resizeGL(int width, int height) override; - - virtual void mousePressEvent(QMouseEvent *event) override; - virtual void mouseMoveEvent(QMouseEvent *event) override; - virtual void mouseReleaseEvent(QMouseEvent *event) override; - virtual void paintEvent(QPaintEvent *event) override; - - /// \brief Draw the grid. - void drawGrid(); - /// Draw vertical lines at marker positions - void drawMarkers(); - - void drawVoltageChannelGraph(ChannelID channel, Graph &graph, int historyIndex); - void drawSpectrumChannelGraph(ChannelID channel, Graph &graph, int historyIndex); - signals: - void markerMoved(unsigned marker, double position); - - private: - // User settings - DsoSettingsScope *scope; - DsoSettingsView *view; - bool zoomed = false; - - // Marker - const unsigned NO_MARKER = UINT_MAX; - #pragma pack(push, 1) - struct Line { - QVector3D x; - QVector3D y; - }; - #pragma pack(pop) - std::vector vaMarker; - unsigned selectedMarker = NO_MARKER; - QOpenGLBuffer m_marker; - QOpenGLVertexArrayObject m_vaoMarker; - - // Grid - QOpenGLBuffer m_grid; - QOpenGLVertexArrayObject m_vaoGrid[3]; - GLsizei gridDrawCounts[3]; - void generateGrid(QOpenGLShaderProgram *program); - - // Graphs - std::list m_GraphHistory; - unsigned currentGraphInHistory = 0; - - // OpenGL shader, matrix, var-locations - bool shaderCompileSuccess = false; - QString errorMessage; - std::unique_ptr m_program; - QMatrix4x4 pmvMatrix; ///< projection, view matrix - int colorLocation; - int vertexLocation; - int matrixLocation; - int selectionLocation; -}; diff --git a/openhantek/src/glscopegraph.cpp b/openhantek/src/glscopegraph.cpp deleted file mode 100644 index 29ca3ade..00000000 --- a/openhantek/src/glscopegraph.cpp +++ /dev/null @@ -1,81 +0,0 @@ -#include "glscopegraph.h" -#include - -Graph::Graph() : buffer(QOpenGLBuffer::VertexBuffer) { - buffer.create(); - buffer.setUsagePattern(QOpenGLBuffer::DynamicDraw); -} - -void Graph::writeData(PPresult *data, QOpenGLShaderProgram *program, int vertexLocation) { - // Determine memory - int neededMemory = 0; - for (ChannelGraph &cg : data->vaChannelVoltage) neededMemory += cg.size() * sizeof(QVector3D); - for (ChannelGraph &cg : data->vaChannelSpectrum) neededMemory += cg.size() * sizeof(QVector3D); - - buffer.bind(); - program->bind(); - - // Allocate space if necessary - if (neededMemory > allocatedMem) { - buffer.allocate(neededMemory); - allocatedMem = neededMemory; - } - - // Write data to buffer - int offset = 0; - vaoVoltage.resize(data->vaChannelVoltage.size()); - vaoSpectrum.resize(data->vaChannelSpectrum.size()); - for (ChannelID channel = 0; channel < vaoVoltage.size(); ++channel) { - int dataSize; - - // Voltage channel - if (channel < vaoVoltage.size()) { - VaoCount &v = vaoVoltage[channel]; - if (!v.first) { - v.first = new QOpenGLVertexArrayObject; - if (!v.first->create()) throw new std::runtime_error("QOpenGLVertexArrayObject create failed"); - } - ChannelGraph &gVoltage = data->vaChannelVoltage[channel]; - v.first->bind(); - dataSize = int(gVoltage.size() * sizeof(QVector3D)); - buffer.write(offset, gVoltage.data(), dataSize); - program->enableAttributeArray(vertexLocation); - program->setAttributeBuffer(vertexLocation, GL_FLOAT, offset, 3, 0); - v.first->release(); - v.second = (int)gVoltage.size(); - offset += dataSize; - } - - // Spectrum channel - if (channel < vaoSpectrum.size()) { - VaoCount &s = vaoSpectrum[channel]; - if (!s.first) { - s.first = new QOpenGLVertexArrayObject; - if (!s.first->create()) throw new std::runtime_error("QOpenGLVertexArrayObject create failed"); - } - ChannelGraph &gSpectrum = data->vaChannelSpectrum[channel]; - s.first->bind(); - dataSize = int(gSpectrum.size() * sizeof(QVector3D)); - buffer.write(offset, gSpectrum.data(), dataSize); - program->enableAttributeArray(vertexLocation); - program->setAttributeBuffer(vertexLocation, GL_FLOAT, offset, 3, 0); - s.first->release(); - s.second = (int)gSpectrum.size(); - offset += dataSize; - } - } - - buffer.release(); -} - -Graph::~Graph() { - for (auto &vao : vaoVoltage) { - vao.first->destroy(); - delete vao.first; - } - for (auto &vao : vaoSpectrum) { - vao.first->destroy(); - delete vao.first; - } - if (buffer.isCreated()) { buffer.destroy(); } -} diff --git a/openhantek/src/glscopegraph.h b/openhantek/src/glscopegraph.h deleted file mode 100644 index 222bd19d..00000000 --- a/openhantek/src/glscopegraph.h +++ /dev/null @@ -1,27 +0,0 @@ -#pragma once - -#include - -#include -#include -#include -#include -#include -#include - -#include "post/ppresult.h" - -struct Graph { - explicit Graph(); - Graph(const Graph &) = delete; - Graph(const Graph &&) = delete; - ~Graph(); - void writeData(PPresult *data, QOpenGLShaderProgram *program, int vertexLocation); - typedef std::pair VaoCount; - - public: - int allocatedMem = 0; - QOpenGLBuffer buffer; - std::vector vaoVoltage; - std::vector vaoSpectrum; -}; diff --git a/openhantek/src/hantekdso/channelusage.cpp b/openhantek/src/hantekdso/channelusage.cpp new file mode 100644 index 00000000..0e7dcb65 --- /dev/null +++ b/openhantek/src/hantekdso/channelusage.cpp @@ -0,0 +1,30 @@ +// SPDX-License-Identifier: GPL-2.0+ + +#include "channelusage.h" + +namespace Dso { + +bool ChannelUsage::isUsed(ChannelID channelId) const { + QMutexLocker locker(usedMutex.get()); + return m_used[channelId].size(); +} + +void ChannelUsage::addChannelUser(ChannelID channelId, void *object) { + QMutexLocker locker(usedMutex.get()); + m_used[channelId].insert(object); + emit usedChanged(channelId, m_used[channelId].size()); +} + +void ChannelUsage::removeChannelUser(ChannelID channelId, void *object) { + QMutexLocker locker(usedMutex.get()); + m_used[channelId].erase(object); + emit usedChanged(channelId, m_used[channelId].size()); +} + +unsigned ChannelUsage::countUsedChannels() const { + unsigned c = 0; + for (auto &b : m_used) + if (b.size()) ++c; + return c; +} +} diff --git a/openhantek/src/hantekdso/channelusage.h b/openhantek/src/hantekdso/channelusage.h new file mode 100644 index 00000000..76190c33 --- /dev/null +++ b/openhantek/src/hantekdso/channelusage.h @@ -0,0 +1,45 @@ +// SPDX-License-Identifier: GPL-2.0+ + +#pragma once + +#include "hantekprotocol/types.h" +#include +#include +#include +#include +#include + +namespace Dso { + +/** + * DsoControl has an automatic channel enable mechanism, based on the usage of a hardware channel. + * You first request this ChannelUsage object from DsoControl and then add/remove channel users. + * The hardware channels will be enabled/disabled based on usage/reference count. + */ +class ChannelUsage : public QObject { + Q_OBJECT + public: + inline ChannelUsage(unsigned channels) { m_used.resize(channels); } + + /// Return true if the channel is used by a voltage, spectrum graph or a math channel. This method is + /// thread-safe. HantekDsoControl should use m_used.size() instead in hot code paths. + bool isUsed(ChannelID channelId) const; + /// Add a user of this channel. As soon as the channel is used by at least one object, it will + /// be activated. This method is to be used by the scope settings setVisible methods. + /// Thread-safe. + void addChannelUser(ChannelID channelId, void *object); + /// Remove a user of this channel. As soon as the channel is not used anymore, it will be deactivated. + /// This method is to be used by the scope settings setVisible methods. + /// Thread-safe. + void removeChannelUser(ChannelID channelId, void *object); + /// Counts the currently used hardware channels. + /// This method will access shared data between DsoControl and device settings and is thread-safe. + unsigned countUsedChannels() const; + + private: + mutable std::unique_ptr usedMutex = std::unique_ptr(new QMutex); + std::vector> m_used; ///< objects, that are using this channel + signals: + void usedChanged(ChannelID channelId, bool used); +}; +} diff --git a/openhantek/src/hantekdso/controlsettings.cpp b/openhantek/src/hantekdso/controlsettings.cpp deleted file mode 100644 index 6b119baf..00000000 --- a/openhantek/src/hantekdso/controlsettings.cpp +++ /dev/null @@ -1,19 +0,0 @@ -#include "controlsettings.h" -#include "hantekprotocol/definitions.h" - -namespace Dso { - -ControlSettings::ControlSettings(const ControlSamplerateLimits * limits, size_t channelCount) : cmdGetLimits(channelCount) -{ - samplerate.limits = limits; - trigger.level.resize(channelCount); - voltage.resize(channelCount); - offsetLimit = new Hantek::OffsetsPerGainStep[channelCount]; -} - -ControlSettings::~ControlSettings() -{ - delete [] offsetLimit; -} - -} diff --git a/openhantek/src/hantekdso/controlsettings.h b/openhantek/src/hantekdso/controlsettings.h deleted file mode 100644 index a328f507..00000000 --- a/openhantek/src/hantekdso/controlsettings.h +++ /dev/null @@ -1,66 +0,0 @@ -#pragma once - -#include "enums.h" -#include "hantekprotocol/types.h" -#include "hantekprotocol/bulkcode.h" -#include "hantekprotocol/controlStructs.h" - -namespace Hantek { -struct OffsetsPerGainStep; -} - -namespace Dso { - -struct ControlSamplerateLimits; - -/// \brief Stores the target samplerate settings of the device. -struct ControlSettingsSamplerateTarget { - double samplerate; ///< The target samplerate set via setSamplerate - double duration; ///< The target record time set via setRecordTime - enum SamplerrateSet { Duration, Samplerrate } samplerateSet; -}; - -/// \brief Stores the current samplerate settings of the device. -struct ControlSettingsSamplerate { - ControlSettingsSamplerateTarget target; ///< The target samplerate values - const ControlSamplerateLimits * limits; ///< The samplerate limits - unsigned int downsampler = 1; ///< The variable downsampling factor - double current = 1e8; ///< The current samplerate -}; - -/// \brief Stores the current trigger settings of the device. -struct ControlSettingsTrigger { - std::vector level; ///< The trigger level for each channel in V - double position = 0.0; ///< The current pretrigger position - unsigned int point = 0; ///< The trigger position in Hantek coding - Dso::TriggerMode mode = Dso::TriggerMode::HARDWARE_SOFTWARE; ///< The trigger mode - Dso::Slope slope = Dso::Slope::Positive; ///< The trigger slope - bool special = false; ///< true, if the trigger source is special - unsigned int source = 0; ///< The trigger source -}; - -/// \brief Stores the current amplification settings of the device. -struct ControlSettingsVoltage { - double offset = 0.0; ///< The screen offset for each channel - double offsetReal = 0.0; ///< The real offset for each channel (Due to quantization) - unsigned gain = 0; ///< The gain id - bool used = false; ///< true, if the channel is used -}; - -/// \brief Stores the current settings of the device. -struct ControlSettings { - ControlSettings(const ControlSamplerateLimits *limits, size_t channelCount); - ~ControlSettings(); - ControlSettingsSamplerate samplerate; ///< The samplerate settings - std::vector voltage; ///< The amplification settings - ControlSettingsTrigger trigger; ///< The trigger settings - RecordLengthID recordLengthId = 1; ///< The id in the record length array - unsigned usedChannels = 0; ///< Number of activated channels - unsigned swSampleMargin = 2000; ///< Software trigger, sample margin - Hantek::OffsetsPerGainStep *offsetLimit; ///< Calibration data for the channel offsets - - - Hantek::ControlBeginCommand beginCommandControl; - Hantek::ControlGetLimits cmdGetLimits; -}; -} diff --git a/openhantek/src/hantekdso/controlspecification.cpp b/openhantek/src/hantekdso/controlspecification.cpp deleted file mode 100644 index 9cdccc9f..00000000 --- a/openhantek/src/hantekdso/controlspecification.cpp +++ /dev/null @@ -1,5 +0,0 @@ -#include "controlspecification.h" - -Dso::ControlSpecification::ControlSpecification(unsigned channels) : channels(channels) { - voltageLimit.resize(channels); -} diff --git a/openhantek/src/hantekdso/controlspecification.h b/openhantek/src/hantekdso/controlspecification.h deleted file mode 100644 index 964207bb..00000000 --- a/openhantek/src/hantekdso/controlspecification.h +++ /dev/null @@ -1,94 +0,0 @@ -// SPDX-License-Identifier: GPL-2.0+ - -#pragma once - -#include "enums.h" -#include "hantekprotocol/definitions.h" -#include "hantekprotocol/types.h" -#include "hantekprotocol/bulkcode.h" -#include - -namespace Dso { - -using namespace Hantek; - -/// \brief Stores the samplerate limits for calculations. -struct ControlSamplerateLimits { - double base; ///< The base for sample rate calculations - double max; ///< The maximum sample rate - unsigned int maxDownsampler; ///< The maximum downsampling ratio - std::vector recordLengths; ///< Available record lengths, UINT_MAX means rolling -}; - -/// \brief Stores the samplerate limits. -struct ControlSpecificationSamplerate { - ControlSamplerateLimits single = {50e6, 50e6, 0, std::vector()}; ///< The limits for single channel mode - ControlSamplerateLimits multi = {100e6, 100e6, 0, std::vector()}; ///< The limits for multi channel mode -}; - -struct ControlSpecificationGainLevel { - /// The index of the selected gain on the hardware - unsigned char gainIndex; - /// Available voltage steps in V/screenheight - double gainSteps; -}; - -struct FixedSampleRate { - unsigned char id; - double samplerate; -}; - -struct SpecialTriggerChannel { - std::string name; - int hardwareID; -}; - -/// \brief Stores the specifications of the currently connected device. -struct ControlSpecification { - ControlSpecification(unsigned channels); - const ChannelID channels; - - // Interface - BulkCode cmdSetChannels = BulkCode::INVALID; ///< Command for setting used channels - BulkCode cmdSetSamplerate = BulkCode::INVALID; ///< Command for samplerate settings - BulkCode cmdSetRecordLength = BulkCode::INVALID; ///< Command for buffer settings - BulkCode cmdSetTrigger = BulkCode::INVALID; ///< Command for trigger settings - BulkCode cmdSetPretrigger = BulkCode::INVALID; ///< Command for pretrigger settings - BulkCode cmdForceTrigger = BulkCode::FORCETRIGGER; ///< Command for forcing a trigger event - BulkCode cmdCaptureStart = BulkCode::STARTSAMPLING; ///< Command for starting the sampling - BulkCode cmdTriggerEnabled = BulkCode::ENABLETRIGGER; ///< Command for enabling the trigger - BulkCode cmdGetData = BulkCode::GETDATA; ///< Command for retrieve sample data - BulkCode cmdGetCaptureState = BulkCode::GETCAPTURESTATE; ///< Command for retrieve the capture state - BulkCode cmdSetGain = BulkCode::SETGAIN; ///< Command for setting the gain - - // Limits - ControlSpecificationSamplerate samplerate; ///< The samplerate specifications - std::vector bufferDividers; ///< Samplerate dividers for record lengths - unsigned char sampleSize; ///< Number of bits per sample - - /// For devices that support only fixed sample rates (isFixedSamplerateDevice=true) - std::vector fixedSampleRates; - - // Calibration - - /// The sample values at the top of the screen - typedef std::vector VoltageLimit; - std::vector voltageLimit; // Per channel - - /// Gain levels - std::vector gain; - - // Features - std::vector specialTriggerChannels; - std::vector couplings = {Dso::Coupling::DC, Dso::Coupling::AC}; - std::vector triggerModes = {TriggerMode::HARDWARE_SOFTWARE, TriggerMode::WAIT_FORCE, - TriggerMode::SINGLE}; - bool isFixedSamplerateDevice = false; - bool isSoftwareTriggerDevice = false; - bool useControlNoBulk = false; - bool supportsCaptureState = true; - bool supportsOffset = true; - bool supportsCouplingRelays = true; - int fixedUSBinLength = 0; -}; -} diff --git a/openhantek/src/hantekdso/devicesettings.cpp b/openhantek/src/hantekdso/devicesettings.cpp new file mode 100644 index 00000000..81398f72 --- /dev/null +++ b/openhantek/src/hantekdso/devicesettings.cpp @@ -0,0 +1,136 @@ +// SPDX-License-Identifier: GPL-2.0+ + +#include "devicesettings.h" +#include "hantekprotocol/definitions.h" + +#include +#include + +namespace Dso { + +DeviceSettings::DeviceSettings(const ModelSpec *specification) : spec(specification) { + this->limits = &specification->normalSamplerate; + while (voltage.size() < specification->channels) voltage.push_back(new Channel); +} + +void DeviceSettings::setRecordLengthId(RecordLengthID value) { + m_recordLengthId = value; + emit recordLengthChanged(m_recordLengthId); +} + +void DeviceSettings::updateCurrentSamplerate(double samplerate, double timebase, unsigned fixedSamplerateIndex) { + m_samplerate.samplerate = samplerate; + m_samplerate.timebase = timebase; + m_samplerate.fixedSamperateId = fixedSamplerateIndex; +} + +Samplerate &DeviceSettings::updateTarget(SamplerateSource source) { + m_samplerateSource = source; + return m_targetSamperate; +} + +void Channel::setOffset(double offset, double offsetHardware) { + m_offset = offset; + m_offsetHardware = offsetHardware; + emit offsetChanged(offset); +} + +void Channel::setTriggerOffset(double offset) { + m_triggerOffset = offset; + emit triggerLevelChanged(offset); +} + +void Channel::setGainStepIndex(unsigned gainId) { + m_gainStepIndex = gainId; + emit gainStepIndexChanged(gainId); +} + +void Channel::setCouplingIndex(unsigned couplingId) { + m_couplingIndex = couplingId; + emit couplingIndexChanged(couplingId); +} + +void Trigger::setPosition(double position) { + this->m_position = position; + emit positionChanged(position); +} + +void Trigger::setTriggerSource(ChannelID channel, bool specialChannel) { + m_source = channel; + m_special = specialChannel; + emit sourceChanged(specialChannel, channel); +} + +void Trigger::setSlope(Slope slope) { + this->m_slope = slope; + emit slopeChanged(slope); +} + +void Trigger::setMode(TriggerMode mode) { + this->m_mode = mode; + emit modeChanged(mode); +} +} + +void Settings::DeviceSettingsIO::read(QSettings *io, Dso::DeviceSettings &control) { + control.m_recordLengthId = std::min(io->value("recordLengthId", control.m_recordLengthId).toUInt(), + (unsigned)control.spec->normalSamplerate.recordLengths.size() - 1); + Dso::Samplerate &localUpdateTarget = control.updateTarget( + (Dso::SamplerateSource)io->value("samplerateSource", (unsigned)control.samplerateSource()).toUInt()); + localUpdateTarget.fixedSamperateId = io->value("fixedSamperateId", localUpdateTarget.fixedSamperateId).toUInt(); + localUpdateTarget.samplerate = io->value("samplerate", localUpdateTarget.samplerate).toDouble(); + localUpdateTarget.timebase = io->value("timebase", localUpdateTarget.timebase).toDouble(); + + control.trigger.m_mode = (Dso::TriggerMode)io->value("trigger.mode", (unsigned)control.trigger.m_mode).toUInt(); + control.trigger.m_slope = (Dso::Slope)io->value("trigger.slope", (unsigned)control.trigger.m_slope).toUInt(); + control.trigger.m_position = io->value("trigger.position", control.trigger.m_position).toDouble(); + control.trigger.m_point = io->value("trigger.point", control.trigger.m_point).toUInt(); + control.trigger.m_source = io->value("trigger.source", control.trigger.m_source).toUInt(); + control.trigger.m_swTriggerThreshold = + io->value("trigger.swTriggerThreshold", control.trigger.m_swTriggerThreshold).toUInt(); + control.trigger.m_swTriggerSampleSet = + io->value("trigger.swTriggerSampleSet", control.trigger.m_swTriggerSampleSet).toUInt(); + control.trigger.m_swSampleMargin = io->value("trigger.swSampleMargin", control.trigger.m_swSampleMargin).toUInt(); + control.trigger.m_special = io->value("trigger.special", control.trigger.m_special).toBool(); + + for (unsigned i = 0; i < control.voltage.size(); ++i) { + io->beginGroup("channel" + QString::number(i)); + Dso::Channel *chan = control.voltage[i]; + chan->m_couplingIndex = io->value("couplingIndex", chan->couplingIndex()).toUInt(); + chan->m_gainStepIndex = + std::min(io->value("gainId", chan->gainStepIndex()).toUInt(), (unsigned)control.spec->gain.size() - 1); + chan->m_offset = io->value("offset", chan->offset()).toDouble(); + chan->m_offsetHardware = io->value("offsetReal", chan->offsetHardware()).toDouble(); + chan->m_triggerOffset = io->value("triggerLevel", chan->triggerLevel()).toDouble(); + io->endGroup(); + } +} + +void Settings::DeviceSettingsIO::write(QSettings *io, const Dso::DeviceSettings &control) { + io->setValue("recordLengthId", control.m_recordLengthId); + io->setValue("samplerateSource", (unsigned)control.samplerateSource()); + io->setValue("fixedSamperateId", control.target().fixedSamperateId); + io->setValue("samplerate", control.target().samplerate); + io->setValue("timebase", control.target().timebase); + + io->setValue("trigger.mode", (unsigned)control.trigger.m_mode); + io->setValue("trigger.slope", (unsigned)control.trigger.m_slope); + io->setValue("trigger.position", control.trigger.m_position); + io->setValue("trigger.point", control.trigger.m_point); + io->setValue("trigger.source", control.trigger.m_source); + io->setValue("trigger.swTriggerThreshold", control.trigger.m_swTriggerThreshold); + io->setValue("trigger.swTriggerSampleSet", control.trigger.m_swTriggerSampleSet); + io->setValue("trigger.swSampleMargin", control.trigger.m_swSampleMargin); + io->setValue("trigger.special", control.trigger.m_special); + + for (unsigned i = 0; i < control.voltage.size(); ++i) { + io->beginGroup("channel" + QString::number(i)); + const Dso::Channel *chan = control.voltage[i]; + io->setValue("couplingIndex", chan->couplingIndex()); + io->setValue("gainId", chan->gainStepIndex()); + io->setValue("offset", chan->offset()); + io->setValue("offsetReal", chan->offsetHardware()); + io->setValue("triggerLevel", chan->triggerLevel()); + io->endGroup(); + } +} diff --git a/openhantek/src/hantekdso/devicesettings.h b/openhantek/src/hantekdso/devicesettings.h new file mode 100644 index 00000000..f6c50ace --- /dev/null +++ b/openhantek/src/hantekdso/devicesettings.h @@ -0,0 +1,261 @@ +// SPDX-License-Identifier: GPL-2.0+ + +#pragma once + +/**< + * Contains all settings for a currently connected device as well as the state that this device is in. + * Changes (write access) are only allowed from within the DsoControl class. You can connect to various signals + * to be notified of changes. + */ + +#include "enums.h" +#include "hantekdso/modelspecification.h" +#include "hantekprotocol/codes.h" +#include "hantekprotocol/types.h" + +#include +#include +#include +#include + +namespace Settings { +struct DeviceSettingsIO; +} + +class DsoControl; + +namespace Dso { + +struct ControlSamplerateLimits; + +/// \brief Stores the current or target samplerate settings of the device. +struct Samplerate { + double samplerate = 1e8; ///< The target samplerate set via setSamplerate + double timebase = 1e-3; ///< The target record time set via setRecordTime + unsigned fixedSamperateId = 0; ///< The target samplerate for fixed samplerate devices set via setFixedSamplerate +}; + +/// \brief Stores the current trigger settings of the device. +class Trigger : public QObject { + friend struct Settings::DeviceSettingsIO; + Q_OBJECT + public: + Dso::TriggerMode mode() const { return m_mode; } ///< The trigger mode + Dso::Slope slope() const { return m_slope; } ///< The trigger slope + double position() const { return m_position; } ///< The current pretrigger position in range [0,1] + bool special() const { return m_special; } ///< true, if the trigger source is special + unsigned source() const { return m_source; } ///< The trigger source + unsigned point() const { return m_point; } ///< The trigger position in Hantek coding + unsigned swTriggerThreshold() const { return m_swTriggerThreshold; } ///< Software trigger, threshold + unsigned swTriggerSampleSet() const { return m_swTriggerSampleSet; } ///< Software trigger, sample set + unsigned swSampleMargin() const { return m_swSampleMargin; } ///< Software trigger, sample margin + + void setPosition(double position); + void setPoint(unsigned point) { this->m_point = point; } + void setTriggerSource(ChannelID channel, bool specialChannel); + void setSlope(Dso::Slope slope); + void setMode(Dso::TriggerMode mode); + + private: + Dso::TriggerMode m_mode = // + Dso::TriggerMode::HARDWARE_SOFTWARE; ///< The trigger mode + Dso::Slope m_slope = Dso::Slope::Positive; ///< The trigger slope + double m_position = 0.0; ///< The current pretrigger position in range [0,1] + bool m_special = false; ///< true, if the trigger source is special + unsigned m_source = 0; ///< The trigger source + unsigned m_point = 0; ///< The trigger position in Hantek coding + unsigned m_swTriggerThreshold = 7; ///< Software trigger, threshold + unsigned m_swTriggerSampleSet = 11; ///< Software trigger, sample set + unsigned m_swSampleMargin = 2000; ///< Software trigger, sample margin + signals: + void modeChanged(Dso::TriggerMode mode); ///< The trigger mode has been changed + void sourceChanged(bool special, unsigned int id); ///< The trigger source has been changed + void slopeChanged(Dso::Slope slope); ///< The trigger slope has been changed + void positionChanged(double position); ///< The trigger position has been changed +}; + +/// \brief Stores the current amplification settings of the device. +class Channel : public QObject { + Q_OBJECT + friend struct Settings::DeviceSettingsIO; + + public: + /// The current coupling index + inline unsigned couplingIndex() const { return m_couplingIndex; } + /// The vertical resolution gain index for gain in V + inline unsigned gainStepIndex() const { return m_gainStepIndex; } + /// The current offset value in [-1,1]. + inline double offset() const { return m_offset; } + /// \return Returns the hardware applied offset. For devices that do not support hardware offsets, this will be 0. + inline double offsetHardware() const { return m_offsetHardware; } + /// \return Returns the trigger level in range [0,1] + inline double triggerLevel() const { return m_triggerOffset; } + + /// Get the coupling value for the specific channel + inline Dso::Coupling coupling(const Dso::ModelSpec *spec) const { return spec->couplings[m_couplingIndex]; } + + /** + * @brief Sets the offset value and emit the corresponding signal. Only to be called by HantekDsoControl. + * @param offset The offset value [-1,1] + * @param offsetHardware Not necessary for math channels. The hardware applied offset. The received raw sample + * set will have this offset applied, so we need to remember and remove it later to output clean sample + * values in range [0,1]. + */ + void setOffset(double offset, double offsetHardware = 0.0); + + /** + * Sets the trigger level / trigger offset and emit the corresponding signal. Only to be called by HantekDsoControl. + * @param offset The offset value [-1,1] + */ + void setTriggerOffset(double offset); + + /** + * @brief Sets the gain id and emit the corresponding signal. Only to be called by HantekDsoControl. + * @param gainId Gain ID. Must be in range with Dso::ModelSpec defined gain ids. + */ + void setGainStepIndex(unsigned gainId); + + /** + * @brief Sets the coupling id and emit the corresponding signal. Only to be called by HantekDsoControl. + * @param couplingId Coupling ID. Must be in range with Dso::ModelSpec defined coupling ids. + */ + void setCouplingIndex(unsigned couplingId); + + private: + unsigned m_couplingIndex = 0; ///< Coupling index (refers to one of the Dso::Coupling values) + unsigned m_gainStepIndex = 0; ///< The vertical resolution gain index for gain in V + double m_offset = 0.; ///< The offset for each channel [-1,1]. + double m_offsetHardware = 0.0; ///< The hardware applied offset for each channel (Quantization+Min/Max considered) + Voltage m_triggerOffset; ///< The trigger level in V + + signals: + void gainStepIndexChanged(unsigned gainId); + void couplingIndexChanged(unsigned couplingIndex); + void offsetChanged(double offset); + void triggerLevelChanged(double triggerLevel); +}; + +/// A samplerate can be set/influenced via the timebase, a samplerate value, a fixed samplerate id that refers +/// to a samplerate. We need to keep track which is the source for the current device samplerate. +enum class SamplerateSource { Duration, FixedSamplerate, Samplerrate }; + +/// Contains the current device settings as well as the current state of the scope device. +/// Those settings and the state are highly interactive with the {@link HantekDsoControl} class. +/// If a change to the state is made, it is propagated via the signals of this class. +class DeviceSettings : public QObject { + Q_OBJECT + friend struct Settings::DeviceSettingsIO; + + public: + DeviceSettings(const ModelSpec *specification); + + /// \brief Return the target samplerate, as set by the user. + /// You have access to a (samperate,record-time,fixed-samplerate-id)-tuple, + /// but only the value defined by samplerateSource() is valid. + /// + /// The value is not necessarly what is applied to the hardware. To lookup the current samplerate, + /// use samplerate() instead. + inline const Samplerate &target() const { return m_targetSamperate; } + inline SamplerateSource samplerateSource() const { return m_samplerateSource; } + + /// Return the current (samperate,record-time,fixed-samplerate-id)-tuple. The fixed-samplerate-id + /// is only valid, if this is a fixed samplerates model. Use spec->isFixedSamplerateDevice if in doubt. + inline const Samplerate &samplerate() const { return m_samplerate; } + + /// Return true if roll-mode is enabled. + inline bool isRollMode() const { return limits->recordLengths[m_recordLengthId].recordLength == ROLL_RECORDLEN; } + + /// Returns true if in fast rate mode (one channel uses all bandwith) + inline bool isFastRate() const { return limits == &spec->fastrateSamplerate; } + + /// \brief Gets the record length id. + /// The id can be used to look up the record length in the model specification. + inline RecordLengthID recordLengthId() const { return m_recordLengthId; } + + /// \brief Sets the record length id. + /// The id will be used to look up the record length in the model specification. + /// Called by DsoControl. Never call this out of DsoControl because the values will not be applied back. + void setRecordLengthId(RecordLengthID value); + + /// Updates the (samperate,record-time,fixed-samplerate-id)-tuple. + /// Called by DsoControl. Never call this out of DsoControl because the values will not be applied back. + void updateCurrentSamplerate(double samplerate, double timebase, unsigned fixedSamplerateIndex); + + /// A samplerate, recordtime or samplerate based on fixed ids can never be set alone. Each parameter + /// influences the others. This method allows to manipulate the target Samplerate structure, but you must + /// define which parameter should be the dominating one / the source for the others. + /// Called by DsoControl. Never call this out of DsoControl because the values will not be applied back. + Samplerate &updateTarget(SamplerateSource source); + + /// \brief Return the hardware applied gain in V. + /// Uses the current gain step id and gain steps defined in the model specification. + inline double gain(ChannelID channel) const { return spec->gain[voltage[channel]->gainStepIndex()].gain; } + + /// \brief Return the record length + /// Uses the current recordLengthId and record lengths defined in the model specification. + inline unsigned getRecordLength() const { return limits->recordLengths[m_recordLengthId].recordLength; } + + /// Returns a step value meant to be used for adjusting the offset value [-1,1]. Because of quantization + /// of the offset which will be in the range of [offsetStart,offsetEnd] of calibration[channel][gainId] + /// we can't just use a pure double. + inline double offsetAdjustStep(ChannelID channel) const { + /// For non physical channels or not supported hardware offset + if (!spec->supportsOffset || channel > spec->calibration.size()) return 0.001; + + const Dso::ModelSpec::GainStepCalibration &c = spec->calibration[channel][voltage[channel]->gainStepIndex()]; + return 1 / (c.offsetEnd - c.offsetStart); + } + + /// \brief Gets the maximum size of one packet transmitted via bulk transfer. + inline unsigned packetSize() const { return m_packetSize; } + + inline unsigned getSampleCount() const { + if (isRollMode()) + return packetSize(); + else { + return (isFastRate()) ? getRecordLength() : getRecordLength() * spec->channels; + } + } + + public: + const ModelSpec *spec; + + // Device settings + std::vector voltage; ///< The amplification settings + Trigger trigger; ///< The trigger settings + + // State variables: Those are not stored/restored + const ControlSamplerateLimits *limits; ///< The samplerate limits + unsigned int downsampler = 1; ///< The variable downsampling factor + unsigned m_packetSize = 0; ///< Device packet size + + private: + // Device settings + SamplerateSource m_samplerateSource; + Samplerate m_targetSamperate; ///< The target samplerate values + + // State variables: Those are not stored/restored + Samplerate m_samplerate; ///< The samplerate settings + RecordLengthID m_recordLengthId = 1; ///< The id in the record length array + signals: + /// The available samplerate range has changed + void samplerateLimitsChanged(double minimum, double maximum); + /// The available samplerate for fixed samplerate devices has changed + void fixedSampleratesChanged(const std::vector &sampleSteps); + /// The available record lengths. Is also emitted whenever the samplerate limits changed + void availableRecordLengthsChanged(const std::vector &recordLengths); + /// The samplerate, or fixed samplerateId or recordTime has changed + void samplerateChanged(Samplerate samplerate); + /// The record length has changed + void recordLengthChanged(unsigned m_recordLengthId); +}; +} +Q_DECLARE_METATYPE(Dso::Samplerate); + +class QSettings; +namespace Settings { +struct DeviceSettingsIO { + static void read(QSettings *io, Dso::DeviceSettings &control); + static void write(QSettings *io, const Dso::DeviceSettings &control); +}; +} diff --git a/openhantek/src/hantekdso/dsocommandqueue.cpp b/openhantek/src/hantekdso/dsocommandqueue.cpp new file mode 100644 index 00000000..05e8f213 --- /dev/null +++ b/openhantek/src/hantekdso/dsocommandqueue.cpp @@ -0,0 +1,118 @@ +#include "dsocommandqueue.h" +#include "dsocontrol.h" +#include "usb/usbdevice.h" +#include "utils/debugnotify.h" +#include "utils/printutils.h" +#include + +#ifdef DEBUG +#define DBGNOTIFY(x, y) emit m_control->debugMessage(x, y) +#else +#define DBGNOTIFY(x, y) +#endif + +DsoCommandQueue::DsoCommandQueue(const Dso::ModelSpec *spec, USBDevice *device, DsoControl *control) + : m_commandMutex(QMutex::Recursive), m_useControlNoBulk(spec->useControlNoBulk), m_control(control), + m_device(device) {} + +DsoCommandQueue::~DsoCommandQueue() { + while (firstBulkCommand) { + BulkCommand *t = firstBulkCommand->next; + delete firstBulkCommand; + firstBulkCommand = t; + } + while (firstControlCommand) { + ControlCommand *t = firstControlCommand->next; + delete firstControlCommand; + firstControlCommand = t; + } +} + +void DsoCommandQueue::addCommand(BulkCommand *newCommand, bool pending) { + newCommand->pending = pending; + command[(uint8_t)newCommand->code] = newCommand; + newCommand->next = firstBulkCommand; + firstBulkCommand = newCommand; +} + +void DsoCommandQueue::addCommand(ControlCommand *newCommand, bool pending) { + newCommand->pending = pending; + control[newCommand->code] = newCommand; + newCommand->next = firstControlCommand; + firstControlCommand = newCommand; +} + +int DsoCommandQueue::bulkCommand(const std::vector *command, int attempts) const { + // Send BeginCommand control command + int errorCode = m_device->controlWrite(&m_control->m_specification->beginCommandControl); + if (errorCode < 0) return errorCode; + + // Send bulk command + return m_device->bulkWrite(command->data(), command->size(), attempts); +} + +bool DsoCommandQueue::sendPendingCommands() { + int errorCode; + QMutexLocker l(&m_commandMutex); + + // Send all pending control bulk commands + BulkCommand *command = m_useControlNoBulk ? nullptr : firstBulkCommand; + while (command) { + if (command->pending) { + DBGNOTIFY(QString("%1, %2") + .arg(QMetaEnum::fromType().valueToKey((int)command->code)) + .arg(hexDump(command->data(), command->size())), + Debug::NotificationType::DeviceCommandSend); + + errorCode = bulkCommand(command); + if (errorCode < 0) { + qWarning() << "Sending bulk command failed: " << libUsbErrorString(errorCode); + emit m_control->communicationError(); + return false; + } else + command->pending = false; + } + command = command->next; + } + + // Send all pending control commands + ControlCommand *controlCommand = firstControlCommand; + while (controlCommand) { + if (controlCommand->pending) { + DBGNOTIFY(QString("%1, %2") + .arg(QMetaEnum::fromType().valueToKey((int)controlCommand->code)) + .arg(hexDump(controlCommand->data(), controlCommand->size())), + Debug::NotificationType::DeviceCommandSend); + + errorCode = m_device->controlWrite(controlCommand); + if (errorCode < 0) { + qWarning("Sending control command %2x failed: %s", (uint8_t)controlCommand->code, + libUsbErrorString(errorCode).toLocal8Bit().data()); + + if (errorCode == LIBUSB_ERROR_NO_DEVICE) { + emit m_control->communicationError(); + return false; + } + } else + controlCommand->pending = false; + } + controlCommand = controlCommand->next; + } + return true; +} + +void DsoCommandQueue::manualCommand(bool isBulk, Hantek::BulkCode bulkCode, Hantek::ControlCode controlCode, + const QByteArray &data) { + if (!m_device->isConnected()) return; + QMutexLocker l(&m_commandMutex); + + if (isBulk) { + BulkCommand *c = modifyCommand(bulkCode); + if (!c) return; + memcpy(c->data(), data.data(), std::min((size_t)data.size(), c->size())); + } else { + ControlCommand *c = modifyCommand(controlCode); + if (!c) return; + memcpy(c->data(), data.data(), std::min((size_t)data.size(), c->size())); + } +} diff --git a/openhantek/src/hantekdso/dsocommandqueue.h b/openhantek/src/hantekdso/dsocommandqueue.h new file mode 100644 index 00000000..d2cf1b7b --- /dev/null +++ b/openhantek/src/hantekdso/dsocommandqueue.h @@ -0,0 +1,86 @@ +// SPDX-License-Identifier: GPL-2.0+ + +#pragma once + +#include + +#include "hantekprotocol/bulkStructs.h" +#include "hantekprotocol/controlStructs.h" + +class DsoControl; +class USBDevice; +namespace Dso { +struct ModelSpec; +} + +/** + * Maintains a usb-bulk and usb-control command queue. To prevent hot-path runtime allocations, + * you need to add the necessary commands first (use addCommand(...)), before using them with + * getCommand() or modifyCommand(). + */ +class DsoCommandQueue : public QObject { + public: + DsoCommandQueue(const Dso::ModelSpec *spec, USBDevice *device,DsoControl *control); + ~DsoCommandQueue(); + + /** + * \brief Add a supported command. This is usually called from a model class within "models/..". + * + * If you do not add a command object and access the command via the command-code later on, + * the application will crash! + * + * @param newCommand A command object + * @param pending If true, the command will be send as soon as the dso sample-fetch loop starts. + */ + void addCommand(BulkCommand *newCommand, bool pending = false); + + template T *modifyCommand(Hantek::BulkCode code) { + T *t = static_cast(command[(uint8_t)code]); + if (t) t->pending = true; + return t; + } + + inline const BulkCommand *getCommand(Hantek::BulkCode code) const { return command[(uint8_t)code]; } + + void addCommand(ControlCommand *newCommand, bool pending = false); + + template T *modifyCommand(Hantek::ControlCode code) { + T *t = static_cast(control[(uint8_t)code]); + if (t) t->pending = true; + return t; + } + + inline bool isCommandSupported(Hantek::ControlCode code) const { return control[(uint8_t)code]; } + inline bool isCommandSupported(Hantek::BulkCode code) const { return command[(uint8_t)code]; } + + const ControlCommand *getCommand(Hantek::ControlCode code) const { return control[(uint8_t)code]; } + + /// Send all pending control and bulk commands. Issued by the run() loop. + bool sendPendingCommands(); + + /// \brief Send a bulk command to the oscilloscope. + /// The hantek protocol requires to send a special control command first, this is handled by this method. + /// + /// \param command The command, that should be sent. + /// \param attempts The number of attempts, that are done on timeouts. + /// \return Number of sent bytes on success, libusb error code on error. + int bulkCommand(const std::vector *command, int attempts = HANTEK_ATTEMPTS) const; + public slots: + /// \brief Sends bulk/control commands directly. + /// \param data The command bytes. + /// \return See ::Dso::ErrorCode. + void manualCommand(bool isBulk, Hantek::BulkCode bulkCode, Hantek::ControlCode controlCode, const QByteArray &data); + + protected: + QMutex m_commandMutex; ///< Makes command/control set-methods and enumerations thread-safe + private: + /// Pointers to bulk/control commands + BulkCommand *command[255] = {0}; + BulkCommand *firstBulkCommand = nullptr; + ControlCommand *control[255] = {0}; + ControlCommand *firstControlCommand = nullptr; + const bool m_useControlNoBulk; + + DsoControl *m_control; + USBDevice *m_device; ///< The USB device for the oscilloscope +}; diff --git a/openhantek/src/hantekdso/dsocontrol.cpp b/openhantek/src/hantekdso/dsocontrol.cpp new file mode 100644 index 00000000..aa3f54ae --- /dev/null +++ b/openhantek/src/hantekdso/dsocontrol.cpp @@ -0,0 +1,893 @@ +// SPDX-License-Identifier: GPL-2.0+ + +#include +#include +#include +#include + +#include +#include +#include +#include + +#include "dsocontrol.h" +#include "dsoloop.h" +#include "dsomodel.h" +#include "usb/usbdevice.h" + +#ifdef WIN32 +#include +#else +#include // for ntohs, network to host order for shorts: big to little endian +#endif + +using namespace Hantek; +using namespace Dso; + +#ifdef DEBUG +#define DBGNOTIFY(x, y) emit debugMessage(x, y) +#else +#define DBGNOTIFY(x, y) +#endif + +DsoControl::DsoControl(USBDevice *device, std::shared_ptr deviceSettings) + : DsoCommandQueue(deviceSettings->spec, device, this), device(device), m_specification(deviceSettings->spec), + m_settings(deviceSettings), m_channelUsage(m_specification->channels), m_loop(new DsoLoop(deviceSettings, this)) { + if (device == nullptr) throw new std::runtime_error("No usb device for HantekDsoControl"); + + qRegisterMetaType(); + qRegisterMetaType>("std::vector"); + qRegisterMetaType("Hantek::BulkCode"); + qRegisterMetaType("Hantek::ControlCode"); + qRegisterMetaType("Samplerate"); + qRegisterMetaType("Dso::RecordLength"); + + if (m_specification->fixedUSBinLength) device->overwriteInPacketLength(m_specification->fixedUSBinLength); + // Apply special requirements by the devices model + device->getModel()->applyRequirements(this); + + // Check for gain level definitions + bool error = m_specification->calibration.size() != m_specification->channels; + if (!error) + for (ChannelID channelId = 0; channelId < m_specification->channels; ++channelId) { + error |= m_specification->calibration[channelId].size() != m_specification->gain.size(); + } + + if (error) { + qWarning() << "Model definition is faulty. Please check voltageLimit and gain levels to be defined for at " + "least HANTEK_GAIN_STEPS"; + } +} + +void DsoControl::start() { + DBGNOTIFY("Init device", Debug::NotificationType::DSOControl); + + retrieveOffsetCalibrationData(); + retrieveConnectionSpeed(); + + for (ChannelID channelId = 0; channelId < m_settings->voltage.size(); ++channelId) { + setCoupling(channelId, m_settings->voltage[channelId]->coupling(m_specification)); + setGain(channelId, m_settings->voltage[channelId]->gainStepIndex(), true); // sets offset as well + setTriggerOffset(channelId, m_settings->voltage[channelId]->triggerLevel(), true); + } + + // connect to use-channel signals + connect(&m_channelUsage, &ChannelUsage::usedChanged, this, &DsoControl::checkChannelUsage); + + checkChannelUsage(); + setRecordLengthByIndex(m_settings->recordLengthId()); + setTriggerMode(m_settings->trigger.mode()); + setPretriggerPosition(m_settings->trigger.position(), true); + setTriggerSlope(m_settings->trigger.slope()); + setTriggerSource(m_settings->trigger.special(), m_settings->trigger.source()); + restoreTargets(); + m_loop->run(); +} + +double DsoControl::minSamplerate() const { + return m_specification->normalSamplerate.minSamplerate(m_settings->recordLengthId()); +} + +double DsoControl::maxSamplerate() const { return m_settings->limits->maxSamplerate(m_settings->recordLengthId()); } + +double DsoControl::maxSingleChannelSamplerate() const { + return m_specification->normalSamplerate.maxSamplerate(m_settings->recordLengthId()); +} + +double DsoControl::computeTimebase(double samplerate) const { + unsigned sampleCount = m_settings->getRecordLength(); + if (m_specification->isSoftwareTriggerDevice) sampleCount -= m_settings->trigger.swSampleMargin(); + return (double)(sampleCount) / samplerate; +} + +Dso::ErrorCode DsoControl::retrieveOffsetCalibrationData() { + // Get channel level data + Hantek::ControlGetLimits cmdGetLimits(m_specification->channels); + + int readBytes = device->controlRead(&cmdGetLimits); + if (readBytes < 0) { + const QString errMsg = "Couldn't get channel level data from oscilloscope"; + qWarning() << errMsg; + DBGNOTIFY(errMsg, Debug::NotificationType::DSOControl); + emit communicationError(); + return Dso::ErrorCode::CONNECTION; + } + + if (readBytes != sizeof(Hantek::ControlGetLimits::OffsetsPerGainStep) * m_specification->channels) { + DBGNOTIFY("Offset calibration data not supported", Debug::NotificationType::DSOControl); + return Dso::ErrorCode::UNSUPPORTED; + } + + DBGNOTIFY("Offset calibration data received", Debug::NotificationType::DSOControl); + + // Access model specification in write-mode. Only retrieveOffsetCalibrationData and the (self-)calibration + // are allowed to do so. + std::vector &cal = const_cast(m_specification)->calibration; + Hantek::ControlGetLimits::OffsetsPerGainStep *data = cmdGetLimits.offsetLimit.get(); + for (ChannelID channelId = 0; channelId < m_specification->channels; ++channelId) { + for (unsigned gainId = 0; gainId < Hantek::ControlGetLimits::HANTEK_GAIN_STEPS; ++gainId) { + // Convert little->big endian if necessary + cal[channelId][gainId].offsetStart = ntohs(data[channelId].step[gainId].start); + cal[channelId][gainId].offsetEnd = ntohs(data[channelId].step[gainId].end); + } + } + + return Dso::ErrorCode::NONE; +} + +std::pair DsoControl::retrieveCaptureState() const { + int errorCode; + + if (!m_specification->supportsCaptureState) return std::make_pair(CAPTURE_READY, 0); + + errorCode = bulkCommand(getCommand(BulkCode::GETCAPTURESTATE), 1); + if (errorCode < 0) { + qWarning() << "Getting capture state failed: " << libUsbErrorString(errorCode); + return std::make_pair(CAPTURE_ERROR, 0); + } + + BulkResponseGetCaptureState response; + errorCode = device->bulkRead(&response); + if (errorCode < 0) { + qWarning() << "Getting capture state failed: " << libUsbErrorString(errorCode); + return std::make_pair(CAPTURE_ERROR, 0); + } + + return std::make_pair((int)response.getCaptureState(), response.getTriggerPoint()); +} + +ErrorCode DsoControl::retrieveSamples(unsigned &previousSampleCount) { + int errorCode; + if (!m_specification->useControlNoBulk) { + // Request data + errorCode = bulkCommand(getCommand(BulkCode::GETDATA), 1); + } else { + errorCode = device->controlWrite(getCommand(ControlCode::ACQUIRE_DATA)); + } + if (errorCode <= 0) { + qWarning() << "Getting sample data failed: " << libUsbErrorString(errorCode); + emit communicationError(); + return ErrorCode::PARAMETER; + } + + unsigned totalSampleCount = m_settings->getSampleCount(); + + // To make sure no samples will remain in the scope buffer, also check the + // sample count before the last sampling started + if (totalSampleCount < previousSampleCount) { + std::swap(totalSampleCount, previousSampleCount); + } else { + previousSampleCount = totalSampleCount; + } + + unsigned dataLength = (m_specification->sampleSize > 8) ? totalSampleCount * 2 : totalSampleCount; + + // Save raw data to temporary buffer + m_rawdata.resize(dataLength); + int errorcode = device->bulkReadMulti(m_rawdata.data(), dataLength); + if (errorcode < 0) { + DBGNOTIFY(QString("Getting sample data failed: %1").arg(libUsbErrorString(errorcode)), + Debug::NotificationType::DSOControl); + return ErrorCode::PARAMETER; + } + m_rawdata.resize((size_t)errorcode); + + static unsigned id = 0; + DBGNOTIFY(QString("Received packet %1").arg(id++), Debug::NotificationType::DSOLoop); + + return ErrorCode::NONE; +} + +ErrorCode DsoControl::retrieveConnectionSpeed() { + int errorCode; + ControlGetSpeed response; + errorCode = device->controlRead(&response); + if (errorCode < 0) { + qWarning() << "Retrieve connection speed failed" << libusb_error_name(errorCode); + return ErrorCode::UNEXPECTED; + } + + if (response.getSpeed() == ConnectionSpeed::FULLSPEED) + m_settings->m_packetSize = 64; + else if (response.getSpeed() == ConnectionSpeed::HIGHSPEED) + m_settings->m_packetSize = 512; + else { + qWarning() << "Unknown USB speed. Please correct source code in DsoControl::retrieveConnectionSpeed()"; + throw new std::runtime_error("Unknown USB speed"); + } + return ErrorCode::NONE; +} + +DsoControl::BestSamplerateResult DsoControl::computeBestSamplerate(double samplerate, bool maximum) const { + BestSamplerateResult r; + + // Abort if the input value is invalid + if (samplerate <= 0.0) return r; + + // When possible, enable fast rate if it is required to reach the requested + // samplerate + r.fastrate = m_specification->supportsFastRate && (m_channelUsage.countUsedChannels() <= 1) && + (samplerate > maxSingleChannelSamplerate()); + + // Get samplerate specifications for this mode and model + const ControlSamplerateLimits *limits; + if (r.fastrate) + limits = &(m_specification->fastrateSamplerate); + else + limits = &(m_specification->normalSamplerate); + + // Get downsampling factor that would provide the requested rate + r.downsampler = limits->computeDownsampler(m_settings->recordLengthId(), samplerate); + // Base samplerate sufficient, or is the maximum better? + if (r.downsampler < 1.0 && (samplerate <= limits->maxSamplerate(m_settings->recordLengthId()) || !maximum)) { + r.downsampler = 0.0; + r.samplerate = limits->maxSamplerate(m_settings->recordLengthId()); + } else { + switch (m_specification->cmdSetSamplerate) { + case BulkCode::SETTRIGGERANDSAMPLERATE: + // DSO-2090 supports the downsampling factors 1, 2, 4 and 5 using + // valueFast or all even values above using valueSlow + if ((maximum && r.downsampler <= 5.0) || (!maximum && r.downsampler < 6.0)) { + // valueFast is used + if (maximum) { + // The samplerate shall not be higher, so we round up + r.downsampler = ceil(r.downsampler); + if (r.downsampler > 2.0) // 3 and 4 not possible with the DSO-2090 + r.downsampler = 5.0; + } else { + // The samplerate shall not be lower, so we round down + r.downsampler = floor(r.downsampler); + if (r.downsampler > 2.0 && r.downsampler < 5.0) // 3 and 4 not possible with the DSO-2090 + r.downsampler = 2.0; + } + } else { + // valueSlow is used + if (maximum) { + r.downsampler = ceil(r.downsampler / 2.0) * 2.0; // Round up to next even value + } else { + r.downsampler = floor(r.downsampler / 2.0) * 2.0; // Round down to next even value + } + if (r.downsampler > 2.0 * 0x10001) // Check for overflow + r.downsampler = 2.0 * 0x10001; + } + break; + + case BulkCode::CSETTRIGGERORSAMPLERATE: + // DSO-5200 may not supports all downsampling factors, requires testing + if (maximum) { + r.downsampler = ceil(r.downsampler); // Round up to next integer value + } else { + r.downsampler = floor(r.downsampler); // Round down to next integer value + } + break; + + case BulkCode::ESETTRIGGERORSAMPLERATE: + // DSO-2250 doesn't have a fast value, so it supports all downsampling + // factors + if (maximum) { + r.downsampler = ceil(r.downsampler); // Round up to next integer value + } else { + r.downsampler = floor(r.downsampler); // Round down to next integer value + } + break; + + default: + return r; + } + + // Limit maximum downsampler value to avoid overflows in the sent commands + if (r.downsampler > limits->maxDownsampler) r.downsampler = limits->maxDownsampler; + r.samplerate = limits->base / r.downsampler / limits->recordLengths[m_settings->recordLengthId()].bufferDivider; + } + return r; +} + +unsigned DsoControl::updateSamplerate(unsigned downsampler, bool fastRate) { + fastRate |= m_specification->supportsFastRate; + // Get samplerate limits + const ControlSamplerateLimits *limits = + fastRate ? &m_specification->normalSamplerate : &m_specification->fastrateSamplerate; + // Update settings + bool fastRateChanged = m_settings->limits != limits; + if (fastRateChanged) { m_settings->limits = limits; } + + // Set the calculated samplerate + switch (m_specification->cmdSetSamplerate) { + case BulkCode::SETTRIGGERANDSAMPLERATE: { + short int downsamplerValue = 0; + unsigned char samplerateId = 0; + bool downsampling = false; + + if (downsampler <= 5) { + // All dividers up to 5 are done using the special samplerate IDs + if (downsampler == 0 && limits->base >= limits->max) + samplerateId = 1; + else if (downsampler <= 2) + samplerateId = downsampler; + else { // Downsampling factors 3 and 4 are not supported + samplerateId = 3; + downsampler = 5; + downsamplerValue = (short int)0xffff; + } + } else { + // For any dividers above the downsampling factor can be set directly + downsampler &= ~0x0001; // Only even values possible + downsamplerValue = (short int)(0x10001 - (downsampler >> 1)); + + downsampling = true; + } + + // Pointers to needed commands + BulkSetTriggerAndSamplerate *commandSetTriggerAndSamplerate = + modifyCommand(BulkCode::SETTRIGGERANDSAMPLERATE); + + // Store if samplerate ID or downsampling factor is used + commandSetTriggerAndSamplerate->setDownsamplingMode(downsampling); + // Store samplerate ID + commandSetTriggerAndSamplerate->setSamplerateId(samplerateId); + // Store downsampling factor + commandSetTriggerAndSamplerate->setDownsampler(downsamplerValue); + // Set fast rate when used + commandSetTriggerAndSamplerate->setFastRate(false /*fastRate*/); + + break; + } + case BulkCode::CSETTRIGGERORSAMPLERATE: { + // Split the resulting divider into the values understood by the device + // The fast value is kept at 4 (or 3) for slow sample rates + long int valueSlow = qMax(((long int)downsampler - 3) / 2, (long int)0); + unsigned char valueFast = downsampler - valueSlow * 2; + + // Pointers to needed commands + BulkSetSamplerate5200 *commandSetSamplerate5200 = + modifyCommand(BulkCode::CSETTRIGGERORSAMPLERATE); + BulkSetTrigger5200 *commandSetTrigger5200 = + modifyCommand(BulkCode::ESETTRIGGERORSAMPLERATE); + + // Store samplerate fast value + commandSetSamplerate5200->setSamplerateFast(4 - valueFast); + // Store samplerate slow value (two's complement) + commandSetSamplerate5200->setSamplerateSlow(valueSlow == 0 ? 0 : 0xffff - valueSlow); + // Set fast rate when used + commandSetTrigger5200->setFastRate(fastRate); + + break; + } + case BulkCode::ESETTRIGGERORSAMPLERATE: { + // Pointers to needed commands + BulkSetSamplerate2250 *commandSetSamplerate2250 = + modifyCommand(BulkCode::ESETTRIGGERORSAMPLERATE); + + bool downsampling = downsampler >= 1; + // Store downsampler state value + commandSetSamplerate2250->setDownsampling(downsampling); + // Store samplerate value + commandSetSamplerate2250->setSamplerate(downsampler > 1 ? 0x10001 - downsampler : 0); + // Set fast rate when used + commandSetSamplerate2250->setFastRate(fastRate); + + break; + } + default: + return UINT_MAX; + } + + m_settings->downsampler = downsampler; + double samplerate; + if (downsampler) + samplerate = m_settings->limits->samplerate(m_settings->recordLengthId(), downsampler); + else + samplerate = m_settings->limits->maxSamplerate(m_settings->recordLengthId()); + + const double timebase = computeTimebase(samplerate); + m_settings->updateCurrentSamplerate(samplerate, timebase, UINT_MAX); + + // Update dependencies + setPretriggerPosition(m_settings->trigger.position()); + + // Emit signals for changed settings + if (fastRateChanged) { + std::vector r; + emit m_settings->availableRecordLengthsChanged(m_settings->limits->recordLengths); + emit m_settings->recordLengthChanged(m_settings->recordLengthId()); + } + + emit m_settings->samplerateChanged(m_settings->samplerate()); + + return downsampler; +} + +void DsoControl::restoreTargets() { + switch (m_settings->samplerateSource()) { + case SamplerateSource::Samplerrate: + setSamplerate(m_settings->target().samplerate); + break; + case SamplerateSource::FixedSamplerate: + setFixedSamplerate(m_settings->target().fixedSamperateId); + break; + case SamplerateSource::Duration: + setRecordTime(m_settings->target().timebase); + break; + } +} + +void DsoControl::notifySamplerateLimits() { + if (m_specification->isFixedSamplerateDevice) { + emit m_settings->fixedSampleratesChanged(m_specification->fixedSampleRates); + } else { + emit m_settings->samplerateLimitsChanged(minSamplerate(), maxSamplerate()); + } +} + +Dso::ErrorCode DsoControl::setRecordLengthByIndex(RecordLengthID index) { + if (!device->isConnected()) return Dso::ErrorCode::CONNECTION; + const auto &recLengths = m_settings->limits->recordLengths; + if (index >= recLengths.size()) return Dso::ErrorCode::PARAMETER; + if (recLengths[index].recordLength == ROLL_RECORDLEN) return Dso::ErrorCode::PARAMETER; + QMutexLocker l(&m_commandMutex); + + switch (m_specification->cmdSetRecordLength) { + case BulkCode::SETTRIGGERANDSAMPLERATE: + modifyCommand(BulkCode::SETTRIGGERANDSAMPLERATE)->setRecordLength((uint8_t)index); + break; + + case BulkCode::DSETBUFFER: + if (m_specification->cmdSetPretrigger == BulkCode::FSETBUFFER) { + modifyCommand(BulkCode::DSETBUFFER)->setRecordLength((uint8_t)index); + } else { + // SetBuffer5200 bulk command for record length + BulkSetBuffer5200 *commandSetBuffer5200 = modifyCommand(BulkCode::DSETBUFFER); + + commandSetBuffer5200->setUsedPre(DTriggerPositionUsed::ON); + commandSetBuffer5200->setUsedPost(DTriggerPositionUsed::ON); + commandSetBuffer5200->setRecordLength((uint8_t)index); + } + + break; + + default: + return Dso::ErrorCode::PARAMETER; + } + + // Check if the divider has changed and adapt samplerate limits accordingly + bool bDividerChanged = recLengths[index].bufferDivider != recLengths[m_settings->recordLengthId()].bufferDivider; + + m_settings->setRecordLengthId(index); + + if (bDividerChanged) { + notifySamplerateLimits(); + // Samplerate dividers changed, recalculate it + restoreTargets(); + setPretriggerPosition(m_settings->trigger.position()); + } + + return Dso::ErrorCode::NONE; +} + +Dso::ErrorCode DsoControl::setFixedSamplerate(unsigned samplerateId) { + if (!m_specification->isFixedSamplerateDevice) return Dso::ErrorCode::PARAMETER; + if (samplerateId > m_specification->fixedSampleRates.size()) return Dso::ErrorCode::PARAMETER; + QMutexLocker l(&m_commandMutex); + + m_settings->updateTarget(SamplerateSource::FixedSamplerate).fixedSamperateId = samplerateId; + + modifyCommand(ControlCode::SETTIMEDIV) + ->setDiv(m_specification->fixedSampleRates[samplerateId].id); + + const double samplerate = m_specification->fixedSampleRates[samplerateId].samplerate; + const double timebase = computeTimebase(samplerate); + m_settings->updateCurrentSamplerate(samplerate, timebase, samplerateId); + + // Update dependencies + setPretriggerPosition(m_settings->trigger.position()); + + // Check for Roll mode + emit m_settings->samplerateChanged(m_settings->samplerate()); + + return Dso::ErrorCode::NONE; +} + +Dso::ErrorCode DsoControl::setSamplerate(double samplerate) { + if (!device->isConnected()) return Dso::ErrorCode::CONNECTION; + if (m_specification->isFixedSamplerateDevice) return Dso::ErrorCode::PARAMETER; + QMutexLocker l(&m_commandMutex); + + m_settings->updateTarget(SamplerateSource::Samplerrate).samplerate = samplerate; + + // What is the nearest, at least as high samplerate the scope can provide? + BestSamplerateResult r = computeBestSamplerate(samplerate, false); + + // Set the calculated samplerate + if (updateSamplerate(r.downsampler, r.fastrate) == UINT_MAX) + return Dso::ErrorCode::PARAMETER; + else { + return Dso::ErrorCode::NONE; + } +} + +Dso::ErrorCode DsoControl::setRecordTime(double duration) { + if (!device->isConnected()) return Dso::ErrorCode::CONNECTION; + QMutexLocker l(&m_commandMutex); + + m_settings->updateTarget(SamplerateSource::Duration).timebase = duration; + + if (!m_specification->isFixedSamplerateDevice) { + // Calculate the maximum samplerate that would still provide the requested + // duration + double maxSamplerate = m_specification->normalSamplerate.samplerate(m_settings->recordLengthId(), duration); + + // When possible, enable fast rate if the record time can't be set that low + // to improve resolution + bool fastRate = m_specification->supportsFastRate && (m_channelUsage.countUsedChannels() <= 1) && + (maxSamplerate >= + m_specification->fastrateSamplerate.samplerate(m_settings->recordLengthId(), (unsigned)1)); + + // What is the nearest, at most as high samplerate the scope can provide? + unsigned downsampler = 0; + + // Set the calculated samplerate + if (this->updateSamplerate(downsampler, fastRate) == UINT_MAX) + return Dso::ErrorCode::PARAMETER; + else { + return Dso::ErrorCode::NONE; + } + } else { + unsigned sampleCount = m_settings->getRecordLength(); + // Ensure that at least 1/2 of remaining samples are available for SW trigger algorithm + if (m_specification->isSoftwareTriggerDevice) sampleCount -= m_settings->trigger.swSampleMargin(); + unsigned samplerateId = 0; + double diff = UINT_MAX; + for (unsigned i = 0; i < m_specification->fixedSampleRates.size(); ++i) { + double d = std::abs(m_specification->fixedSampleRates[i].samplerate * duration - sampleCount); + if (d < diff) { + diff = d; + samplerateId = i; + } + } + + // Usable sample value + modifyCommand(ControlCode::SETTIMEDIV) + ->setDiv(m_specification->fixedSampleRates[samplerateId].id); + + const double samplerate = m_specification->fixedSampleRates[samplerateId].samplerate; + const double timebase = computeTimebase(samplerate); + m_settings->updateCurrentSamplerate(samplerate, timebase, samplerateId); + + // Update dependencies + setPretriggerPosition(m_settings->trigger.position()); + + emit m_settings->samplerateChanged(m_settings->samplerate()); + return Dso::ErrorCode::NONE; + } +} + +Dso::ErrorCode DsoControl::checkChannelUsage() { + if (!device->isConnected()) return Dso::ErrorCode::CONNECTION; + + // Calculate the UsedChannels field for the command + UsedChannels usedChannels = m_channelUsage.isUsed(0) ? UsedChannels::USED_CH1 : UsedChannels::USED_NONE; + + if (m_channelUsage.isUsed(1)) { + if (m_channelUsage.isUsed(0)) { + usedChannels = UsedChannels::USED_CH1CH2; + } else { + // DSO-2250 uses a different value for channel 2 + if (m_specification->cmdSetChannels == BulkCode::BSETCHANNELS) + usedChannels = UsedChannels::BUSED_CH2; + else + usedChannels = UsedChannels::USED_CH2; + } + } + + switch (m_specification->cmdSetChannels) { + case BulkCode::SETTRIGGERANDSAMPLERATE: { + // SetTriggerAndSamplerate bulk command for trigger source + modifyCommand(BulkCode::SETTRIGGERANDSAMPLERATE) + ->setUsedChannels((uint8_t)usedChannels); + break; + } + case BulkCode::BSETCHANNELS: { + // SetChannels2250 bulk command for active channels + modifyCommand(BulkCode::BSETCHANNELS)->setUsedChannels((uint8_t)usedChannels); + break; + } + case BulkCode::ESETTRIGGERORSAMPLERATE: { + // SetTrigger5200s bulk command for trigger source + modifyCommand(BulkCode::ESETTRIGGERORSAMPLERATE)->setUsedChannels((uint8_t)usedChannels); + break; + } + default: + break; + } + + notifySamplerateLimits(); + + return Dso::ErrorCode::NONE; +} + +Dso::ErrorCode DsoControl::setCoupling(ChannelID channel, Dso::Coupling coupling) { + if (!device->isConnected()) return Dso::ErrorCode::CONNECTION; + if (channel >= m_specification->channels) return Dso::ErrorCode::PARAMETER; + QMutexLocker l(&m_commandMutex); + + unsigned index = INT_MAX; + for (unsigned i = 0; i < m_specification->couplings.size(); ++i) + if (m_specification->couplings[i] == coupling) { + index = i; + break; + } + if (index == INT_MAX) return Dso::ErrorCode::PARAMETER; + + // SetRelays control command for coupling relays + if (m_specification->supportsCouplingRelays) { + modifyCommand(ControlCode::SETRELAYS)->setCoupling(channel, coupling != Dso::Coupling::AC); + } + + m_settings->voltage[channel]->setCouplingIndex(index); + + return Dso::ErrorCode::NONE; +} + +Dso::ErrorCode DsoControl::setGain(ChannelID channel, unsigned gainId, bool overwrite) { + if (!device->isConnected()) return Dso::ErrorCode::CONNECTION; + if (channel >= m_specification->channels) return Dso::ErrorCode::PARAMETER; + if (gainId >= m_specification->gain.size()) return Dso::ErrorCode::PARAMETER; + Channel *c = m_settings->voltage[channel]; + if (!overwrite && c->gainStepIndex() == gainId) return Dso::ErrorCode::UNCHANGED; + QMutexLocker l(&m_commandMutex); + + const ControlSpecificationGainLevel &gain = m_specification->gain[gainId]; + + if (m_specification->useControlNoBulk) { + if (channel == 0) { + modifyCommand(ControlCode::SETVOLTDIV_CH1)->setDiv(gain.gainIdentificator); + } else if (channel == 1) { + modifyCommand(ControlCode::SETVOLTDIV_CH2)->setDiv(gain.gainIdentificator); + } else + qDebug("%s: Unsuported channel: %i\n", __func__, channel); + } else { + modifyCommand(BulkCode::SETGAIN)->setGain(channel, gain.gainIdentificator); + + // SetRelays control command for gain relays + ControlSetRelays *controlSetRelays = modifyCommand(ControlCode::SETRELAYS); + controlSetRelays->setBelow1V(channel, gainId < 3); // TODO That needs to be changed to rely on spec information + controlSetRelays->setBelow100mV(channel, gainId < 6); + } + + c->setGainStepIndex(gainId); + setOffset(channel, c->offset(), overwrite); + setTriggerOffset(channel, c->triggerLevel(), overwrite); + + return Dso::ErrorCode::NONE; +} + +Dso::ErrorCode DsoControl::setOffset(ChannelID channel, double offset, bool overwrite) { + if (!device->isConnected()) return Dso::ErrorCode::CONNECTION; + if (offset < -1 || offset > 1) return Dso::ErrorCode::PARAMETER; + if (channel >= m_specification->channels) return Dso::ErrorCode::PARAMETER; + Channel *c = m_settings->voltage[channel]; + if (!overwrite && c->offset() == offset) return Dso::ErrorCode::UNCHANGED; + QMutexLocker l(&m_commandMutex); + + if (m_specification->supportsOffset) { + const auto &channelOffLimit = m_specification->calibration[channel][c->gainStepIndex()]; + const uint16_t minimum = channelOffLimit.offsetStart; + const uint16_t maximum = channelOffLimit.offsetEnd; + const uint16_t range = maximum - minimum; + + // Offset is at the moment in range [-1.0,1.0] but we need it as [0.0,1.0] + const double normalizedOffset = (offset + 1) / 2.0; + // We are now in [0,1] but hardware wants it in [min,max] + const uint16_t offsetValue = (uint16_t)std::ceil(normalizedOffset * range + minimum); + modifyCommand(ControlCode::SETOFFSET)->setOffset(channel, offsetValue); + /// Due to uint16_ts limited resolution, + /// the hardware applied offset is a little off compared to the given offset. We need to store it as well + /// to later compensate for it on the received sampleset. + c->setOffset(offset, std::ceil(offset * range) / (double)range); + DBGNOTIFY(QString("HardOffset c:%1,l:%2").arg(channel).arg(c->offsetHardware()), + Debug::NotificationType::DSOControl); + } else { + c->setOffset(offset, 0); + DBGNOTIFY(QString("SoftOffset c:%1,l:%2").arg(channel).arg(c->offset()), Debug::NotificationType::DSOControl); + } + + return Dso::ErrorCode::NONE; +} + +Dso::ErrorCode DsoControl::setTriggerMode(Dso::TriggerMode mode) { + if (!device->isConnected()) return Dso::ErrorCode::CONNECTION; + QMutexLocker l(&m_commandMutex); + + m_settings->trigger.setMode(mode); + return Dso::ErrorCode::NONE; +} + +Dso::ErrorCode DsoControl::setTriggerSource(bool special, ChannelID channel) { + if (!device->isConnected()) return Dso::ErrorCode::CONNECTION; + QMutexLocker l(&m_commandMutex); + + if (m_specification->isSoftwareTriggerDevice) { + DBGNOTIFY(QString("TriggerSrc %1").arg(channel), Debug::NotificationType::DSOControl); + m_settings->trigger.setTriggerSource(channel, special); + return Dso::ErrorCode::NONE; + } + + if (!special && channel >= m_specification->channels) return Dso::ErrorCode::PARAMETER; + + if (special && channel >= m_specification->specialTriggerChannels.size()) return Dso::ErrorCode::PARAMETER; + + int hardwareID = special ? m_specification->specialTriggerChannels[channel].hardwareID : (int)channel; + + switch (m_specification->cmdSetTrigger) { + case BulkCode::SETTRIGGERANDSAMPLERATE: + // SetTriggerAndSamplerate bulk command for trigger source + modifyCommand(BulkCode::SETTRIGGERANDSAMPLERATE)->setTriggerSource(1 - hardwareID); + break; + + case BulkCode::CSETTRIGGERORSAMPLERATE: + // SetTrigger2250 bulk command for trigger source + modifyCommand(BulkCode::CSETTRIGGERORSAMPLERATE)->setTriggerSource(2 + hardwareID); + break; + + case BulkCode::ESETTRIGGERORSAMPLERATE: + // SetTrigger5200 bulk command for trigger source + modifyCommand(BulkCode::ESETTRIGGERORSAMPLERATE)->setTriggerSource(1 - hardwareID); + break; + + default: + return Dso::ErrorCode::UNSUPPORTED; + } + + // SetRelays control command for external trigger relay + modifyCommand(ControlCode::SETRELAYS)->setTrigger(special); + + DBGNOTIFY(QString("TriggerSrc %1").arg(channel), Debug::NotificationType::DSOControl); + m_settings->trigger.setTriggerSource(channel, special); + + // Apply trigger level of the new source + if (special) { + // SetOffset control command for changed trigger level + modifyCommand(ControlCode::SETOFFSET)->setTriggerLevel(0x7f); + } else + this->setTriggerOffset(channel, m_settings->voltage[channel]->triggerLevel()); + + return Dso::ErrorCode::NONE; +} + +Dso::ErrorCode DsoControl::setTriggerOffset(ChannelID channel, double offset, bool overwrite) { + if (!device->isConnected()) return Dso::ErrorCode::CONNECTION; + if (offset < -1 || offset > 1) return Dso::ErrorCode::PARAMETER; + if (channel >= m_specification->channels) return Dso::ErrorCode::PARAMETER; + + Channel *c = m_settings->voltage[channel]; + if (!overwrite && c->triggerLevel() == offset) return Dso::ErrorCode::UNCHANGED; + QMutexLocker l(&m_commandMutex); + + c->setTriggerOffset(offset); + + if (!m_specification->isSoftwareTriggerDevice) { + const auto &channelOffLimit = m_specification->calibration[channel][c->gainStepIndex()]; + const uint16_t minimum = channelOffLimit.offsetStart; + const uint16_t maximum = channelOffLimit.offsetEnd; + const uint16_t range = maximum - minimum; + + // Offset is at the moment in range [-1.0,1.0] but we need it as [0.0,1.0] + const double normalizedOffset = (offset + 1) / 2.0; + // We are now in [0,1] but hardware wants it in [min,max] + const uint16_t offsetValue = (uint16_t)std::ceil(normalizedOffset * range + minimum); + modifyCommand(ControlCode::SETOFFSET)->setTriggerLevel(offsetValue); + DBGNOTIFY(QString("HardTriggerLevel c:%1,l:%2").arg(channel).arg(offsetValue), + Debug::NotificationType::DSOControl); + } else { + DBGNOTIFY(QString("SoftTriggerLevel c:%1,l:%2").arg(channel).arg(c->triggerLevel()), + Debug::NotificationType::DSOControl); + } + + return Dso::ErrorCode::NONE; +} + +Dso::ErrorCode DsoControl::setTriggerSlope(Dso::Slope slope) { + if (!device->isConnected()) return Dso::ErrorCode::CONNECTION; + QMutexLocker l(&m_commandMutex); + + switch (m_specification->cmdSetTrigger) { + case BulkCode::SETTRIGGERANDSAMPLERATE: { + // SetTriggerAndSamplerate bulk command for trigger slope + modifyCommand(BulkCode::SETTRIGGERANDSAMPLERATE)->setTriggerSlope((uint8_t)slope); + break; + } + case BulkCode::CSETTRIGGERORSAMPLERATE: { + // SetTrigger2250 bulk command for trigger slope + modifyCommand(BulkCode::CSETTRIGGERORSAMPLERATE)->setTriggerSlope((uint8_t)slope); + break; + } + case BulkCode::ESETTRIGGERORSAMPLERATE: { + // SetTrigger5200 bulk command for trigger slope + modifyCommand(BulkCode::ESETTRIGGERORSAMPLERATE)->setTriggerSlope((uint8_t)slope); + break; + } + default: + if (!m_specification->isSoftwareTriggerDevice) return Dso::ErrorCode::UNSUPPORTED; + } + + DBGNOTIFY(QString("TriggerSlope %1").arg((int)slope), Debug::NotificationType::DSOControl); + m_settings->trigger.setSlope(slope); + return Dso::ErrorCode::NONE; +} + +void DsoControl::forceTrigger() { modifyCommand(BulkCode::FORCETRIGGER); } + +ErrorCode DsoControl::setPretriggerPosition(double position, bool overwrite) { + if (!device->isConnected()) return Dso::ErrorCode::CONNECTION; + if (!overwrite && m_settings->trigger.position() == position) return Dso::ErrorCode::NONE; + Samples positionSamples = position * m_settings->samplerate().samplerate; + QMutexLocker l(&m_commandMutex); + + // All trigger positions are measured in samples + unsigned recordLength = m_settings->getRecordLength(); + // Fast rate mode uses both channels + if (m_settings->isFastRate()) positionSamples /= m_specification->channels; + + switch (m_specification->cmdSetPretrigger) { + case BulkCode::SETTRIGGERANDSAMPLERATE: { + // Calculate the position value (Start point depending on record length) + unsigned triggerPosition = m_settings->isRollMode() ? 0x1 : 0x7ffff - recordLength + (unsigned)positionSamples; + + // SetTriggerAndSamplerate bulk command for trigger position + modifyCommand(BulkCode::SETTRIGGERANDSAMPLERATE) + ->setTriggerPosition(triggerPosition); + DBGNOTIFY(QString("TriggerPosition %1").arg(position), Debug::NotificationType::DSOControl); + break; + } + case BulkCode::FSETBUFFER: { + // Calculate the position values (Inverse, maximum is 0x7ffff) + unsigned positionPre = 0x7ffff - recordLength + (unsigned)positionSamples; + unsigned positionPost = 0x7ffff - (unsigned)positionSamples; + + // SetBuffer2250 bulk command for trigger position + BulkSetBuffer2250 *commandSetBuffer2250 = modifyCommand(BulkCode::FSETBUFFER); + commandSetBuffer2250->setTriggerPositionPre(positionPre); + commandSetBuffer2250->setTriggerPositionPost(positionPost); + DBGNOTIFY(QString("TriggerPosition %1").arg(position), Debug::NotificationType::DSOControl); + break; + } + case BulkCode::ESETTRIGGERORSAMPLERATE: { + // Calculate the position values (Inverse, maximum is 0xffff) + unsigned positionPre = 0xffff - recordLength + (unsigned)positionSamples; + unsigned positionPost = 0xffff - (unsigned)positionSamples; + + // SetBuffer5200 bulk command for trigger position + BulkSetBuffer5200 *commandSetBuffer5200 = modifyCommand(BulkCode::DSETBUFFER); + commandSetBuffer5200->setTriggerPositionPre((uint16_t)positionPre); + commandSetBuffer5200->setTriggerPositionPost((uint16_t)positionPost); + DBGNOTIFY(QString("TriggerPosition %1").arg(position), Debug::NotificationType::DSOControl); + break; + } + default: + if (!m_specification->isSoftwareTriggerDevice) return Dso::ErrorCode::UNSUPPORTED; + DBGNOTIFY(QString("SoftTriggerPosition %1").arg(position), Debug::NotificationType::DSOControl); + } + + m_settings->trigger.setPosition(position); + return Dso::ErrorCode::NONE; +} diff --git a/openhantek/src/hantekdso/dsocontrol.h b/openhantek/src/hantekdso/dsocontrol.h new file mode 100644 index 00000000..3d455e4f --- /dev/null +++ b/openhantek/src/hantekdso/dsocontrol.h @@ -0,0 +1,244 @@ +// SPDX-License-Identifier: GPL-2.0+ + +#pragma once + +#define NOMINMAX // disable windows.h min/max global methods +#include + +#include "channelusage.h" +#include "devicesettings.h" +#include "dsocommandqueue.h" +#include "dsosamples.h" +#include "errorcodes.h" +#include "modelspecification.h" +#include "states.h" +#include "utils/debugnotify.h" +#include "utils/printutils.h" + +#include "hantekprotocol/definitions.h" + +#include + +#include +#include +#include +#include +#include + +class USBDevice; +class DsoLoop; + +/// \brief The DsoControl abstraction layer for Hantek USB DSOs. +class DsoControl : public DsoCommandQueue { + Q_OBJECT + friend class DsoLoop; + friend class DsoCommandQueue; + + public: + /** + * Creates a dsoControl object. The actual event loop / timer is not started. + * You can optionally create a thread and move the created object to the + * thread. + * + * You need to call updateInterval() to start the timer. This is done implicitly + * if run() is called. + * + * DSO channels are not enabled by default. To enable a channel, + * use deviceSettings->voltage[channel]->addChannelUser(). + * + * @param device The usb device. No ownership is taken. + * @param deviceSettings Runtime device settings. You must provide this, but you can use + * a default constructed object. If no other consumer of the device settings exist, this + * class will clean it up. + */ + DsoControl(USBDevice *device, std::shared_ptr deviceSettings); + + /// Call this to initialize the device with the deviceSettings and start the processing, by calling run() + /// internally. + /// It is wise to move this class object to an own thread and call start() from there. + /// + /// To stop processing, just destruct this object, disconnect the usbdevice object or stop the corresponding thread. + void start(); + + /// Return a read-only device control settings pointer. Use the set- Methods on this class to change + /// device settings. You can save device settings and restore the device state by initializing this + /// class with a loaded device settings object. + inline const std::shared_ptr deviceSettings() const { return m_settings; } + + /// Return the device specification. This is a convenience function and returns the same + /// as getDevice()->getModel()->spec(). + inline const Dso::ModelSpec *specification() const { return m_specification; } + + /** + * @return Returns the management object responsible for channel usage + */ + inline Dso::ChannelUsage *channelUsage() { return &m_channelUsage; } + + /// \brief Get minimum samplerate for this oscilloscope. + /// \return The minimum samplerate for the current configuration in S/s. + double minSamplerate() const; + + /// \brief Get maximum samplerate for this oscilloscope. + /// \return The maximum samplerate for the current configuration in S/s. + double maxSamplerate() const; + + double maxSingleChannelSamplerate() const; + + /// Return the associated usb device. + inline const USBDevice *getDevice() const { return device; } + + inline DsoLoop *loopControl() { return m_loop.get(); } + + protected: // Non-public methods + double computeTimebase(double samplerate) const; + + /// Called right at the beginning to retrieve scope calibration data + Dso::ErrorCode retrieveOffsetCalibrationData(); + + /// \brief Gets the current state. + /// \return The current CaptureState of the oscilloscope. + std::pair retrieveCaptureState() const; + + /// \brief Retrieve sample data from the oscilloscope + Dso::ErrorCode retrieveSamples(unsigned &expectedSampleCount); + + /// \brief Gets the speed of the connection. + /// \return The ::ConnectionSpeed of the USB connection. + Dso::ErrorCode retrieveConnectionSpeed(); + + /// \brief The resulting tuple of the computeBestSamplerate() function + struct BestSamplerateResult { + unsigned downsampler = 0; + double samplerate = 0.0; + bool fastrate = false; + }; + /// \brief Calculate the nearest samplerate supported by the oscilloscope. + /// \param samplerate The target samplerate, that should be met as good as possible. + /// \param maximum The target samplerate is the maximum allowed when true, the + /// minimum otherwise. + /// \return Tuple: The nearest samplerate supported, 0.0 on error and downsampling factor. + BestSamplerateResult computeBestSamplerate(double samplerate, bool maximum = false) const; + + /// \brief Sets the samplerate based on the parameters calculated by + /// Control::getBestSamplerate. + /// \param downsampler The downsampling factor. + /// \param fastRate true, if one channel uses all buffers. + /// \return The downsampling factor that has been set. + unsigned updateSamplerate(unsigned downsampler, bool fastRate); + + /// \brief Restore the samplerate/timebase targets after divider updates. + void restoreTargets(); + + /// \brief Update the minimum and maximum supported samplerate. + void notifySamplerateLimits(); + + /// \brief Enables/disables filtering of the given channel. + /// + /// This may have influence on the sampling speed: Some scopes support a fast-mode, if only + /// a limited set of channels (usually only 1) is activated. + /// + /// This method is not meant to be used directly. Use the deviceSettings to enable/disable channels, + /// by "using" them. + /// + /// \param channel The channel that should be set. + /// \return See ::Dso::ErrorCode. + Dso::ErrorCode checkChannelUsage(); + + protected: // non-public variables + // Communication with device + USBDevice *device; ///< The USB device for the oscilloscope + + // Device setup + const Dso::ModelSpec *m_specification; ///< The specifications of the device + std::shared_ptr m_settings; ///< The current settings of the device + Dso::ChannelUsage m_channelUsage; + + // Raw sample cache + std::vector m_rawdata; + std::unique_ptr m_loop; + + public slots: + /// \brief Sets the size of the oscilloscopes sample buffer. + /// \param index The record length index that should be set. + Dso::ErrorCode setRecordLengthByIndex(RecordLengthID size); + + /// \brief Sets the samplerate of the oscilloscope. + /// \param samplerate The samplerate that should be met (S/s) + /// current samplerate. You cannot set a samplerate for a fixed samplerate device with + /// this method. Use setFixedSamplerate() instead. + Dso::ErrorCode setSamplerate(double samplerate); + + /// \brief Sets the samplerate of the oscilloscope for fixed samplerate devices. Does + /// nothing on a device that supports a ranged samplerate value. Check with + /// deviceSpecification->isFixedSamplerateDevice if in doubt. + /// \param samplerrateId The samplerate id + Dso::ErrorCode setFixedSamplerate(unsigned samplerrateId); + + /// \brief Sets the time duration of one aquisition by adapting the samplerate. + /// \param duration The record time duration that should be met (s) + Dso::ErrorCode setRecordTime(double duration); + + /// \brief Set the coupling for the given channel. + /// \param channel The channel that should be set. + /// \param coupling The new coupling for the channel. + /// \return See ::Dso::ErrorCode. + Dso::ErrorCode setCoupling(ChannelID channel, Dso::Coupling coupling); + + /// \brief Sets the gain for the given channel. + /// Get the actual gain by specification.gainSteps[hardwareGainIndex] + /// \param channel The channel that should be set. + /// \param hardwareGainIndex The gain index that refers to a gain value, defined by the device model. + /// \return See ::Dso::ErrorCode. + Dso::ErrorCode setGain(ChannelID channel, unsigned hardwareGainIndex, bool overwrite = false); + + /// \brief Set the offset for the given channel. + /// Get the actual offset for the channel from devicesettings.voltage[channel].offsetReal + /// \param channel The channel that should be set. + /// \param offset The new offset value [-1.0,1.0]. Default is 0. + /// \return See ::Dso::ErrorCode. + Dso::ErrorCode setOffset(ChannelID channel, double offset, bool overwrite = false); + + /// \brief Set the trigger mode. + /// \return See ::Dso::ErrorCode. + Dso::ErrorCode setTriggerMode(Dso::TriggerMode mode); + + /// \brief Set the trigger source. + /// \param special true for a special channel (EXT, ...) as trigger source. + /// \param id The number of the channel, that should be used as trigger. + /// \return See ::Dso::ErrorCode. + Dso::ErrorCode setTriggerSource(bool special, ChannelID channel); + + /// \brief Set the trigger level. + /// \param channel The channel that should be set. + /// \param level The new trigger offset value [-1.0,1.0]. Default is 0. + /// \return The trigger level that has been set, ::Dso::ErrorCode on error. + Dso::ErrorCode setTriggerOffset(ChannelID channel, double offset, bool overwrite = false); + + /// \brief Set the trigger slope. + /// \param slope The Slope that should cause a trigger. + /// \return See ::Dso::ErrorCode. + Dso::ErrorCode setTriggerSlope(Dso::Slope slope); + + /// \brief Set the pre-trigger sample range in percentage. + /// + /// A sample set is longer than what is displayed on screen and only the part where the hard- or software trigger + /// found a matching signal slope is shown. To move the visible area, this method can be used by providing a + /// percentage value. + /// + /// \param position The new pre-trigger position [0.0,1.0]. Default is 0. + /// \return See ::Dso::ErrorCode. + Dso::ErrorCode setPretriggerPosition(double position, bool overwrite = false); + + /// \brief Forces a hardware-trigger to trigger although the condition is not met + /// Does nothing on software-trigger devices. + void forceTrigger(); + + signals: + void samplingStatusChanged(bool enabled); ///< The oscilloscope started/stopped sampling/waiting for trigger + void samplesAvailable(const DSOsamples *samples); ///< New sample data is available + void communicationError() const; ///< USB device error (disconnect/transfer/misbehave problem) + void debugMessage(const QString &msg, Debug::NotificationType typeEnum) const; +}; + +Q_DECLARE_METATYPE(DSOsamples *) +Q_DECLARE_METATYPE(std::vector) diff --git a/openhantek/src/hantekdso/dsoloop.cpp b/openhantek/src/hantekdso/dsoloop.cpp new file mode 100644 index 00000000..5fd1c3f4 --- /dev/null +++ b/openhantek/src/hantekdso/dsoloop.cpp @@ -0,0 +1,388 @@ +// SPDX-License-Identifier: GPL-2.0+ + +#include "dsoloop.h" + +#include "dsocontrol.h" +#include "models/modelDSO6022.h" +#include "usb/usbdevice.h" +#include +#include + +#ifdef DEBUG +#define DBGNOTIFY(x, y) emit m_control->debugMessage(x, y) +#else +#define DBGNOTIFY(x, y) +#endif + +#if __has_cpp_attribute(clang::fallthrough) +#define FALLTHROUGH [[clang::fallthrough]]; +#elif __has_cpp_attribute(fallthrough) +#define FALLTHROUGH [[fallthrough]]; +#else +#define FALLTHROUGH +#endif + +using namespace Hantek; +using namespace std::chrono; + +DsoLoop::DsoLoop(std::shared_ptr settings, DsoControl *control) + : m_specification(settings->spec), m_settings(settings), m_modelID(control->device->getModel()->ID), + result(settings->spec->channels), m_control(control) {} + +void DsoLoop::runRollMode() { + if (!m_control->sendPendingCommands()) return; + + int errorCode = 0; + captureState = CAPTURE_WAITING; + bool toNextState = true; + + switch (this->rollState) { + case RollState::STARTSAMPLING: + // Don't iterate through roll mode steps when stopped + if (!this->sampling) { + toNextState = false; + break; + } + + // Sampling hasn't started, update the expected sample count + expectedSampleCount = m_settings->getSampleCount(); + + errorCode = m_control->bulkCommand(m_control->getCommand(BulkCode::STARTSAMPLING)); + if (errorCode < 0) { + if (errorCode == LIBUSB_ERROR_NO_DEVICE) { + emit m_control->communicationError(); + return; + } + break; + } + + DBGNOTIFY("Starting to capture", Debug::NotificationType::DSOLoop); + + this->_samplingStarted = true; + + break; + + case RollState::ENABLETRIGGER: + errorCode = m_control->bulkCommand(m_control->getCommand(BulkCode::ENABLETRIGGER)); + if (errorCode < 0) { + if (errorCode == LIBUSB_ERROR_NO_DEVICE) { + emit m_control->communicationError(); + return; + } + break; + } + + DBGNOTIFY("Enabling trigger", Debug::NotificationType::DSOLoop); + + break; + + case RollState::FORCETRIGGER: + errorCode = m_control->bulkCommand(m_control->getCommand(BulkCode::FORCETRIGGER)); + if (errorCode < 0) { + if (errorCode == LIBUSB_ERROR_NO_DEVICE) { + emit m_control->communicationError(); + return; + } + break; + } + + DBGNOTIFY("Forcing trigger", Debug::NotificationType::DSOControl); + + break; + + case RollState::GETDATA: { + m_control->retrieveSamples(expectedSampleCount); + if (this->_samplingStarted) { + convertRawDataToSamples(m_control->m_rawdata); + emit m_control->samplesAvailable(&result); + } + } + + // Check if we're in single trigger mode + if (m_settings->trigger.mode() == Dso::TriggerMode::SINGLE && this->_samplingStarted) + this->enableSampling(false); + + // Sampling completed, restart it when necessary + this->_samplingStarted = false; + + break; + + default: + DBGNOTIFY("Roll mode state unknown", Debug::NotificationType::DSOControl); + break; + } + + // Go to next state, or restart if last state was reached + if (toNextState) this->rollState = (RollState)(((int)rollState + 1) % (int)RollState::_COUNT); + + this->updateInterval(); + QTimer::singleShot(cycleTime, this, m_settings->isRollMode() ? &DsoLoop::runRollMode : &DsoLoop::runStandardMode); +} + +void DsoLoop::runStandardMode() { + if (!m_control->sendPendingCommands()) return; + + int errorCode = 0; + this->rollState = RollState::STARTSAMPLING; + + const int lastCaptureState = this->captureState; + unsigned triggerPoint; + std::tie(captureState, triggerPoint) = m_control->retrieveCaptureState(); + m_settings->trigger.setPoint(calculateTriggerPoint(triggerPoint)); + if (this->captureState < 0) { + const QString errMsg = QString("Getting capture state failed: %1").arg(libUsbErrorString(this->captureState)); + qWarning() << errMsg; + DBGNOTIFY(errMsg, Debug::NotificationType::DSOControl); + } else if (this->captureState != lastCaptureState) + DBGNOTIFY(QString("Capture state changed to %1").arg(this->captureState), Debug::NotificationType::DSOLoop); + + switch (this->captureState) { + case CAPTURE_READY: + case CAPTURE_READY2250: + case CAPTURE_READY5200: { + m_control->retrieveSamples(expectedSampleCount); + if (this->_samplingStarted) { + convertRawDataToSamples(m_control->m_rawdata); + emit m_control->samplesAvailable(&result); + } + } + + // Check if we're in single trigger mode + if (m_settings->trigger.mode() == Dso::TriggerMode::SINGLE && this->_samplingStarted) + this->enableSampling(false); + + // Sampling completed, restart it when necessary + this->_samplingStarted = false; + + // Start next capture if necessary by leaving out the break statement + + if (!this->sampling) + break; + else { + FALLTHROUGH + } + case CAPTURE_WAITING: + // Sampling hasn't started, update the expected sample count + expectedSampleCount = m_settings->getSampleCount(); + + if (!m_specification->useControlNoBulk) { + if (_samplingStarted && lastTriggerMode == m_settings->trigger.mode()) { + ++this->cycleCounter; + + if (this->cycleCounter == this->startCycle && !m_settings->isRollMode()) { + // Buffer refilled completely since start of sampling, enable the + // trigger now + errorCode = m_control->bulkCommand(m_control->getCommand(BulkCode::ENABLETRIGGER)); + if (errorCode < 0) { + if (errorCode == LIBUSB_ERROR_NO_DEVICE) { + emit m_control->communicationError(); + return; + } + break; + } + + DBGNOTIFY("Enabling trigger", Debug::NotificationType::DSOLoop); + } else if (cycleCounter >= 8ms + startCycle && + m_settings->trigger.mode() == Dso::TriggerMode::WAIT_FORCE) { + // Force triggering + errorCode = m_control->bulkCommand(m_control->getCommand(BulkCode::FORCETRIGGER)); + if (errorCode < 0) { + if (errorCode == LIBUSB_ERROR_NO_DEVICE) { + emit m_control->communicationError(); + return; + } + break; + } + + DBGNOTIFY("Forcing trigger", Debug::NotificationType::DSOLoop); + } + + if (cycleCounter < 20ms || cycleCounter < milliseconds(4000 / cycleTime.count())) break; + } + + // Start capturing + errorCode = m_control->bulkCommand(m_control->getCommand(BulkCode::STARTSAMPLING)); + if (errorCode < 0) { + if (errorCode == LIBUSB_ERROR_NO_DEVICE) { + emit m_control->communicationError(); + return; + } + break; + } + + DBGNOTIFY("Starting to capture", Debug::NotificationType::DSOLoop); + } + + this->_samplingStarted = true; + this->cycleCounter = 0ms; + this->startCycle = milliseconds(int(m_settings->trigger.position() * 1000 / cycleTime.count())) + 1ms; + this->lastTriggerMode = m_settings->trigger.mode(); + break; + + case CAPTURE_SAMPLING: + break; + default: + break; + } + + this->updateInterval(); + QTimer::singleShot(cycleTime, this, m_settings->isRollMode() ? &DsoLoop::runRollMode : &DsoLoop::runStandardMode); +} + +void DsoLoop::enableSampling(bool enabled) { + sampling = enabled; + emit m_control->samplingStatusChanged(enabled); +} + +/// \brief Updates the interval of the periodic thread timer. +void DsoLoop::updateInterval() { + // Check the current oscilloscope state everytime 25% of the time the buffer + // should be refilled + double recLen; + if (m_settings->isRollMode()) + recLen = m_settings->packetSize() / (m_settings->isFastRate() ? 1 : m_specification->channels); + else + recLen = m_settings->getRecordLength(); + + // Not more often than every 10 ms though but at least once every second + cycleTime = std::chrono::milliseconds(qBound(10, int(recLen / m_settings->samplerate().samplerate * 250), 1000)); +} + +unsigned DsoLoop::calculateTriggerPoint(unsigned value) { + unsigned result = value; + + // Each set bit inverts all bits with a lower value + for (unsigned bitValue = 1; bitValue; bitValue <<= 1) + if (result & bitValue) result ^= bitValue - 1; + + return result; +} + +void DsoLoop::convertRawDataToSamples(const std::vector &rawData) { + const size_t totalSampleCount = (m_specification->sampleSize > 8) ? rawData.size() / 2 : rawData.size(); + + QWriteLocker locker(&result.lock); + + const unsigned extraBitsSize = m_specification->sampleSize - 8; // Number of extra bits + const uint16_t extraBitsMask = (0x00ff << extraBitsSize) & 0xff00; // Mask for extra bits extraction + + // Convert channel data + if (m_settings->isFastRate()) { + result.prepareForWrite(1, m_settings->samplerate().samplerate, m_settings->isRollMode()); + + // Fast rate mode, one channel is using all buffers. Find that channel + ChannelID channelId = 0; + for (; channelId < m_specification->channels; ++channelId) { + if (m_control->m_channelUsage.isUsed(channelId)) break; + } + + if (channelId >= m_specification->channels) return; + + // Resize sample vector + result.data[0].id = channelId; + DSOsamples::ChannelSamples &samples = result.data[0]; + samples.resize(totalSampleCount); + + const unsigned gainID = m_settings->voltage[channelId]->gainStepIndex(); + const double limit = m_specification->calibration[channelId][gainID].voltageLimit; + const double offsetCorrection = m_specification->calibration[channelId][gainID].offsetCorrection; + const double harwareOffset = m_settings->voltage[channelId]->offsetHardware(); + const double gainStep = m_specification->gain[gainID].gain; + + // Convert data from the oscilloscope and write it into the sample buffer + unsigned bufferPosition = m_settings->trigger.point() * 2; + if (m_specification->sampleSize > 8) { + for (unsigned pos = 0; pos < totalSampleCount; ++pos, ++bufferPosition) { + if (bufferPosition >= totalSampleCount) bufferPosition %= totalSampleCount; + + const uint16_t low = rawData[bufferPosition]; + const unsigned extraBitsPosition = bufferPosition % m_specification->channels; + const unsigned shift = (8 - (m_specification->channels - 1 - extraBitsPosition) * extraBitsSize); + const uint16_t high = + ((uint16_t)rawData[totalSampleCount + bufferPosition - extraBitsPosition] << shift) & extraBitsMask; + const uint16_t v = low + high; + double samplePoint = (v / limit - harwareOffset) * gainStep - offsetCorrection; + if (samplePoint < samples.minVoltage) samples.minVoltage = samplePoint; + if (samplePoint > samples.maxVoltage) samples.maxVoltage = samplePoint; + if (v < samples.minRaw) samples.minRaw = v; + if (v > samples.maxRaw) samples.maxRaw = v; + samples[pos] = samplePoint; + } + } else { + for (unsigned pos = 0; pos < totalSampleCount; ++pos, ++bufferPosition) { + if (bufferPosition >= totalSampleCount) bufferPosition %= totalSampleCount; + const uint16_t v = rawData[bufferPosition]; + double samplePoint = (v / limit - harwareOffset) * gainStep - offsetCorrection; + if (samplePoint < samples.minVoltage) samples.minVoltage = samplePoint; + if (samplePoint > samples.maxVoltage) samples.maxVoltage = samplePoint; + if (v < samples.minRaw) samples.minRaw = v; + if (v > samples.maxRaw) samples.maxRaw = v; + samples[pos] = samplePoint; + } + } + } else { + result.prepareForWrite(m_specification->channels, m_settings->samplerate().samplerate, + m_settings->isRollMode()); + + // Normal mode, channels are using their separate buffers + for (ChannelID channelId = 0; channelId < m_specification->channels; ++channelId) { + DSOsamples::ChannelSamples &samples = result.data[channelId]; + samples.id = channelId; + samples.resize(totalSampleCount / m_specification->channels); + + const unsigned gainID = m_settings->voltage[channelId]->gainStepIndex(); + const double limit = m_specification->calibration[channelId][gainID].voltageLimit; + const double offsetCorrection = m_specification->calibration[channelId][gainID].offsetCorrection; + const double harwareOffset = m_settings->voltage[channelId]->offsetHardware(); + const double gainStep = m_specification->gain[gainID].gain; + int16_t shiftDataBuf = 0; + + // Convert data from the oscilloscope and write it into the sample buffer + unsigned bufferPosition = m_settings->trigger.point() * 2; + if (m_specification->sampleSize > 8) { + // Additional most significant bits after the normal data + const unsigned extraBitsIndex = 8 - channelId * 2; // Bit position offset for extra bits extraction + const unsigned lowPosShift = m_specification->channels - 1 - channelId; + + for (unsigned pos = 0; pos < samples.size(); ++pos, bufferPosition += m_specification->channels) { + if (bufferPosition >= totalSampleCount) bufferPosition %= totalSampleCount; + + const uint16_t low = rawData[bufferPosition + lowPosShift]; + const uint16_t high = + ((uint16_t)rawData[bufferPosition + totalSampleCount] << extraBitsIndex) & extraBitsMask; + + const uint16_t v = low + high; + double samplePoint = (v / limit - harwareOffset) * gainStep - offsetCorrection; + if (samplePoint < samples.minVoltage) samples.minVoltage = samplePoint; + if (samplePoint > samples.maxVoltage) samples.maxVoltage = samplePoint; + if (v < samples.minRaw) samples.minRaw = v; + if (v > samples.maxRaw) samples.maxRaw = v; + samples[pos] = samplePoint; + } + } else if (m_modelID == ModelDSO6022BE::ID) { + // if device is 6022BE/BL, drop heading & trailing samples + const unsigned DROP_DSO6022_HEAD = 0x410; + const unsigned DROP_DSO6022_TAIL = 0x3F0; + if (!m_settings->isRollMode()) { + samples.resize(samples.size() - (DROP_DSO6022_HEAD + DROP_DSO6022_TAIL)); + // DROP_DSO6022_HEAD two times for two channels + bufferPosition += DROP_DSO6022_HEAD * 2; + } + bufferPosition += channelId; + shiftDataBuf = 0x83; + } else { + bufferPosition += m_specification->channels - 1 - channelId; + } + for (unsigned pos = 0; pos < samples.size(); ++pos, bufferPosition += m_specification->channels) { + if (bufferPosition >= totalSampleCount) bufferPosition %= totalSampleCount; + const int16_t v = rawData[bufferPosition] - shiftDataBuf; + double samplePoint = (v / limit - harwareOffset) * gainStep - offsetCorrection; + if (samplePoint < samples.minVoltage) samples.minVoltage = samplePoint; + if (samplePoint > samples.maxVoltage) samples.maxVoltage = samplePoint; + if (v < samples.minRaw) samples.minRaw = v; + if (v > samples.maxRaw) samples.maxRaw = v; + samples[pos] = samplePoint; + } + } + } +} diff --git a/openhantek/src/hantekdso/dsoloop.h b/openhantek/src/hantekdso/dsoloop.h new file mode 100644 index 00000000..e839b610 --- /dev/null +++ b/openhantek/src/hantekdso/dsoloop.h @@ -0,0 +1,74 @@ +// SPDX-License-Identifier: GPL-2.0+ + +#pragma once + +#include "devicesettings.h" +#include "dsosamples.h" +#include "errorcodes.h" +#include "hantekdso/modelspecification.h" +#include "hantekprotocol/bulkStructs.h" +#include "hantekprotocol/controlStructs.h" +#include "hantekprotocol/definitions.h" +#include "utils/debugnotify.h" + +#include + +class DsoControl; + +/** + * Implements the Dso logic for fetching/converting the samples at the right time. + */ +class DsoLoop : public QObject { + Q_OBJECT + public: + DsoLoop(std::shared_ptr settings, DsoControl *control); + + /// Call this to start the processing loop. + /// This method will call itself periodically from there on. + inline void run() { m_settings->isRollMode() ? runRollMode() : runStandardMode(); } + + inline bool isSampling() const { return sampling; } + + /// \brief If sampling is disabled, no samplesAvailable() signals are send anymore, no samples + /// are fetched from the device and no processing takes place. + /// \param enabled Enables/Disables sampling + void enableSampling(bool enabled); + + /// Return the last sample set + inline const DSOsamples &getLastSamples() { return result; } + + private: + void runRollMode(); + void runStandardMode(); + + void updateInterval(); + + /// \brief Calculates the trigger point from the CommandGetCaptureState data. + /// \param value The data value that contains the trigger point. + /// \return The calculated trigger point for the given data. + static unsigned calculateTriggerPoint(unsigned value); + + /// \brief Converts raw oscilloscope data to sample data + void convertRawDataToSamples(const std::vector &rawData); + + private: + int captureState = Hantek::CAPTURE_WAITING; + Hantek::RollState rollState = Hantek::RollState::STARTSAMPLING; + bool _samplingStarted = false; + Dso::TriggerMode lastTriggerMode = (Dso::TriggerMode)-1; + std::chrono::milliseconds cycleCounter = 0ms; + std::chrono::milliseconds startCycle = 0ms; + std::chrono::milliseconds cycleTime = 0ms; + bool sampling = false; ///< true, if the oscilloscope is taking samples + unsigned expectedSampleCount = 0; ///< The expected total number of samples at + /// the last check before sampling started + // Device setup + const Dso::ModelSpec *m_specification; ///< The specifications of the device + std::shared_ptr m_settings; ///< The current settings of the device + const int m_modelID; + + // Results + DSOsamples result; + + DsoControl *m_control; +}; diff --git a/openhantek/src/hantekdso/dsomodel.cpp b/openhantek/src/hantekdso/dsomodel.cpp index 968bb55e..c8628296 100644 --- a/openhantek/src/hantekdso/dsomodel.cpp +++ b/openhantek/src/hantekdso/dsomodel.cpp @@ -1,13 +1,14 @@ - // SPDX-License-Identifier: GPL-2.0+ #include "dsomodel.h" #include "modelregistry.h" +#include "hantekdso/modelspecification.h" DSOModel::DSOModel(int id, long vendorID, long productID, long vendorIDnoFirmware, long productIDnoFirmware, - const std::string &firmwareToken, const std::string &name, - const Dso::ControlSpecification &&specification) + const std::string &firmwareToken, const std::string &name, Dso::ModelSpec *specification) : ID(id), vendorID(vendorID), productID(productID), vendorIDnoFirmware(vendorIDnoFirmware), productIDnoFirmware(productIDnoFirmware), firmwareToken(firmwareToken), name(name), specification(specification) { ModelRegistry::get()->add(this); } + +DSOModel::~DSOModel() { delete specification; } diff --git a/openhantek/src/hantekdso/dsomodel.h b/openhantek/src/hantekdso/dsomodel.h index eb469304..f749e221 100644 --- a/openhantek/src/hantekdso/dsomodel.h +++ b/openhantek/src/hantekdso/dsomodel.h @@ -1,13 +1,14 @@ - // SPDX-License-Identifier: GPL-2.0+ #pragma once -#include "controlspecification.h" #include #include -class HantekDsoControl; +class DsoCommandQueue; +namespace Dso { +struct ModelSpec; +} /** * @brief Describes a device @@ -27,14 +28,14 @@ class DSOModel { std::string firmwareToken; std::string name; ///< User visible name. Does not need internationalisation/translation. protected: - Dso::ControlSpecification specification; + Dso::ModelSpec *specification; public: - /// This model may need to modify the HantekDsoControl class to work correctly - virtual void applyRequirements(HantekDsoControl *) const = 0; + /// Add available commands to the command queue object + virtual void applyRequirements(DsoCommandQueue *) const = 0; DSOModel(int id, long vendorID, long productID, long vendorIDnoFirmware, long productIDnoFirmware, - const std::string &firmwareToken, const std::string &name, const Dso::ControlSpecification &&specification); - virtual ~DSOModel() = default; + const std::string &firmwareToken, const std::string &name, Dso::ModelSpec *specification); + virtual ~DSOModel(); /// Return the device specifications - inline const Dso::ControlSpecification *spec() const { return &specification; } + inline const Dso::ModelSpec *spec() const { return specification; } }; diff --git a/openhantek/src/hantekdso/dsosamples.h b/openhantek/src/hantekdso/dsosamples.h index faeb6837..096a4f81 100644 --- a/openhantek/src/hantekdso/dsosamples.h +++ b/openhantek/src/hantekdso/dsosamples.h @@ -2,14 +2,50 @@ #pragma once +#include "hantekprotocol/types.h" #include #include #include #include struct DSOsamples { - std::vector> data; ///< Pointer to input data from device - double samplerate = 0.0; ///< The samplerate of the input data - bool append = false; ///< true, if waiting data should be appended + struct ChannelSamples : public std::vector { + ChannelID id; + // some statistics + double minVoltage; + double maxVoltage; + uint16_t minRaw; + uint16_t maxRaw; + }; + std::vector data; ///< Pointer to input data from device + double samplerate = 0.0; ///< The samplerate of the input data + bool append = false; ///< true, if waiting data should be appended + unsigned availableChannels = 0; mutable QReadWriteLock lock; + + DSOsamples(unsigned channels) { data.resize(channels); } + inline unsigned channelCount() const { return availableChannels; } + /** + * Clears the sample array and sets all fields. For performance reasons, we do not + * resize the channel dimension of the data array. This way all allocated resources + * are still allocated and can potentially be reused. + * + * @param channels The number of available channels. This must be lower than the channel count + * of the constructor. + * @param samplerate A samplerate + * @param append Roll mode or not + */ + inline void prepareForWrite(unsigned channels, double samplerate, bool append) { + this->samplerate = samplerate; + this->append = append; + this->availableChannels = channels; + for (ChannelSamples &v : data) { + v.id = (unsigned)-1; /// Invalid id + v.clear(); /// Clear all samples + v.maxRaw = 0; + v.minRaw = 0; + v.minVoltage = 2; + v.maxVoltage = -2; + } + } }; diff --git a/openhantek/src/hantekdso/enums.cpp b/openhantek/src/hantekdso/enums.cpp index 8c573ffd..740d476c 100644 --- a/openhantek/src/hantekdso/enums.cpp +++ b/openhantek/src/hantekdso/enums.cpp @@ -1,96 +1,92 @@ +// SPDX-License-Identifier: GPL-2.0+ + #include "enums.h" #include namespace Dso { - Enum TriggerModeEnum; - Enum SlopeEnum; - Enum GraphFormatEnum; - /// \brief Return string representation of the given channel mode. - /// \param mode The ::ChannelMode that should be returned as string. - /// \return The string that should be used in labels etc., empty when invalid. - QString channelModeString(ChannelMode mode) { - switch (mode) { - case ChannelMode::Voltage: - return QCoreApplication::tr("Voltage"); - case ChannelMode::Spectrum: - return QCoreApplication::tr("Spectrum"); - } - return QString(); +/// \brief Return string representation of the given channel mode. +/// \param mode The ::ChannelMode that should be returned as string. +/// \return The string that should be used in labels etc., empty when invalid. +QString channelModeString(ChannelMode mode) { + switch (mode) { + case ChannelMode::Voltage: + return QCoreApplication::tr("Voltage"); + case ChannelMode::Spectrum: + return QCoreApplication::tr("Spectrum"); } + return QString(); +} - /// \brief Return string representation of the given graph format. - /// \param format The ::GraphFormat that should be returned as string. - /// \return The string that should be used in labels etc. - QString graphFormatString(GraphFormat format) { - switch (format) { - case GraphFormat::TY: - return QCoreApplication::tr("T - Y"); - case GraphFormat::XY: - return QCoreApplication::tr("X - Y"); - } - return QString(); +/// \brief Return string representation of the given graph format. +/// \param format The ::GraphFormat that should be returned as string. +/// \return The string that should be used in labels etc. +QString graphFormatString(GraphFormat format) { + switch (format) { + case GraphFormat::TY: + return QCoreApplication::tr("T - Y"); + case GraphFormat::XY: + return QCoreApplication::tr("X - Y"); } + return QString(); +} - /// \brief Return string representation of the given channel coupling. - /// \param coupling The ::Coupling that should be returned as string. - /// \return The string that should be used in labels etc. - QString couplingString(Coupling coupling) { - switch (coupling) { - case Coupling::AC: - return QCoreApplication::tr("AC"); - case Coupling::DC: - return QCoreApplication::tr("DC"); - case Coupling::GND: - return QCoreApplication::tr("GND"); - } - return QString(); +/// \brief Return string representation of the given channel coupling. +/// \param coupling The ::Coupling that should be returned as string. +/// \return The string that should be used in labels etc. +QString couplingString(Coupling coupling) { + switch (coupling) { + case Coupling::AC: + return QCoreApplication::tr("AC"); + case Coupling::DC: + return QCoreApplication::tr("DC"); + case Coupling::GND: + return QCoreApplication::tr("GND"); } + return QString(); +} - - /// \brief Return string representation of the given trigger mode. - /// \param mode The ::TriggerMode that should be returned as string. - /// \return The string that should be used in labels etc. - QString triggerModeString(TriggerMode mode) { - switch (mode) { - case TriggerMode::WAIT_FORCE: - return QCoreApplication::tr("Wait/Force"); - case TriggerMode::HARDWARE_SOFTWARE: - return QCoreApplication::tr("Hard-/Software"); - case TriggerMode::SINGLE: - return QCoreApplication::tr("Single"); - } - return QString(); +/// \brief Return string representation of the given trigger mode. +/// \param mode The ::TriggerMode that should be returned as string. +/// \return The string that should be used in labels etc. +QString triggerModeString(TriggerMode mode) { + switch (mode) { + case TriggerMode::WAIT_FORCE: + return QCoreApplication::tr("Wait/Force"); + case TriggerMode::HARDWARE_SOFTWARE: + return QCoreApplication::tr("Hard-/Software"); + case TriggerMode::SINGLE: + return QCoreApplication::tr("Single"); } + return QString(); +} - /// \brief Return string representation of the given trigger slope. - /// \param slope The ::Slope that should be returned as string. - /// \return The string that should be used in labels etc. - QString slopeString(Slope slope) { - switch (slope) { - case Slope::Positive: - return QString::fromUtf8("\u2197"); - case Slope::Negative: - return QString::fromUtf8("\u2198"); - default: - return QString(); - } +/// \brief Return string representation of the given trigger slope. +/// \param slope The ::Slope that should be returned as string. +/// \return The string that should be used in labels etc. +QString slopeString(Slope slope) { + switch (slope) { + case Slope::Positive: + return QString::fromUtf8("\u2197"); + case Slope::Negative: + return QString::fromUtf8("\u2198"); } + return QString(); +} - /// \brief Return string representation of the given graph interpolation mode. - /// \param interpolation The ::InterpolationMode that should be returned as - /// string. - /// \return The string that should be used in labels etc. - QString interpolationModeString(InterpolationMode interpolation) { - switch (interpolation) { - case INTERPOLATION_OFF: - return QCoreApplication::tr("Off"); - case INTERPOLATION_LINEAR: - return QCoreApplication::tr("Linear"); - case INTERPOLATION_SINC: - return QCoreApplication::tr("Sinc"); - default: - return QString(); - } +/// \brief Return string representation of the given graph interpolation mode. +/// \param interpolation The ::InterpolationMode that should be returned as +/// string. +/// \return The string that should be used in labels etc. +QString interpolationModeString(InterpolationMode interpolation) { + switch (interpolation) { + case InterpolationMode::OFF: + return QCoreApplication::tr("Off"); + case InterpolationMode::LINEAR: + return QCoreApplication::tr("Linear"); + case InterpolationMode::SINC: + return QCoreApplication::tr("Sinc"); } + return QString(); +} } diff --git a/openhantek/src/hantekdso/enums.h b/openhantek/src/hantekdso/enums.h index f89711c9..933f5990 100644 --- a/openhantek/src/hantekdso/enums.h +++ b/openhantek/src/hantekdso/enums.h @@ -1,24 +1,27 @@ +// SPDX-License-Identifier: GPL-2.0+ + #pragma once -#include "utils/enumclass.h" #include #include namespace Dso { +Q_NAMESPACE + /// \enum ChannelMode /// \brief The channel display modes. enum class ChannelMode { Voltage, ///< Standard voltage view Spectrum ///< Spectrum view }; +Q_ENUM_NS(ChannelMode) /// \enum GraphFormat /// \brief The possible viewing formats for the graphs on the scope. -enum GraphFormat { +enum class GraphFormat { TY, ///< The standard mode XY ///< CH1 on X-axis, CH2 on Y-axis }; - -extern Enum GraphFormatEnum; +Q_ENUM_NS(GraphFormat) /// \enum Coupling /// \brief The coupling modes for the channels. @@ -27,6 +30,7 @@ enum class Coupling { DC, ///< No filtering GND ///< Channel is grounded }; +Q_ENUM_NS(Coupling) /// \enum TriggerMode /// \brief The different triggering modes. @@ -35,7 +39,7 @@ enum class TriggerMode { WAIT_FORCE, ///< Automatic without trigger event SINGLE ///< Stop after the first trigger event }; -extern Enum TriggerModeEnum; +Q_ENUM_NS(TriggerMode) /// \enum Slope /// \brief The slope that causes a trigger. @@ -43,16 +47,16 @@ enum class Slope : uint8_t { Positive = 0, ///< From lower to higher voltage Negative = 1 ///< From higher to lower voltage }; -extern Enum SlopeEnum; +Q_ENUM_NS(Slope) /// \enum InterpolationMode /// \brief The different interpolation modes for the graphs. -enum InterpolationMode { - INTERPOLATION_OFF = 0, ///< Just dots for each sample - INTERPOLATION_LINEAR, ///< Sample dots connected by lines - INTERPOLATION_SINC, ///< Smooth graph through the dots - INTERPOLATION_COUNT ///< Total number of interpolation modes +enum class InterpolationMode { + OFF = 0, ///< Just dots for each sample + LINEAR, ///< Sample dots connected by lines + SINC, ///< Smooth graph through the dots }; +Q_ENUM_NS(InterpolationMode) QString channelModeString(ChannelMode mode); QString graphFormatString(GraphFormat format); @@ -61,10 +65,3 @@ QString triggerModeString(TriggerMode mode); QString slopeString(Slope slope); QString interpolationModeString(InterpolationMode interpolation); } - -Q_DECLARE_METATYPE(Dso::TriggerMode) -Q_DECLARE_METATYPE(Dso::Slope) -Q_DECLARE_METATYPE(Dso::Coupling) -Q_DECLARE_METATYPE(Dso::GraphFormat) -Q_DECLARE_METATYPE(Dso::ChannelMode) -Q_DECLARE_METATYPE(Dso::InterpolationMode) diff --git a/openhantek/src/hantekdso/errorcodes.h b/openhantek/src/hantekdso/errorcodes.h index 97aee670..3e0e660a 100644 --- a/openhantek/src/hantekdso/errorcodes.h +++ b/openhantek/src/hantekdso/errorcodes.h @@ -7,9 +7,10 @@ namespace Dso { /// \brief The return codes for device control methods. enum class ErrorCode { NONE = 0, ///< Successful operation - CONNECTION = -1, ///< Device not connected or communication error + CONNECTION = -1, ///< Device not connected UNSUPPORTED = -2, ///< Not supported by this device - PARAMETER = -3 ///< Parameter out of range + PARAMETER = -3, ///< Parameter out of range + UNCHANGED = -4, ///< Not applied because unchanged + UNEXPECTED = -5 ///< The device responded with an (unexpected) usb error code }; - } diff --git a/openhantek/src/hantekdso/hantekdsocontrol.cpp b/openhantek/src/hantekdso/hantekdsocontrol.cpp deleted file mode 100644 index 5bd3b6da..00000000 --- a/openhantek/src/hantekdso/hantekdsocontrol.cpp +++ /dev/null @@ -1,1326 +0,0 @@ -// SPDX-License-Identifier: GPL-2.0+ - -#include -#include -#include -#include -#include - -#include -#include -#include -#include -#include - -#include "hantekdsocontrol.h" -#include "hantekprotocol/bulkStructs.h" -#include "hantekprotocol/controlStructs.h" -#include "models/modelDSO6022.h" -#include "usb/usbdevice.h" - -using namespace Hantek; -using namespace Dso; - -/// \brief Start sampling process. -void HantekDsoControl::enableSampling(bool enabled) { - sampling = enabled; - - // Emit signals for initial settings - // emit availableRecordLengthsChanged(controlsettings.samplerate.limits->recordLengths); - // updateSamplerateLimits(); - // emit recordLengthChanged(getRecordLength()); - // if (!isRollMode()) emit recordTimeChanged((double)getRecordLength() / controlsettings.samplerate.current); - // emit samplerateChanged(controlsettings.samplerate.current); - - emit samplingStatusChanged(enabled); -} - -const USBDevice *HantekDsoControl::getDevice() const { return device; } - -const DSOsamples &HantekDsoControl::getLastSamples() { return result; } - -HantekDsoControl::HantekDsoControl(USBDevice *device) - : device(device), specification(device->getModel()->spec()), - controlsettings(&(specification->samplerate.single), specification->channels) { - if (device == nullptr) throw new std::runtime_error("No usb device for HantekDsoControl"); - - qRegisterMetaType(); - - if (specification->fixedUSBinLength) device->overwriteInPacketLength(specification->fixedUSBinLength); - - // Apply special requirements by the devices model - device->getModel()->applyRequirements(this); - - retrieveChannelLevelData(); -} - -HantekDsoControl::~HantekDsoControl() { - while (firstBulkCommand) { - BulkCommand *t = firstBulkCommand->next; - delete firstBulkCommand; - firstBulkCommand = t; - } - while (firstControlCommand) { - ControlCommand *t = firstControlCommand->next; - delete firstControlCommand; - firstControlCommand = t; - } -} - -int HantekDsoControl::bulkCommand(const std::vector *command, int attempts) const { - if (specification->useControlNoBulk) return LIBUSB_SUCCESS; - - // Send BeginCommand control command - int errorCode = device->controlWrite(&controlsettings.beginCommandControl); - if (errorCode < 0) return errorCode; - - // Send bulk command - return device->bulkWrite(command->data(), command->size(), attempts); -} - -unsigned HantekDsoControl::getChannelCount() const { return specification->channels; } - -const ControlSettings *HantekDsoControl::getDeviceSettings() const { return &controlsettings; } - -const std::vector &HantekDsoControl::getAvailableRecordLengths() const { // - return controlsettings.samplerate.limits->recordLengths; -} - -double HantekDsoControl::getMinSamplerate() const { - return (double)specification->samplerate.single.base / specification->samplerate.single.maxDownsampler; -} - -double HantekDsoControl::getMaxSamplerate() const { - if (controlsettings.usedChannels <= 1) - return specification->samplerate.multi.max; - else - return specification->samplerate.single.max; -} - -bool HantekDsoControl::isSampling() const { return sampling; } - -/// \brief Updates the interval of the periodic thread timer. -void HantekDsoControl::updateInterval() { - // Check the current oscilloscope state everytime 25% of the time the buffer - // should be refilled - if (isRollMode()) - cycleTime = (int)((double)getPacketSize() / (isFastRate() ? 1 : specification->channels) / - controlsettings.samplerate.current * 250); - else - cycleTime = (int)((double)getRecordLength() / controlsettings.samplerate.current * 250); - - // Not more often than every 10 ms though but at least once every second - cycleTime = qBound(10, cycleTime, 1000); -} - -bool HantekDsoControl::isRollMode() const { - return controlsettings.samplerate.limits->recordLengths[controlsettings.recordLengthId] == UINT_MAX; -} - -bool HantekDsoControl::isFastRate() const { - return controlsettings.samplerate.limits == &specification->samplerate.multi; -} - -unsigned HantekDsoControl::getRecordLength() const { - return controlsettings.samplerate.limits->recordLengths[controlsettings.recordLengthId]; -} - -Dso::ErrorCode HantekDsoControl::retrieveChannelLevelData() { - // Get channel level data - int errorCode = device->controlRead(&controlsettings.cmdGetLimits); - if (errorCode < 0) { - qWarning() << tr("Couldn't get channel level data from oscilloscope"); - emit statusMessage(tr("Couldn't get channel level data from oscilloscope"), 0); - emit communicationError(); - return Dso::ErrorCode::CONNECTION; - } - - memcpy(controlsettings.offsetLimit, controlsettings.cmdGetLimits.offsetLimitData(), - sizeof(OffsetsPerGainStep) * specification->channels); - - return Dso::ErrorCode::NONE; -} - -unsigned HantekDsoControl::calculateTriggerPoint(unsigned value) { - unsigned result = value; - - // Each set bit inverts all bits with a lower value - for (unsigned bitValue = 1; bitValue; bitValue <<= 1) - if (result & bitValue) result ^= bitValue - 1; - - return result; -} - -std::pair HantekDsoControl::getCaptureState() const { - int errorCode; - - if (!specification->supportsCaptureState) return std::make_pair(CAPTURE_READY, 0); - - errorCode = bulkCommand(getCommand(BulkCode::GETCAPTURESTATE), 1); - if (errorCode < 0) { - qWarning() << "Getting capture state failed: " << libUsbErrorString(errorCode); - return std::make_pair(CAPTURE_ERROR, 0); - } - - BulkResponseGetCaptureState response; - errorCode = device->bulkRead(&response); - if (errorCode < 0) { - qWarning() << "Getting capture state failed: " << libUsbErrorString(errorCode); - return std::make_pair(CAPTURE_ERROR, 0); - } - - return std::make_pair((int)response.getCaptureState(), response.getTriggerPoint()); -} - -std::vector HantekDsoControl::getSamples(unsigned &previousSampleCount) const { - int errorCode; - if (!specification->useControlNoBulk) { - // Request data - errorCode = bulkCommand(getCommand(BulkCode::GETDATA), 1); - } else { - errorCode = device->controlWrite(getCommand(ControlCode::CONTROL_ACQUIIRE_HARD_DATA)); - } - if (errorCode < 0) { - qWarning() << "Getting sample data failed: " << libUsbErrorString(errorCode); - emit communicationError(); - return std::vector(); - } - - unsigned totalSampleCount = this->getSampleCount(); - - // To make sure no samples will remain in the scope buffer, also check the - // sample count before the last sampling started - if (totalSampleCount < previousSampleCount) { - std::swap(totalSampleCount, previousSampleCount); - } else { - previousSampleCount = totalSampleCount; - } - - unsigned dataLength = (specification->sampleSize > 8) ? totalSampleCount * 2 : totalSampleCount; - - // Save raw data to temporary buffer - std::vector data(dataLength); - int errorcode = device->bulkReadMulti(data.data(), dataLength); - if (errorcode < 0) { - qWarning() << "Getting sample data failed: " << libUsbErrorString(errorcode); - return std::vector(); - } - data.resize((size_t)errorcode); - - static unsigned id = 0; - ++id; - timestampDebug(QString("Received packet %1").arg(id)); - - return data; -} - -void HantekDsoControl::convertRawDataToSamples(const std::vector &rawData) { - const size_t totalSampleCount = (specification->sampleSize > 8) ? rawData.size() / 2 : rawData.size(); - - QWriteLocker locker(&result.lock); - result.samplerate = controlsettings.samplerate.current; - result.append = isRollMode(); - // Prepare result buffers - result.data.resize(specification->channels); - for (ChannelID channelCounter = 0; channelCounter < specification->channels; ++channelCounter) - result.data[channelCounter].clear(); - - const unsigned extraBitsSize = specification->sampleSize - 8; // Number of extra bits - const unsigned short extraBitsMask = (0x00ff << extraBitsSize) & 0xff00; // Mask for extra bits extraction - - // Convert channel data - if (isFastRate()) { - // Fast rate mode, one channel is using all buffers - ChannelID channel = 0; - for (; channel < specification->channels; ++channel) { - if (controlsettings.voltage[channel].used) break; - } - - if (channel >= specification->channels) return; - - // Resize sample vector - result.data[channel].resize(totalSampleCount); - - const unsigned gainID = controlsettings.voltage[channel].gain; - const unsigned short limit = specification->voltageLimit[channel][gainID]; - const double offset = controlsettings.voltage[channel].offsetReal; - const double gainStep = specification->gain[gainID].gainSteps; - - // Convert data from the oscilloscope and write it into the sample buffer - unsigned bufferPosition = controlsettings.trigger.point * 2; - if (specification->sampleSize > 8) { - for (unsigned pos = 0; pos < totalSampleCount; ++pos, ++bufferPosition) { - if (bufferPosition >= totalSampleCount) bufferPosition %= totalSampleCount; - - const unsigned short low = rawData[bufferPosition]; - const unsigned extraBitsPosition = bufferPosition % specification->channels; - const unsigned shift = (8 - (specification->channels - 1 - extraBitsPosition) * extraBitsSize); - const unsigned short high = - ((unsigned short int)rawData[totalSampleCount + bufferPosition - extraBitsPosition] << shift) & - extraBitsMask; - - result.data[channel][pos] = ((double)(low + high) / limit - offset) * gainStep; - } - } else { - for (unsigned pos = 0; pos < totalSampleCount; ++pos, ++bufferPosition) { - if (bufferPosition >= totalSampleCount) bufferPosition %= totalSampleCount; - - double dataBuf = (double)((int)rawData[bufferPosition]); - result.data[channel][pos] = (dataBuf / limit - offset) * gainStep; - } - } - } else { - // Normal mode, channels are using their separate buffers - for (ChannelID channel = 0; channel < specification->channels; ++channel) { - result.data[channel].resize(totalSampleCount / specification->channels); - - const unsigned gainID = controlsettings.voltage[channel].gain; - const unsigned short limit = specification->voltageLimit[channel][gainID]; - const double offset = controlsettings.voltage[channel].offsetReal; - const double gainStep = specification->gain[gainID].gainSteps; - int shiftDataBuf = 0; - - // Convert data from the oscilloscope and write it into the sample buffer - unsigned bufferPosition = controlsettings.trigger.point * 2; - if (specification->sampleSize > 8) { - // Additional most significant bits after the normal data - unsigned extraBitsIndex = 8 - channel * 2; // Bit position offset for extra bits extraction - - for (unsigned realPosition = 0; realPosition < result.data[channel].size(); - ++realPosition, bufferPosition += specification->channels) { - if (bufferPosition >= totalSampleCount) bufferPosition %= totalSampleCount; - - const unsigned short low = rawData[bufferPosition + specification->channels - 1 - channel]; - const unsigned short high = - ((unsigned short int)rawData[totalSampleCount + bufferPosition] << extraBitsIndex) & - extraBitsMask; - - result.data[channel][realPosition] = ((double)(low + high) / limit - offset) * gainStep; - } - } else if (device->getModel()->ID == ModelDSO6022BE::ID) { - // if device is 6022BE, drop heading & trailing samples - const unsigned DROP_DSO6022_HEAD = 0x410; - const unsigned DROP_DSO6022_TAIL = 0x3F0; - if (!isRollMode()) { - result.data[channel].resize(result.data[channel].size() - (DROP_DSO6022_HEAD + DROP_DSO6022_TAIL)); - // if device is 6022BE, offset DROP_DSO6022_HEAD incrementally - bufferPosition += DROP_DSO6022_HEAD * 2; - } - bufferPosition += channel; - shiftDataBuf = 0x83; - } else { - bufferPosition += specification->channels - 1 - channel; - } - for (unsigned pos = 0; pos < result.data[channel].size(); - ++pos, bufferPosition += specification->channels) { - if (bufferPosition >= totalSampleCount) bufferPosition %= totalSampleCount; - double dataBuf = (double)((int)(rawData[bufferPosition] - shiftDataBuf)); - result.data[channel][pos] = (dataBuf / limit - offset) * gainStep; - } - } - } -} - -double HantekDsoControl::getBestSamplerate(double samplerate, bool fastRate, bool maximum, - unsigned *downsampler) const { - // Abort if the input value is invalid - if (samplerate <= 0.0) return 0.0; - - double bestSamplerate = 0.0; - - // Get samplerate specifications for this mode and model - const ControlSamplerateLimits *limits; - if (fastRate) - limits = &(specification->samplerate.multi); - else - limits = &(specification->samplerate.single); - - // Get downsampling factor that would provide the requested rate - double bestDownsampler = limits->base / specification->bufferDividers[controlsettings.recordLengthId] / samplerate; - // Base samplerate sufficient, or is the maximum better? - if (bestDownsampler < 1.0 && - (samplerate <= limits->max / specification->bufferDividers[controlsettings.recordLengthId] || !maximum)) { - bestDownsampler = 0.0; - bestSamplerate = limits->max / specification->bufferDividers[controlsettings.recordLengthId]; - } else { - switch (specification->cmdSetSamplerate) { - case BulkCode::SETTRIGGERANDSAMPLERATE: - // DSO-2090 supports the downsampling factors 1, 2, 4 and 5 using - // valueFast or all even values above using valueSlow - if ((maximum && bestDownsampler <= 5.0) || (!maximum && bestDownsampler < 6.0)) { - // valueFast is used - if (maximum) { - // The samplerate shall not be higher, so we round up - bestDownsampler = ceil(bestDownsampler); - if (bestDownsampler > 2.0) // 3 and 4 not possible with the DSO-2090 - bestDownsampler = 5.0; - } else { - // The samplerate shall not be lower, so we round down - bestDownsampler = floor(bestDownsampler); - if (bestDownsampler > 2.0 && bestDownsampler < 5.0) // 3 and 4 not possible with the DSO-2090 - bestDownsampler = 2.0; - } - } else { - // valueSlow is used - if (maximum) { - bestDownsampler = ceil(bestDownsampler / 2.0) * 2.0; // Round up to next even value - } else { - bestDownsampler = floor(bestDownsampler / 2.0) * 2.0; // Round down to next even value - } - if (bestDownsampler > 2.0 * 0x10001) // Check for overflow - bestDownsampler = 2.0 * 0x10001; - } - break; - - case BulkCode::CSETTRIGGERORSAMPLERATE: - // DSO-5200 may not supports all downsampling factors, requires testing - if (maximum) { - bestDownsampler = ceil(bestDownsampler); // Round up to next integer value - } else { - bestDownsampler = floor(bestDownsampler); // Round down to next integer value - } - break; - - case BulkCode::ESETTRIGGERORSAMPLERATE: - // DSO-2250 doesn't have a fast value, so it supports all downsampling - // factors - if (maximum) { - bestDownsampler = ceil(bestDownsampler); // Round up to next integer value - } else { - bestDownsampler = floor(bestDownsampler); // Round down to next integer value - } - break; - - default: - return 0.0; - } - - // Limit maximum downsampler value to avoid overflows in the sent commands - if (bestDownsampler > limits->maxDownsampler) bestDownsampler = limits->maxDownsampler; - - bestSamplerate = limits->base / bestDownsampler / specification->bufferDividers[controlsettings.recordLengthId]; - } - - if (downsampler) *downsampler = (unsigned)bestDownsampler; - return bestSamplerate; -} - -unsigned HantekDsoControl::getSampleCount() const { - if (isRollMode()) { - // TODO handle libusb error - return getPacketSize(); - } else { - if (isFastRate()) - return getRecordLength(); - else - return getRecordLength() * specification->channels; - } -} - -unsigned HantekDsoControl::updateRecordLength(RecordLengthID index) { - if (index >= controlsettings.samplerate.limits->recordLengths.size()) return 0; - - switch (specification->cmdSetRecordLength) { - case BulkCode::SETTRIGGERANDSAMPLERATE: - modifyCommand(BulkCode::SETTRIGGERANDSAMPLERATE)->setRecordLength(index); - break; - - case BulkCode::DSETBUFFER: - if (specification->cmdSetPretrigger == BulkCode::FSETBUFFER) { - modifyCommand(BulkCode::DSETBUFFER)->setRecordLength(index); - } else { - // SetBuffer5200 bulk command for record length - BulkSetBuffer5200 *commandSetBuffer5200 = modifyCommand(BulkCode::DSETBUFFER); - - commandSetBuffer5200->setUsedPre(DTriggerPositionUsed::ON); - commandSetBuffer5200->setUsedPost(DTriggerPositionUsed::ON); - commandSetBuffer5200->setRecordLength(index); - } - - break; - - default: - return 0; - } - - // Check if the divider has changed and adapt samplerate limits accordingly - bool bDividerChanged = - specification->bufferDividers[index] != specification->bufferDividers[controlsettings.recordLengthId]; - - controlsettings.recordLengthId = index; - - if (bDividerChanged) { - this->updateSamplerateLimits(); - - // Samplerate dividers changed, recalculate it - this->restoreTargets(); - } - - return controlsettings.samplerate.limits->recordLengths[index]; -} - -unsigned HantekDsoControl::updateSamplerate(unsigned downsampler, bool fastRate) { - // Get samplerate limits - const ControlSamplerateLimits *limits = - fastRate ? &specification->samplerate.multi : &specification->samplerate.single; - - // Set the calculated samplerate - switch (specification->cmdSetSamplerate) { - case BulkCode::SETTRIGGERANDSAMPLERATE: { - short int downsamplerValue = 0; - unsigned char samplerateId = 0; - bool downsampling = false; - - if (downsampler <= 5) { - // All dividers up to 5 are done using the special samplerate IDs - if (downsampler == 0 && limits->base >= limits->max) - samplerateId = 1; - else if (downsampler <= 2) - samplerateId = downsampler; - else { // Downsampling factors 3 and 4 are not supported - samplerateId = 3; - downsampler = 5; - downsamplerValue = (short int)0xffff; - } - } else { - // For any dividers above the downsampling factor can be set directly - downsampler &= ~0x0001; // Only even values possible - downsamplerValue = (short int)(0x10001 - (downsampler >> 1)); - - downsampling = true; - } - - // Pointers to needed commands - BulkSetTriggerAndSamplerate *commandSetTriggerAndSamplerate = - modifyCommand(BulkCode::SETTRIGGERANDSAMPLERATE); - - // Store if samplerate ID or downsampling factor is used - commandSetTriggerAndSamplerate->setDownsamplingMode(downsampling); - // Store samplerate ID - commandSetTriggerAndSamplerate->setSamplerateId(samplerateId); - // Store downsampling factor - commandSetTriggerAndSamplerate->setDownsampler(downsamplerValue); - // Set fast rate when used - commandSetTriggerAndSamplerate->setFastRate(false /*fastRate*/); - - break; - } - case BulkCode::CSETTRIGGERORSAMPLERATE: { - // Split the resulting divider into the values understood by the device - // The fast value is kept at 4 (or 3) for slow sample rates - long int valueSlow = qMax(((long int)downsampler - 3) / 2, (long int)0); - unsigned char valueFast = downsampler - valueSlow * 2; - - // Pointers to needed commands - BulkSetSamplerate5200 *commandSetSamplerate5200 = - modifyCommand(BulkCode::CSETTRIGGERORSAMPLERATE); - BulkSetTrigger5200 *commandSetTrigger5200 = - modifyCommand(BulkCode::ESETTRIGGERORSAMPLERATE); - - // Store samplerate fast value - commandSetSamplerate5200->setSamplerateFast(4 - valueFast); - // Store samplerate slow value (two's complement) - commandSetSamplerate5200->setSamplerateSlow(valueSlow == 0 ? 0 : 0xffff - valueSlow); - // Set fast rate when used - commandSetTrigger5200->setFastRate(fastRate); - - break; - } - case BulkCode::ESETTRIGGERORSAMPLERATE: { - // Pointers to needed commands - BulkSetSamplerate2250 *commandSetSamplerate2250 = - modifyCommand(BulkCode::ESETTRIGGERORSAMPLERATE); - - bool downsampling = downsampler >= 1; - // Store downsampler state value - commandSetSamplerate2250->setDownsampling(downsampling); - // Store samplerate value - commandSetSamplerate2250->setSamplerate(downsampler > 1 ? 0x10001 - downsampler : 0); - // Set fast rate when used - commandSetSamplerate2250->setFastRate(fastRate); - - break; - } - default: - return UINT_MAX; - } - - // Update settings - bool fastRateChanged = fastRate != (controlsettings.samplerate.limits == &specification->samplerate.multi); - if (fastRateChanged) { controlsettings.samplerate.limits = limits; } - - controlsettings.samplerate.downsampler = downsampler; - if (downsampler) - controlsettings.samplerate.current = controlsettings.samplerate.limits->base / - specification->bufferDividers[controlsettings.recordLengthId] / - downsampler; - else - controlsettings.samplerate.current = - controlsettings.samplerate.limits->max / specification->bufferDividers[controlsettings.recordLengthId]; - - // Update dependencies - this->setPretriggerPosition(controlsettings.trigger.position); - - // Emit signals for changed settings - if (fastRateChanged) { - emit availableRecordLengthsChanged(controlsettings.samplerate.limits->recordLengths); - emit recordLengthChanged(getRecordLength()); - } - - // Check for Roll mode - if (!isRollMode()) emit recordTimeChanged((double)getRecordLength() / controlsettings.samplerate.current); - emit samplerateChanged(controlsettings.samplerate.current); - - return downsampler; -} - -void HantekDsoControl::restoreTargets() { - if (controlsettings.samplerate.target.samplerateSet == ControlSettingsSamplerateTarget::Samplerrate) - this->setSamplerate(); - else - this->setRecordTime(); -} - -void HantekDsoControl::updateSamplerateLimits() { - if (specification->isFixedSamplerateDevice) { - // Convert to GUI presentable values (1e5 -> 1.0, 48e6 -> 480.0 etc) - QList sampleSteps; - for (auto &v : specification->fixedSampleRates) { sampleSteps << v.samplerate / 1e5; } - emit samplerateSet(1, sampleSteps); - } else { - // Works only if the minimum samplerate for normal mode is lower than for fast - // rate mode, which is the case for all models - const double min = - (double)specification->samplerate.single.base / specification->samplerate.single.maxDownsampler; - const double max = getMaxSamplerate(); - - emit samplerateLimitsChanged(min / specification->bufferDividers[controlsettings.recordLengthId], - max / specification->bufferDividers[controlsettings.recordLengthId]); - } -} - -Dso::ErrorCode HantekDsoControl::setRecordLength(unsigned index) { - if (!device->isConnected()) return Dso::ErrorCode::CONNECTION; - - if (!updateRecordLength(index)) return Dso::ErrorCode::PARAMETER; - - restoreTargets(); - setPretriggerPosition(controlsettings.trigger.position); - - emit recordLengthChanged(getRecordLength()); - return Dso::ErrorCode::NONE; -} - -Dso::ErrorCode HantekDsoControl::setSamplerate(double samplerate) { - if (!device->isConnected()) return Dso::ErrorCode::CONNECTION; - - if (samplerate == 0.0) { - samplerate = controlsettings.samplerate.target.samplerate; - } else { - controlsettings.samplerate.target.samplerate = samplerate; - controlsettings.samplerate.target.samplerateSet = ControlSettingsSamplerateTarget::Samplerrate; - } - - if (!specification->isSoftwareTriggerDevice) { - // When possible, enable fast rate if it is required to reach the requested - // samplerate - bool fastRate = (controlsettings.usedChannels <= 1) && - (samplerate > specification->samplerate.single.max / - specification->bufferDividers[controlsettings.recordLengthId]); - - // What is the nearest, at least as high samplerate the scope can provide? - unsigned downsampler = 0; - getBestSamplerate(samplerate, fastRate, false, &(downsampler)); - - // Set the calculated samplerate - if (this->updateSamplerate(downsampler, fastRate) == UINT_MAX) - return Dso::ErrorCode::PARAMETER; - else { - return Dso::ErrorCode::NONE; - } - } else { - unsigned sampleId; - for (sampleId = 0; sampleId < specification->fixedSampleRates.size() - 1; ++sampleId) - if (specification->fixedSampleRates[sampleId].samplerate == samplerate) break; - modifyCommand(ControlCode::CONTROL_SETTIMEDIV) - ->setDiv(specification->fixedSampleRates[sampleId].id); - controlsettings.samplerate.current = samplerate; - - // Check for Roll mode - if (!isRollMode()) - emit recordTimeChanged((double)(getRecordLength() - controlsettings.swSampleMargin) / - controlsettings.samplerate.current); - emit samplerateChanged(controlsettings.samplerate.current); - - return Dso::ErrorCode::NONE; - } -} - -Dso::ErrorCode HantekDsoControl::setRecordTime(double duration) { - if (!device->isConnected()) return Dso::ErrorCode::CONNECTION; - - if (duration == 0.0) { - duration = controlsettings.samplerate.target.duration; - } else { - controlsettings.samplerate.target.duration = duration; - controlsettings.samplerate.target.samplerateSet = ControlSettingsSamplerateTarget::Duration; - } - - if (!specification->isFixedSamplerateDevice) { - // Calculate the maximum samplerate that would still provide the requested - // duration - double maxSamplerate = - (double)specification->samplerate.single.recordLengths[controlsettings.recordLengthId] / duration; - - // When possible, enable fast rate if the record time can't be set that low - // to improve resolution - bool fastRate = (controlsettings.usedChannels <= 1) && - (maxSamplerate >= specification->samplerate.multi.base / - specification->bufferDividers[controlsettings.recordLengthId]); - - // What is the nearest, at most as high samplerate the scope can provide? - unsigned downsampler = 0; - - // Set the calculated samplerate - if (this->updateSamplerate(downsampler, fastRate) == UINT_MAX) - return Dso::ErrorCode::PARAMETER; - else { - return Dso::ErrorCode::NONE; - } - } else { - // For now - we go for the 10240 size sampling - the other seems not to be supported - // Find highest samplerate using less than 10240 samples to obtain our duration. - unsigned sampleCount = 10240; - if (specification->isSoftwareTriggerDevice) sampleCount -= controlsettings.swSampleMargin; - unsigned sampleId; - for (sampleId = 0; sampleId < specification->fixedSampleRates.size(); ++sampleId) { - if (specification->fixedSampleRates[sampleId].samplerate * duration < sampleCount) break; - } - // Usable sample value - modifyCommand(ControlCode::CONTROL_SETTIMEDIV) - ->setDiv(specification->fixedSampleRates[sampleId].id); - controlsettings.samplerate.current = specification->fixedSampleRates[sampleId].samplerate; - - emit samplerateChanged(controlsettings.samplerate.current); - return Dso::ErrorCode::NONE; - } -} - -Dso::ErrorCode HantekDsoControl::setChannelUsed(ChannelID channel, bool used) { - if (!device->isConnected()) return Dso::ErrorCode::CONNECTION; - - if (channel >= specification->channels) return Dso::ErrorCode::PARAMETER; - - // Update settings - controlsettings.voltage[channel].used = used; - ChannelID channelCount = 0; - for (unsigned c = 0; c < specification->channels; ++c) { - if (controlsettings.voltage[c].used) ++channelCount; - } - - // Calculate the UsedChannels field for the command - UsedChannels usedChannels = UsedChannels::USED_CH1; - - if (controlsettings.voltage[1].used) { - if (controlsettings.voltage[0].used) { - usedChannels = UsedChannels::USED_CH1CH2; - } else { - // DSO-2250 uses a different value for channel 2 - if (specification->cmdSetChannels == BulkCode::BSETCHANNELS) - usedChannels = UsedChannels::BUSED_CH2; - else - usedChannels = UsedChannels::USED_CH2; - } - } - - switch (specification->cmdSetChannels) { - case BulkCode::SETTRIGGERANDSAMPLERATE: { - // SetTriggerAndSamplerate bulk command for trigger source - modifyCommand(BulkCode::SETTRIGGERANDSAMPLERATE) - ->setUsedChannels((uint8_t)usedChannels); - break; - } - case BulkCode::BSETCHANNELS: { - // SetChannels2250 bulk command for active channels - modifyCommand(BulkCode::BSETCHANNELS)->setUsedChannels((uint8_t)usedChannels); - break; - } - case BulkCode::ESETTRIGGERORSAMPLERATE: { - // SetTrigger5200s bulk command for trigger source - modifyCommand(BulkCode::ESETTRIGGERORSAMPLERATE)->setUsedChannels((uint8_t)usedChannels); - break; - } - default: - break; - } - - // Check if fast rate mode availability changed - bool fastRateChanged = (controlsettings.usedChannels <= 1) != (channelCount <= 1); - controlsettings.usedChannels = channelCount; - - if (fastRateChanged) this->updateSamplerateLimits(); - - return Dso::ErrorCode::NONE; -} - -Dso::ErrorCode HantekDsoControl::setCoupling(ChannelID channel, Dso::Coupling coupling) { - if (!device->isConnected()) return Dso::ErrorCode::CONNECTION; - - if (channel >= specification->channels) return Dso::ErrorCode::PARAMETER; - - // SetRelays control command for coupling relays - if (specification->supportsCouplingRelays) { - modifyCommand(ControlCode::CONTROL_SETRELAYS) - ->setCoupling(channel, coupling != Dso::Coupling::AC); - } - - return Dso::ErrorCode::NONE; -} - -Dso::ErrorCode HantekDsoControl::setGain(ChannelID channel, double gain) { - if (!device->isConnected()) return Dso::ErrorCode::CONNECTION; - - if (channel >= specification->channels) return Dso::ErrorCode::PARAMETER; - - // Find lowest gain voltage thats at least as high as the requested - unsigned gainID; - for (gainID = 0; gainID < specification->gain.size() - 1; ++gainID) - if (specification->gain[gainID].gainSteps >= gain) break; - - if (specification->useControlNoBulk) { - if (channel == 0) { - modifyCommand(ControlCode::CONTROL_SETVOLTDIV_CH1) - ->setDiv(specification->gain[gainID].gainIndex); - } else if (channel == 1) { - modifyCommand(ControlCode::CONTROL_SETVOLTDIV_CH2) - ->setDiv(specification->gain[gainID].gainIndex); - } else - qDebug("%s: Unsuported channel: %i\n", __func__, channel); - } else { - // SetGain bulk command for gain - modifyCommand(BulkCode::SETGAIN)->setGain(channel, specification->gain[gainID].gainIndex); - - // SetRelays control command for gain relays - ControlSetRelays *controlSetRelays = modifyCommand(ControlCode::CONTROL_SETRELAYS); - controlSetRelays->setBelow1V(channel, gainID < 3); - controlSetRelays->setBelow100mV(channel, gainID < 6); - } - - controlsettings.voltage[channel].gain = gainID; - - this->setOffset(channel, controlsettings.voltage[channel].offset); - - return Dso::ErrorCode::NONE; -} - -Dso::ErrorCode HantekDsoControl::setOffset(ChannelID channel, const double offset) { - if (!device->isConnected()) return Dso::ErrorCode::CONNECTION; - - if (channel >= specification->channels) return Dso::ErrorCode::PARAMETER; - - if (specification->supportsOffset) { - Offset &channelOffLimit = controlsettings.offsetLimit[channel].step[controlsettings.voltage[channel].gain]; - // Calculate the offset value - // The range is given by the calibration data (convert from big endian) - unsigned short int minimum = ((unsigned short int)*((unsigned char *)&(channelOffLimit.start)) << 8) + - *((unsigned char *)&(channelOffLimit.start) + 1); - unsigned short int maximum = ((unsigned short int)*((unsigned char *)&(channelOffLimit.end)) << 8) + - *((unsigned char *)&(channelOffLimit.end) + 1); - unsigned short int offsetValue = offset * (maximum - minimum) + minimum + 0.5; - double offsetReal = (double)(offsetValue - minimum) / (maximum - minimum); - - modifyCommand(ControlCode::CONTROL_SETOFFSET)->setChannel(channel, offsetValue); - controlsettings.voltage[channel].offsetReal = offsetReal; - } - - controlsettings.voltage[channel].offset = offset; - - this->setTriggerLevel(channel, controlsettings.trigger.level[channel]); - - return Dso::ErrorCode::NONE; -} - -Dso::ErrorCode HantekDsoControl::setTriggerMode(Dso::TriggerMode mode) { - if (!device->isConnected()) return Dso::ErrorCode::CONNECTION; - - controlsettings.trigger.mode = mode; - return Dso::ErrorCode::NONE; -} - -Dso::ErrorCode HantekDsoControl::setTriggerSource(bool special, unsigned id) { - if (!device->isConnected()) return Dso::ErrorCode::CONNECTION; - if (specification->isSoftwareTriggerDevice) return Dso::ErrorCode::UNSUPPORTED; - - if (!special && id >= specification->channels) return Dso::ErrorCode::PARAMETER; - - if (special && id >= specification->specialTriggerChannels.size()) return Dso::ErrorCode::PARAMETER; - - int hardwareID = special ? specification->specialTriggerChannels[id].hardwareID : (int)id; - - switch (specification->cmdSetTrigger) { - case BulkCode::SETTRIGGERANDSAMPLERATE: - // SetTriggerAndSamplerate bulk command for trigger source - modifyCommand(BulkCode::SETTRIGGERANDSAMPLERATE)->setTriggerSource(1 - hardwareID); - break; - - case BulkCode::CSETTRIGGERORSAMPLERATE: - // SetTrigger2250 bulk command for trigger source - modifyCommand(BulkCode::CSETTRIGGERORSAMPLERATE)->setTriggerSource(2 + hardwareID); - break; - - case BulkCode::ESETTRIGGERORSAMPLERATE: - // SetTrigger5200 bulk command for trigger source - modifyCommand(BulkCode::ESETTRIGGERORSAMPLERATE)->setTriggerSource(1 - hardwareID); - break; - - default: - return Dso::ErrorCode::UNSUPPORTED; - } - - // SetRelays control command for external trigger relay - modifyCommand(ControlCode::CONTROL_SETRELAYS)->setTrigger(special); - - controlsettings.trigger.special = special; - controlsettings.trigger.source = id; - - // Apply trigger level of the new source - if (special) { - // SetOffset control command for changed trigger level - modifyCommand(ControlCode::CONTROL_SETOFFSET)->setTrigger(0x7f); - } else - this->setTriggerLevel(id, controlsettings.trigger.level[id]); - - return Dso::ErrorCode::NONE; -} - -Dso::ErrorCode HantekDsoControl::setTriggerLevel(ChannelID channel, double level) { - if (!device->isConnected()) return Dso::ErrorCode::CONNECTION; - - if (channel >= specification->channels) return Dso::ErrorCode::PARAMETER; - - controlsettings.trigger.level[channel] = level; - if (!specification->supportsOffset) return Dso::ErrorCode::UNSUPPORTED; - - // Calculate the trigger level value - unsigned short minimum, maximum; - if (specification->sampleSize > 8) { - Offset &offsetLimit = controlsettings.offsetLimit[channel].step[controlsettings.voltage[channel].gain]; - // The range is the same as used for the offsets for 10 bit models - minimum = ((unsigned short)*((unsigned char *)&(offsetLimit.start)) << 8) + - *((unsigned char *)&(offsetLimit.start) + 1); - maximum = - ((unsigned short)*((unsigned char *)&(offsetLimit.end)) << 8) + *((unsigned char *)&(offsetLimit.end) + 1); - } else { - // It's from 0x00 to 0xfd for the 8 bit models - minimum = 0x00; - maximum = 0xfd; - } - - // Never get out of the limits - const unsigned gainID = controlsettings.voltage[channel].gain; - const double offsetReal = controlsettings.voltage[channel].offsetReal; - const double gainStep = specification->gain[gainID].gainSteps; - const unsigned short levelValue = qBound( - minimum, (unsigned short)(((offsetReal + level / gainStep) * (maximum - minimum) + 0.5) + minimum), maximum); - - // Check if the set channel is the trigger source - if (!controlsettings.trigger.special && channel == controlsettings.trigger.source) { - // SetOffset control command for trigger level - modifyCommand(ControlCode::CONTROL_SETOFFSET)->setTrigger(levelValue); - } - - /// \todo Get alternating trigger in here - - return Dso::ErrorCode::NONE; -} - -Dso::ErrorCode HantekDsoControl::setTriggerSlope(Dso::Slope slope) { - if (!device->isConnected()) return Dso::ErrorCode::CONNECTION; - - switch (specification->cmdSetTrigger) { - case BulkCode::SETTRIGGERANDSAMPLERATE: { - // SetTriggerAndSamplerate bulk command for trigger slope - modifyCommand(BulkCode::SETTRIGGERANDSAMPLERATE)->setTriggerSlope((uint8_t)slope); - break; - } - case BulkCode::CSETTRIGGERORSAMPLERATE: { - // SetTrigger2250 bulk command for trigger slope - modifyCommand(BulkCode::CSETTRIGGERORSAMPLERATE)->setTriggerSlope((uint8_t)slope); - break; - } - case BulkCode::ESETTRIGGERORSAMPLERATE: { - // SetTrigger5200 bulk command for trigger slope - modifyCommand(BulkCode::ESETTRIGGERORSAMPLERATE)->setTriggerSlope((uint8_t)slope); - break; - } - default: - return Dso::ErrorCode::UNSUPPORTED; - } - - controlsettings.trigger.slope = slope; - return Dso::ErrorCode::NONE; -} - -void HantekDsoControl::forceTrigger() { modifyCommand(BulkCode::FORCETRIGGER); } - -Dso::ErrorCode HantekDsoControl::setPretriggerPosition(double position) { - if (!device->isConnected()) return Dso::ErrorCode::CONNECTION; - - // All trigger positions are measured in samples - double positionSamples = position * controlsettings.samplerate.current; - unsigned recordLength = getRecordLength(); - // Fast rate mode uses both channels - if (isFastRate()) positionSamples /= specification->channels; - - switch (specification->cmdSetPretrigger) { - case BulkCode::SETTRIGGERANDSAMPLERATE: { - // Calculate the position value (Start point depending on record length) - unsigned triggerPosition = isRollMode() ? 0x1 : 0x7ffff - recordLength + (unsigned)positionSamples; - - // SetTriggerAndSamplerate bulk command for trigger position - modifyCommand(BulkCode::SETTRIGGERANDSAMPLERATE) - ->setTriggerPosition(triggerPosition); - break; - } - case BulkCode::FSETBUFFER: { - // Calculate the position values (Inverse, maximum is 0x7ffff) - unsigned positionPre = 0x7ffff - recordLength + (unsigned)positionSamples; - unsigned positionPost = 0x7ffff - (unsigned)positionSamples; - - // SetBuffer2250 bulk command for trigger position - BulkSetBuffer2250 *commandSetBuffer2250 = modifyCommand(BulkCode::FSETBUFFER); - commandSetBuffer2250->setTriggerPositionPre(positionPre); - commandSetBuffer2250->setTriggerPositionPost(positionPost); - break; - } - case BulkCode::ESETTRIGGERORSAMPLERATE: { - // Calculate the position values (Inverse, maximum is 0xffff) - unsigned positionPre = 0xffff - recordLength + (unsigned)positionSamples; - unsigned positionPost = 0xffff - (unsigned)positionSamples; - - // SetBuffer5200 bulk command for trigger position - BulkSetBuffer5200 *commandSetBuffer5200 = modifyCommand(BulkCode::DSETBUFFER); - commandSetBuffer5200->setTriggerPositionPre((unsigned short)positionPre); - commandSetBuffer5200->setTriggerPositionPost((unsigned short)positionPost); - break; - } - default: - return Dso::ErrorCode::UNSUPPORTED; - } - - controlsettings.trigger.position = position; - return Dso::ErrorCode::NONE; -} - -Dso::ErrorCode HantekDsoControl::stringCommand(const QString &commandString) { - if (!device->isConnected()) return Dso::ErrorCode::CONNECTION; - - QStringList commandParts = commandString.split(' ', QString::SkipEmptyParts); - - if (commandParts.count() < 1) return Dso::ErrorCode::PARAMETER; - if (commandParts[0] != "send") return Dso::ErrorCode::UNSUPPORTED; - if (commandParts.count() < 2) return Dso::ErrorCode::PARAMETER; - - uint8_t codeIndex = 0; - hexParse(commandParts[2], &codeIndex, 1); - QString data = commandString.section(' ', 2, -1, QString::SectionSkipEmpty); - - if (commandParts[1] == "bulk") { - if (!command[codeIndex]) return Dso::ErrorCode::UNSUPPORTED; - - BulkCommand *c = modifyCommand((BulkCode)codeIndex); - hexParse(data, c->data(), c->size()); - return Dso::ErrorCode::NONE; - } else if (commandParts[1] == "control") { - if (!control[codeIndex]) return Dso::ErrorCode::UNSUPPORTED; - - ControlCommand *c = modifyCommand((ControlCode)codeIndex); - hexParse(data, c->data(), c->size()); - return Dso::ErrorCode::NONE; - } else - return Dso::ErrorCode::UNSUPPORTED; -} - -void HantekDsoControl::addCommand(BulkCommand *newCommand, bool pending) { - newCommand->pending = pending; - command[(uint8_t)newCommand->code] = newCommand; - newCommand->next = firstBulkCommand; - firstBulkCommand = newCommand; -} - -const BulkCommand *HantekDsoControl::getCommand(BulkCode code) const { return command[(uint8_t)code]; } - -void HantekDsoControl::addCommand(ControlCommand *newCommand, bool pending) { - newCommand->pending = pending; - control[newCommand->code] = newCommand; - newCommand->next = firstControlCommand; - firstControlCommand = newCommand; -} - -const ControlCommand *HantekDsoControl::getCommand(ControlCode code) const { return control[(uint8_t)code]; } - -void HantekDsoControl::run() { - int errorCode = 0; - - // Send all pending bulk commands - BulkCommand *command = firstBulkCommand; - while (command) { - if (command->pending) { - timestampDebug(QString("Sending bulk command:%1").arg(hexDump(command->data(), command->size()))); - - errorCode = bulkCommand(command); - if (errorCode < 0) { - qWarning() << "Sending bulk command failed: " << libUsbErrorString(errorCode); - emit communicationError(); - return; - } else - command->pending = false; - } - command = command->next; - } - - // Send all pending control commands - ControlCommand *controlCommand = firstControlCommand; - while (controlCommand) { - if (controlCommand->pending) { - timestampDebug(QString("Sending control command %1:%2") - .arg(QString::number(controlCommand->code, 16), - hexDump(controlCommand->data(), controlCommand->size()))); - - errorCode = device->controlWrite(controlCommand); - if (errorCode < 0) { - qWarning("Sending control command %2x failed: %s", (uint8_t)controlCommand->code, - libUsbErrorString(errorCode).toLocal8Bit().data()); - - if (errorCode == LIBUSB_ERROR_NO_DEVICE) { - emit communicationError(); - return; - } - } else - controlCommand->pending = false; - } - controlCommand = controlCommand->next; - } - - // State machine for the device communication - if (isRollMode()) { - // Roll mode - this->captureState = CAPTURE_WAITING; - bool toNextState = true; - - switch (this->rollState) { - case RollState::STARTSAMPLING: - // Don't iterate through roll mode steps when stopped - if (!this->sampling) { - toNextState = false; - break; - } - - // Sampling hasn't started, update the expected sample count - expectedSampleCount = this->getSampleCount(); - - errorCode = bulkCommand(getCommand(BulkCode::STARTSAMPLING)); - if (errorCode < 0) { - if (errorCode == LIBUSB_ERROR_NO_DEVICE) { - emit communicationError(); - return; - } - break; - } - - timestampDebug("Starting to capture"); - - this->_samplingStarted = true; - - break; - - case RollState::ENABLETRIGGER: - errorCode = bulkCommand(getCommand(BulkCode::ENABLETRIGGER)); - if (errorCode < 0) { - if (errorCode == LIBUSB_ERROR_NO_DEVICE) { - emit communicationError(); - return; - } - break; - } - - timestampDebug("Enabling trigger"); - - break; - - case RollState::FORCETRIGGER: - errorCode = bulkCommand(getCommand(BulkCode::FORCETRIGGER)); - if (errorCode < 0) { - if (errorCode == LIBUSB_ERROR_NO_DEVICE) { - emit communicationError(); - return; - } - break; - } - - timestampDebug("Forcing trigger"); - - break; - - case RollState::GETDATA: { - std::vector rawData = this->getSamples(expectedSampleCount); - if (this->_samplingStarted) { - convertRawDataToSamples(rawData); - emit samplesAvailable(&result); - } - } - - // Check if we're in single trigger mode - if (controlsettings.trigger.mode == Dso::TriggerMode::SINGLE && this->_samplingStarted) - this->enableSampling(false); - - // Sampling completed, restart it when necessary - this->_samplingStarted = false; - - break; - - default: - timestampDebug("Roll mode state unknown"); - break; - } - - // Go to next state, or restart if last state was reached - if (toNextState) this->rollState = (RollState)(((int)rollState + 1) % (int)RollState::_COUNT); - } else { - // Standard mode - this->rollState = RollState::STARTSAMPLING; - - const int lastCaptureState = this->captureState; - unsigned triggerPoint; - std::tie(captureState, triggerPoint) = this->getCaptureState(); - controlsettings.trigger.point = calculateTriggerPoint(triggerPoint); - if (this->captureState < 0) { - qWarning() << tr("Getting capture state failed: %1").arg(libUsbErrorString(this->captureState)); - emit statusMessage(tr("Getting capture state failed: %1").arg(libUsbErrorString(this->captureState)), 0); - } else if (this->captureState != lastCaptureState) - timestampDebug(QString("Capture state changed to %1").arg(this->captureState)); - - switch (this->captureState) { - case CAPTURE_READY: - case CAPTURE_READY2250: - case CAPTURE_READY5200: { - std::vector rawData = this->getSamples(expectedSampleCount); - if (this->_samplingStarted) { - convertRawDataToSamples(rawData); - emit samplesAvailable(&result); - } - } - - // Check if we're in single trigger mode - if (controlsettings.trigger.mode == Dso::TriggerMode::SINGLE && this->_samplingStarted) - this->enableSampling(false); - - // Sampling completed, restart it when necessary - this->_samplingStarted = false; - - // Start next capture if necessary by leaving out the break statement - - if (!this->sampling) break; -#if __has_cpp_attribute(clang::fallthrough) -#define FALLTHROUGH [[clang::fallthrough]]; -#elif __has_cpp_attribute(fallthrough) -#define FALLTHROUGH [[fallthrough]]; -#else -#define FALLTHROUGH -#endif - else { - FALLTHROUGH - } - case CAPTURE_WAITING: - // Sampling hasn't started, update the expected sample count - expectedSampleCount = this->getSampleCount(); - - if (_samplingStarted && lastTriggerMode == controlsettings.trigger.mode) { - ++this->cycleCounter; - - if (this->cycleCounter == this->startCycle && !isRollMode()) { - // Buffer refilled completely since start of sampling, enable the - // trigger now - errorCode = bulkCommand(getCommand(BulkCode::ENABLETRIGGER)); - if (errorCode < 0) { - if (errorCode == LIBUSB_ERROR_NO_DEVICE) { - emit communicationError(); - return; - } - break; - } - - timestampDebug("Enabling trigger"); - } else if (cycleCounter >= 8 + this->startCycle && - controlsettings.trigger.mode == Dso::TriggerMode::WAIT_FORCE) { - // Force triggering - errorCode = bulkCommand(getCommand(BulkCode::FORCETRIGGER)); - if (errorCode < 0) { - if (errorCode == LIBUSB_ERROR_NO_DEVICE) { - emit communicationError(); - return; - } - break; - } - - timestampDebug("Forcing trigger"); - } - - if (this->cycleCounter < 20 || this->cycleCounter < 4000 / cycleTime) break; - } - - // Start capturing - errorCode = bulkCommand(getCommand(BulkCode::STARTSAMPLING)); - if (errorCode < 0) { - if (errorCode == LIBUSB_ERROR_NO_DEVICE) { - emit communicationError(); - return; - } - break; - } - - timestampDebug("Starting to capture"); - - this->_samplingStarted = true; - this->cycleCounter = 0; - this->startCycle = int(controlsettings.trigger.position * 1000.0 / cycleTime + 1.0); - this->lastTriggerMode = controlsettings.trigger.mode; - break; - - case CAPTURE_SAMPLING: - break; - default: - break; - } - } - - this->updateInterval(); -#if (QT_VERSION >= QT_VERSION_CHECK(5, 4, 0)) - QTimer::singleShot(cycleTime, this, &HantekDsoControl::run); -#else - QTimer::singleShot(cycleTime, this, SLOT(run())); -#endif -} - -int HantekDsoControl::getConnectionSpeed() const { - int errorCode; - ControlGetSpeed response; - errorCode = device->controlRead(&response); - if (errorCode < 0) return errorCode; - - return response.getSpeed(); -} - -int HantekDsoControl::getPacketSize() const { - const int s = getConnectionSpeed(); - if (s == CONNECTION_FULLSPEED) - return 64; - else if (s == CONNECTION_HIGHSPEED) - return 512; - else if (s > CONNECTION_HIGHSPEED) { - qWarning() << "Unknown USB speed. Please correct source code in USBDevice::getPacketSize()"; - throw new std::runtime_error("Unknown USB speed"); - } else if (s < 0) - return s; - return 0; -} diff --git a/openhantek/src/hantekdso/hantekdsocontrol.h b/openhantek/src/hantekdso/hantekdsocontrol.h deleted file mode 100644 index 1765da4f..00000000 --- a/openhantek/src/hantekdso/hantekdsocontrol.h +++ /dev/null @@ -1,296 +0,0 @@ -// SPDX-License-Identifier: GPL-2.0+ - -#pragma once - -#define NOMINMAX // disable windows.h min/max global methods -#include - -#include "controlsettings.h" -#include "controlspecification.h" -#include "dsosamples.h" -#include "errorcodes.h" -#include "states.h" -#include "utils/printutils.h" - -#include "hantekprotocol/bulkStructs.h" -#include "hantekprotocol/controlStructs.h" -#include "hantekprotocol/definitions.h" - -#include - -#include -#include -#include -#include - -class USBDevice; - -/// \brief The DsoControl abstraction layer for %Hantek USB DSOs. -/// TODO Please anyone, refactor this class into smaller pieces (Separation of Concerns!). -class HantekDsoControl : public QObject { - Q_OBJECT - - public: - /** - * Creates a dsoControl object. The actual event loop / timer is not started. - * You can optionally create a thread and move the created object to the - * thread. - * You need to call updateInterval() to start the timer. This is done implicitly - * if run() is called. - * @param device The usb device. This object does not take ownership. - */ - HantekDsoControl(USBDevice *device); - - /// \brief Cleans up - ~HantekDsoControl(); - - /// Call this to start the processing. This method will call itself - /// periodically from there on. - /// It is wise to move this class object to an own thread and call run from - /// there. - void run(); - - /// \brief Gets the physical channel count for this oscilloscope. - /// \return The number of physical channels. - unsigned getChannelCount() const; - - /// Return the read-only device control settings. Use the set- Methods to change - /// device settings. - const Dso::ControlSettings *getDeviceSettings() const; - - /// \brief Get available record lengths for this oscilloscope. - /// \return The number of physical channels, empty list for continuous. - const std::vector &getAvailableRecordLengths() const; - - /// \brief Get minimum samplerate for this oscilloscope. - /// \return The minimum samplerate for the current configuration in S/s. - double getMinSamplerate() const; - - /// \brief Get maximum samplerate for this oscilloscope. - /// \return The maximum samplerate for the current configuration in S/s. - double getMaxSamplerate() const; - - bool isSampling() const; - - /// Return the associated usb device. - const USBDevice *getDevice() const; - - /// \brief Gets the speed of the connection. - /// \return The ::ConnectionSpeed of the USB connection. - int getConnectionSpeed() const; - - /// \brief Gets the maximum size of one packet transmitted via bulk transfer. - /// \return The maximum packet size in bytes, negative libusb error code on error. - int getPacketSize() const; - - /// Return the last sample set - const DSOsamples &getLastSamples(); - - /// \brief Sends bulk/control commands directly. - ///

- /// Syntax:
- ///
- /// Bulk command: - ///

send bulk [hex data]
- /// %Control command: - ///
send control [hex code] [hex data]
- ///

- /// \param command The command as string (Has to be parsed). - /// \return See ::Dso::ErrorCode. - Dso::ErrorCode stringCommand(const QString &commandString); - - void addCommand(BulkCommand *newCommand, bool pending = true); - template T *modifyCommand(Hantek::BulkCode code) { - command[(uint8_t)code]->pending = true; - return static_cast(command[(uint8_t)code]); - } - const BulkCommand *getCommand(Hantek::BulkCode code) const; - - void addCommand(ControlCommand *newCommand, bool pending = true); - template T *modifyCommand(Hantek::ControlCode code) { - control[(uint8_t)code]->pending = true; - return static_cast(control[(uint8_t)code]); - } - const ControlCommand *getCommand(Hantek::ControlCode code) const; - - private: - bool isRollMode() const; - bool isFastRate() const; - unsigned getRecordLength() const; - - Dso::ErrorCode retrieveChannelLevelData(); - - /// \brief Calculated the nearest samplerate supported by the oscilloscope. - /// \param samplerate The target samplerate, that should be met as good as - /// possible. - /// \param fastRate true, if the fast rate mode is enabled. - /// \param maximum The target samplerate is the maximum allowed when true, the - /// minimum otherwise. - /// \param downsampler Pointer to where the selected downsampling factor should - /// be written. - /// \return The nearest samplerate supported, 0.0 on error. - double getBestSamplerate(double samplerate, bool fastRate = false, bool maximum = false, - unsigned *downsampler = 0) const; - - /// Get the number of samples that are expected returned by the scope. - /// In rolling mode this is depends on the usb speed and packet size. - /// \return The total number of samples the scope should return. - unsigned getSampleCount() const; - - void updateInterval(); - - /// \brief Calculates the trigger point from the CommandGetCaptureState data. - /// \param value The data value that contains the trigger point. - /// \return The calculated trigger point for the given data. - static unsigned calculateTriggerPoint(unsigned value); - - /// \brief Gets the current state. - /// \return The current CaptureState of the oscilloscope. - std::pair getCaptureState() const; - - /// \brief Gets sample data from the oscilloscope - std::vector getSamples(unsigned &expectedSampleCount) const; - - /// \brief Converts raw oscilloscope data to sample data - void convertRawDataToSamples(const std::vector &rawData); - - /// \brief Sets the size of the sample buffer without updating dependencies. - /// \param index The record length index that should be set. - /// \return The record length that has been set, 0 on error. - unsigned updateRecordLength(RecordLengthID size); - - /// \brief Sets the samplerate based on the parameters calculated by - /// Control::getBestSamplerate. - /// \param downsampler The downsampling factor. - /// \param fastRate true, if one channel uses all buffers. - /// \return The downsampling factor that has been set. - unsigned updateSamplerate(unsigned downsampler, bool fastRate); - - /// \brief Restore the samplerate/timebase targets after divider updates. - void restoreTargets(); - - /// \brief Update the minimum and maximum supported samplerate. - void updateSamplerateLimits(); - - private: - /// Pointers to bulk/control commands - BulkCommand *command[255] = {0}; - BulkCommand *firstBulkCommand = nullptr; - ControlCommand *control[255] = {0}; - ControlCommand *firstControlCommand = nullptr; - - // Communication with device - USBDevice *device; ///< The USB device for the oscilloscope - bool sampling = false; ///< true, if the oscilloscope is taking samples - - // Device setup - const Dso::ControlSpecification *specification; ///< The specifications of the device - Dso::ControlSettings controlsettings; ///< The current settings of the device - - // Results - DSOsamples result; - unsigned expectedSampleCount = 0; ///< The expected total number of samples at - /// the last check before sampling started - - // State of the communication thread - int captureState = Hantek::CAPTURE_WAITING; - Hantek::RollState rollState = Hantek::RollState::STARTSAMPLING; - bool _samplingStarted = false; - Dso::TriggerMode lastTriggerMode = (Dso::TriggerMode)-1; - int cycleCounter = 0; - int startCycle = 0; - int cycleTime = 0; - - /// \brief Send a bulk command to the oscilloscope. - /// \param command The command, that should be sent. - /// \param attempts The number of attempts, that are done on timeouts. - /// \return Number of sent bytes on success, libusb error code on error. - int bulkCommand(const std::vector *command, int attempts = HANTEK_ATTEMPTS) const; - - public slots: - /// \brief If sampling is disabled, no samplesAvailable() signals are send anymore, no samples - /// are fetched from the device and no processing takes place. - /// \param enabled Enables/Disables sampling - void enableSampling(bool enabled); - - /// \brief Sets the size of the oscilloscopes sample buffer. - /// \param index The record length index that should be set. - /// \return The record length that has been set, 0 on error. - Dso::ErrorCode setRecordLength(unsigned size); - /// \brief Sets the samplerate of the oscilloscope. - /// \param samplerate The samplerate that should be met (S/s), 0.0 to restore - /// current samplerate. - /// \return The samplerate that has been set, 0.0 on error. - Dso::ErrorCode setSamplerate(double samplerate = 0.0); - /// \brief Sets the time duration of one aquisition by adapting the samplerate. - /// \param duration The record time duration that should be met (s), 0.0 to - /// restore current record time. - /// \return The record time duration that has been set, 0.0 on error. - Dso::ErrorCode setRecordTime(double duration = 0.0); - - /// \brief Enables/disables filtering of the given channel. - /// \param channel The channel that should be set. - /// \param used true if the channel should be sampled. - /// \return See ::Dso::ErrorCode. - Dso::ErrorCode setChannelUsed(ChannelID channel, bool used); - /// \brief Set the coupling for the given channel. - /// \param channel The channel that should be set. - /// \param coupling The new coupling for the channel. - /// \return See ::Dso::ErrorCode. - Dso::ErrorCode setCoupling(ChannelID channel, Dso::Coupling coupling); - /// \brief Sets the gain for the given channel. - /// Get the actual gain by specification.gainSteps[gainId] - /// \param channel The channel that should be set. - /// \param gain The gain that should be met (V/div). - /// \return The gain that has been set, ::Dso::ErrorCode on error. - Dso::ErrorCode setGain(ChannelID channel, double gain); - /// \brief Set the offset for the given channel. - /// Get the actual offset for the channel from controlsettings.voltage[channel].offsetReal - /// \param channel The channel that should be set. - /// \param offset The new offset value (0.0 - 1.0). - Dso::ErrorCode setOffset(ChannelID channel, const double offset); - - /// \brief Set the trigger mode. - /// \return See ::Dso::ErrorCode. - Dso::ErrorCode setTriggerMode(Dso::TriggerMode mode); - /// \brief Set the trigger source. - /// \param special true for a special channel (EXT, ...) as trigger source. - /// \param id The number of the channel, that should be used as trigger. - /// \return See ::Dso::ErrorCode. - Dso::ErrorCode setTriggerSource(bool special, unsigned id); - /// \brief Set the trigger level. - /// \param channel The channel that should be set. - /// \param level The new trigger level (V). - /// \return The trigger level that has been set, ::Dso::ErrorCode on error. - Dso::ErrorCode setTriggerLevel(ChannelID channel, double level); - /// \brief Set the trigger slope. - /// \param slope The Slope that should cause a trigger. - /// \return See ::Dso::ErrorCode. - Dso::ErrorCode setTriggerSlope(Dso::Slope slope); - /// \brief Set the trigger position. - /// \param position The new trigger position (in s). - /// \return The trigger position that has been set. - Dso::ErrorCode setPretriggerPosition(double position); - void forceTrigger(); - - signals: - void samplingStatusChanged(bool enabled); ///< The oscilloscope started/stopped sampling/waiting for trigger - void statusMessage(const QString &message, int timeout); ///< Status message about the oscilloscope - void samplesAvailable(const DSOsamples *samples); ///< New sample data is available - - void availableRecordLengthsChanged(const std::vector &recordLengths); ///< The available record - /// lengths, empty list for - - /// The available samplerate range has changed - void samplerateLimitsChanged(double minimum, double maximum); - /// The available samplerate for fixed samplerate devices has changed - void samplerateSet(int mode, QList sampleSteps); - - void recordLengthChanged(unsigned long duration); ///< The record length has changed - void recordTimeChanged(double duration); ///< The record time duration has changed - void samplerateChanged(double samplerate); ///< The samplerate has changed - - void communicationError() const; -}; - -Q_DECLARE_METATYPE(DSOsamples *) diff --git a/openhantek/src/hantekdso/modelregistry.cpp b/openhantek/src/hantekdso/modelregistry.cpp index 7c3f9c5d..171f7232 100644 --- a/openhantek/src/hantekdso/modelregistry.cpp +++ b/openhantek/src/hantekdso/modelregistry.cpp @@ -2,9 +2,10 @@ #include "modelregistry.h" -ModelRegistry *ModelRegistry::instance = new ModelRegistry(); - -ModelRegistry *ModelRegistry::get() { return instance; } +ModelRegistry *ModelRegistry::get() { + static ModelRegistry *instance = new ModelRegistry(); + return instance; +} void ModelRegistry::add(DSOModel *model) { supportedModels.push_back(model); } diff --git a/openhantek/src/hantekdso/modelregistry.h b/openhantek/src/hantekdso/modelregistry.h index 31289db2..1e1872fc 100644 --- a/openhantek/src/hantekdso/modelregistry.h +++ b/openhantek/src/hantekdso/modelregistry.h @@ -1,4 +1,3 @@ - // SPDX-License-Identifier: GPL-2.0+ #pragma once @@ -6,11 +5,11 @@ #include "dsomodel.h" class ModelRegistry { -public: + public: static ModelRegistry *get(); - void add(DSOModel* model); - const std::list models() const; -private: - static ModelRegistry* instance; - std::list supportedModels; + void add(DSOModel *model); + const std::list models() const; + + private: + std::list supportedModels; }; diff --git a/openhantek/src/hantekdso/models/modelDSO2090.cpp b/openhantek/src/hantekdso/models/modelDSO2090.cpp index cb81bb42..1035935f 100644 --- a/openhantek/src/hantekdso/models/modelDSO2090.cpp +++ b/openhantek/src/hantekdso/models/modelDSO2090.cpp @@ -1,65 +1,62 @@ #include "modelDSO2090.h" -#include "hantekprotocol/bulkStructs.h" -#include "hantekprotocol/controlStructs.h" -#include "hantekdsocontrol.h" +#include "dsocommandqueue.h" +#include "modelspecification.h" using namespace Hantek; static ModelDSO2090 modelInstance; static ModelDSO2090A modelInstance2; -void _applyRequirements(HantekDsoControl *dsoControl) { - dsoControl->addCommand(new BulkForceTrigger(), false); - dsoControl->addCommand(new BulkCaptureStart(), false); - dsoControl->addCommand(new BulkTriggerEnabled(), false); - dsoControl->addCommand(new BulkGetData(), false); - dsoControl->addCommand(new BulkGetCaptureState(), false); - dsoControl->addCommand(new BulkSetGain(), false); - - dsoControl->addCommand(new BulkSetTriggerAndSamplerate(), false); - dsoControl->addCommand(new ControlSetOffset(), false); - dsoControl->addCommand(new ControlSetRelays(), false); +void _applyRequirements(DsoCommandQueue *commandQueue) { + commandQueue->addCommand(new BulkForceTrigger(), false); + commandQueue->addCommand(new BulkCaptureStart(), false); + commandQueue->addCommand(new BulkTriggerEnabled(), false); + commandQueue->addCommand(new BulkGetData(), false); + commandQueue->addCommand(new BulkGetCaptureState(), false); + commandQueue->addCommand(new BulkSetGain(), false); + + commandQueue->addCommand(new BulkSetTriggerAndSamplerate(), false); + commandQueue->addCommand(new ControlSetOffset(), false); + commandQueue->addCommand(new ControlSetRelays(), false); } -void initSpecifications(Dso::ControlSpecification& specification) { - specification.cmdSetRecordLength = BulkCode::SETTRIGGERANDSAMPLERATE; - specification.cmdSetChannels = BulkCode::SETTRIGGERANDSAMPLERATE; - specification.cmdSetSamplerate = BulkCode::SETTRIGGERANDSAMPLERATE; - specification.cmdSetTrigger = BulkCode::SETTRIGGERANDSAMPLERATE; - specification.cmdSetPretrigger = BulkCode::SETTRIGGERANDSAMPLERATE; - - specification.samplerate.single.base = 50e6; - specification.samplerate.single.max = 50e6; - specification.samplerate.single.maxDownsampler = 131072; - specification.samplerate.single.recordLengths = {UINT_MAX, 10240, 32768}; - specification.samplerate.multi.base = 100e6; - specification.samplerate.multi.max = 100e6; - specification.samplerate.multi.maxDownsampler = 131072; - specification.samplerate.multi.recordLengths = {UINT_MAX, 20480, 65536}; - specification.bufferDividers = { 1000 , 1 , 1 }; - specification.voltageLimit[0] = { 255 , 255 , 255 , 255 , 255 , 255 , 255 , 255 , 255 }; - specification.voltageLimit[1] = { 255 , 255 , 255 , 255 , 255 , 255 , 255 , 255 , 255 }; - specification.gain = { {0,0.08} , {1,0.16} , {2,0.40} , {0,0.80} , - {1,1.60} , {2,4.00} , {0,8.00} , {1,16.00} , {2,40.00} }; - specification.sampleSize = 8; - specification.specialTriggerChannels = {{"EXT", -2}, {"EXT/10", -3}}; +void initSpecifications(Dso::ModelSpec *specification) { + specification->cmdSetRecordLength = BulkCode::SETTRIGGERANDSAMPLERATE; + specification->cmdSetChannels = BulkCode::SETTRIGGERANDSAMPLERATE; + specification->cmdSetSamplerate = BulkCode::SETTRIGGERANDSAMPLERATE; + specification->cmdSetTrigger = BulkCode::SETTRIGGERANDSAMPLERATE; + specification->cmdSetPretrigger = BulkCode::SETTRIGGERANDSAMPLERATE; + + specification->normalSamplerate.base = 50e6; + specification->normalSamplerate.max = 50e6; + specification->normalSamplerate.maxDownsampler = 131072; + specification->normalSamplerate.recordLengths = {{UINT_MAX, 1000}, {10240, 1}, {32768, 1}}; + specification->fastrateSamplerate.base = 100e6; + specification->fastrateSamplerate.max = 100e6; + specification->fastrateSamplerate.maxDownsampler = 131072; + specification->fastrateSamplerate.recordLengths = {{UINT_MAX, 1000}, {20480, 1}, {65536, 1}}; + specification->calibration[0] = {{0x0000, 0xffff, 255}, {0x0000, 0xffff, 255}, {0x0000, 0xffff, 255}, + {0x0000, 0xffff, 255}, {0x0000, 0xffff, 255}, {0x0000, 0xffff, 255}, + {0x0000, 0xffff, 255}, {0x0000, 0xffff, 255}, {0x0000, 0xffff, 255}}; + specification->calibration[1] = {{0x0000, 0xffff, 255}, {0x0000, 0xffff, 255}, {0x0000, 0xffff, 255}, + {0x0000, 0xffff, 255}, {0x0000, 0xffff, 255}, {0x0000, 0xffff, 255}, + {0x0000, 0xffff, 255}, {0x0000, 0xffff, 255}, {0x0000, 0xffff, 255}}; + specification->gain = {{0, 0.08}, {1, 0.16}, {2, 0.40}, {0, 0.80}, {1, 1.60}, + {2, 4.00}, {0, 8.00}, {1, 16.00}, {2, 40.00}}; + specification->sampleSize = 8; + specification->specialTriggerChannels = {{"EXT", -2}, {"EXT/10", -3}}; } -ModelDSO2090::ModelDSO2090() : DSOModel(ID, 0x04b5, 0x2090, 0x04b4, 0x2090, "dso2090x86", "DSO-2090", - Dso::ControlSpecification(2)) { +ModelDSO2090::ModelDSO2090() + : DSOModel(ID, 0x04b5, 0x2090, 0x04b4, 0x2090, "dso2090x86", "DSO-2090", new Dso::ModelSpec(2)) { initSpecifications(specification); } -void ModelDSO2090::applyRequirements(HantekDsoControl *dsoControl) const { - _applyRequirements(dsoControl); -} +void ModelDSO2090::applyRequirements(DsoCommandQueue *dsoControl) const { _applyRequirements(dsoControl); } -ModelDSO2090A::ModelDSO2090A() : DSOModel(ID, 0x04b5, 0x2090, 0x04b4, 0x8613, "dso2090x86", "DSO-2090", - Dso::ControlSpecification(2)) { +ModelDSO2090A::ModelDSO2090A() + : DSOModel(ID, 0x04b5, 0x2090, 0x04b4, 0x8613, "dso2090x86", "DSO-2090", new Dso::ModelSpec(2)) { initSpecifications(specification); } -void ModelDSO2090A::applyRequirements(HantekDsoControl *dsoControl) const { - _applyRequirements(dsoControl); -} - +void ModelDSO2090A::applyRequirements(DsoCommandQueue *dsoControl) const { _applyRequirements(dsoControl); } diff --git a/openhantek/src/hantekdso/models/modelDSO2090.h b/openhantek/src/hantekdso/models/modelDSO2090.h index 55458918..69a5d3b9 100644 --- a/openhantek/src/hantekdso/models/modelDSO2090.h +++ b/openhantek/src/hantekdso/models/modelDSO2090.h @@ -2,18 +2,17 @@ #include "dsomodel.h" -class HantekDsoControl; -using namespace Hantek; +class DsoCommandQueue; struct ModelDSO2090 : public DSOModel { static const int ID = 0x2090; ModelDSO2090(); - void applyRequirements(HantekDsoControl* dsoControl) const override; + void applyRequirements(DsoCommandQueue *dsoControl) const override; }; struct ModelDSO2090A : public DSOModel { static const int ID = 0x2090; ModelDSO2090A(); - void applyRequirements(HantekDsoControl* dsoControl) const override; + void applyRequirements(DsoCommandQueue* dsoControl) const override; }; diff --git a/openhantek/src/hantekdso/models/modelDSO2150.cpp b/openhantek/src/hantekdso/models/modelDSO2150.cpp index 0334eea1..ce9f1733 100644 --- a/openhantek/src/hantekdso/models/modelDSO2150.cpp +++ b/openhantek/src/hantekdso/models/modelDSO2150.cpp @@ -1,46 +1,48 @@ #include "modelDSO2150.h" -#include "hantekprotocol/bulkStructs.h" -#include "hantekprotocol/controlStructs.h" -#include "hantekdsocontrol.h" +#include "dsocommandqueue.h" +#include "modelspecification.h" using namespace Hantek; static ModelDSO2150 modelInstance; -ModelDSO2150::ModelDSO2150() : DSOModel(ID, 0x04b5, 0x2150, 0x04b4, 0x2150, "dso2150x86", "DSO-2150", - Dso::ControlSpecification(2)) { - specification.cmdSetRecordLength = BulkCode::SETTRIGGERANDSAMPLERATE; - specification.cmdSetChannels = BulkCode::SETTRIGGERANDSAMPLERATE; - specification.cmdSetSamplerate = BulkCode::SETTRIGGERANDSAMPLERATE; - specification.cmdSetTrigger = BulkCode::SETTRIGGERANDSAMPLERATE; - specification.cmdSetPretrigger = BulkCode::SETTRIGGERANDSAMPLERATE; +ModelDSO2150::ModelDSO2150() + : DSOModel(ID, 0x04b5, 0x2150, 0x04b4, 0x2150, "dso2150x86", "DSO-2150", new Dso::ModelSpec(2)) { + specification->cmdSetRecordLength = BulkCode::SETTRIGGERANDSAMPLERATE; + specification->cmdSetChannels = BulkCode::SETTRIGGERANDSAMPLERATE; + specification->cmdSetSamplerate = BulkCode::SETTRIGGERANDSAMPLERATE; + specification->cmdSetTrigger = BulkCode::SETTRIGGERANDSAMPLERATE; + specification->cmdSetPretrigger = BulkCode::SETTRIGGERANDSAMPLERATE; - specification.samplerate.single.base = 50e6; - specification.samplerate.single.max = 75e6; - specification.samplerate.single.maxDownsampler = 131072; - specification.samplerate.single.recordLengths = {UINT_MAX, 10240, 32768}; - specification.samplerate.multi.base = 100e6; - specification.samplerate.multi.max = 150e6; - specification.samplerate.multi.maxDownsampler = 131072; - specification.samplerate.multi.recordLengths = {UINT_MAX, 20480, 65536}; - specification.bufferDividers = { 1000 , 1 , 1 }; - specification.voltageLimit[0] = { 255 , 255 , 255 , 255 , 255 , 255 , 255 , 255 , 255 }; - specification.voltageLimit[1] = { 255 , 255 , 255 , 255 , 255 , 255 , 255 , 255 , 255 }; - specification.gain = { {0,0.08} , {1,0.16} , {2,0.40} , {0,0.80} , - {1,1.60} , {2,4.00} , {0,8.00} , {1,16.00} , {2,40.00} }; - specification.sampleSize = 8; - specification.specialTriggerChannels = {{"EXT", -2}, {"EXT/10", -3}}; + specification->normalSamplerate.base = 50e6; + specification->normalSamplerate.max = 75e6; + specification->normalSamplerate.maxDownsampler = 131072; + specification->normalSamplerate.recordLengths = {{UINT_MAX, 1000}, {10240, 1}, {32768, 1}}; + specification->fastrateSamplerate.base = 100e6; + specification->fastrateSamplerate.max = 150e6; + specification->fastrateSamplerate.maxDownsampler = 131072; + specification->fastrateSamplerate.recordLengths = {{UINT_MAX, 1000}, {20480, 1}, {65536, 1}}; + specification->calibration[0] = {{0x0000, 0xffff, 255}, {0x0000, 0xffff, 255}, {0x0000, 0xffff, 255}, + {0x0000, 0xffff, 255}, {0x0000, 0xffff, 255}, {0x0000, 0xffff, 255}, + {0x0000, 0xffff, 255}, {0x0000, 0xffff, 255}, {0x0000, 0xffff, 255}}; + specification->calibration[1] = {{0x0000, 0xffff, 255}, {0x0000, 0xffff, 255}, {0x0000, 0xffff, 255}, + {0x0000, 0xffff, 255}, {0x0000, 0xffff, 255}, {0x0000, 0xffff, 255}, + {0x0000, 0xffff, 255}, {0x0000, 0xffff, 255}, {0x0000, 0xffff, 255}}; + specification->gain = {{0, 0.08}, {1, 0.16}, {2, 0.40}, {0, 0.80}, {1, 1.60}, + {2, 4.00}, {0, 8.00}, {1, 16.00}, {2, 40.00}}; + specification->sampleSize = 8; + specification->specialTriggerChannels = {{"EXT", -2}, {"EXT/10", -3}}; } -void ModelDSO2150::applyRequirements(HantekDsoControl *dsoControl) const { - dsoControl->addCommand(new BulkForceTrigger(), false); - dsoControl->addCommand(new BulkCaptureStart(), false); - dsoControl->addCommand(new BulkTriggerEnabled(), false); - dsoControl->addCommand(new BulkGetData(), false); - dsoControl->addCommand(new BulkGetCaptureState(), false); - dsoControl->addCommand(new BulkSetGain(), false); +void ModelDSO2150::applyRequirements(DsoCommandQueue *commandQueue) const { + commandQueue->addCommand(new BulkForceTrigger(), false); + commandQueue->addCommand(new BulkCaptureStart(), false); + commandQueue->addCommand(new BulkTriggerEnabled(), false); + commandQueue->addCommand(new BulkGetData(), false); + commandQueue->addCommand(new BulkGetCaptureState(), false); + commandQueue->addCommand(new BulkSetGain(), false); - dsoControl->addCommand(new BulkSetTriggerAndSamplerate(), false); - dsoControl->addCommand(new ControlSetOffset(), false); - dsoControl->addCommand(new ControlSetRelays(), false); + commandQueue->addCommand(new BulkSetTriggerAndSamplerate(), false); + commandQueue->addCommand(new ControlSetOffset(), false); + commandQueue->addCommand(new ControlSetRelays(), false); } diff --git a/openhantek/src/hantekdso/models/modelDSO2150.h b/openhantek/src/hantekdso/models/modelDSO2150.h index d4ff4c8c..a9b086da 100644 --- a/openhantek/src/hantekdso/models/modelDSO2150.h +++ b/openhantek/src/hantekdso/models/modelDSO2150.h @@ -2,11 +2,10 @@ #include "dsomodel.h" -class HantekDsoControl; -using namespace Hantek; +class DsoControl; struct ModelDSO2150 : public DSOModel { static const int ID = 0x2150; ModelDSO2150(); - virtual void applyRequirements(HantekDsoControl* dsoControl) const override; + virtual void applyRequirements(DsoCommandQueue* dsoControl) const override; }; diff --git a/openhantek/src/hantekdso/models/modelDSO2250.cpp b/openhantek/src/hantekdso/models/modelDSO2250.cpp index e562665c..2e79a991 100644 --- a/openhantek/src/hantekdso/models/modelDSO2250.cpp +++ b/openhantek/src/hantekdso/models/modelDSO2250.cpp @@ -1,38 +1,40 @@ #include "modelDSO2250.h" -#include "hantekprotocol/bulkStructs.h" -#include "hantekprotocol/controlStructs.h" -#include "hantekdsocontrol.h" +#include "dsocommandqueue.h" +#include "modelspecification.h" using namespace Hantek; static ModelDSO2250 modelInstance; -ModelDSO2250::ModelDSO2250() : DSOModel(ID, 0x04b5, 0x2250, 0x04b4, 0x2250, "dso2250x86", "DSO-2250", - Dso::ControlSpecification(2)) { - specification.cmdSetRecordLength = BulkCode::DSETBUFFER; - specification.cmdSetChannels = BulkCode::BSETCHANNELS; - specification.cmdSetSamplerate = BulkCode::ESETTRIGGERORSAMPLERATE; - specification.cmdSetTrigger = BulkCode::CSETTRIGGERORSAMPLERATE; - specification.cmdSetPretrigger = BulkCode::FSETBUFFER; +ModelDSO2250::ModelDSO2250() + : DSOModel(ID, 0x04b5, 0x2250, 0x04b4, 0x2250, "dso2250x86", "DSO-2250", new Dso::ModelSpec(2)) { + specification->cmdSetRecordLength = BulkCode::DSETBUFFER; + specification->cmdSetChannels = BulkCode::BSETCHANNELS; + specification->cmdSetSamplerate = BulkCode::ESETTRIGGERORSAMPLERATE; + specification->cmdSetTrigger = BulkCode::CSETTRIGGERORSAMPLERATE; + specification->cmdSetPretrigger = BulkCode::FSETBUFFER; - specification.samplerate.single.base = 100e6; - specification.samplerate.single.max = 100e6; - specification.samplerate.single.maxDownsampler = 65536; - specification.samplerate.single.recordLengths = {UINT_MAX, 10240, 524288}; - specification.samplerate.multi.base = 200e6; - specification.samplerate.multi.max = 250e6; - specification.samplerate.multi.maxDownsampler = 65536; - specification.samplerate.multi.recordLengths = {UINT_MAX, 20480, 1048576}; - specification.bufferDividers = { 1000 , 1 , 1 }; - specification.voltageLimit[0] = { 255 , 255 , 255 , 255 , 255 , 255 , 255 , 255 , 255 }; - specification.voltageLimit[1] = { 255 , 255 , 255 , 255 , 255 , 255 , 255 , 255 , 255 }; - specification.gain = { {0,0.08} , {2,0.16} , {3,0.40} , {0,0.80} , - {2,1.60} , {3,4.00} , {0,8.00} , {2,16.00} , {3,40.00} }; - specification.sampleSize = 8; - specification.specialTriggerChannels = {{"EXT", -2}}; + specification->normalSamplerate.base = 100e6; + specification->normalSamplerate.max = 100e6; + specification->normalSamplerate.maxDownsampler = 65536; + specification->normalSamplerate.recordLengths = {{UINT_MAX, 1000}, {10240, 1}, {524288, 1}}; + specification->fastrateSamplerate.base = 200e6; + specification->fastrateSamplerate.max = 250e6; + specification->fastrateSamplerate.maxDownsampler = 65536; + specification->fastrateSamplerate.recordLengths = {{UINT_MAX, 1000}, {20480, 1}, {1048576, 1}}; + specification->calibration[0] = {{0x0000, 0xffff, 255}, {0x0000, 0xffff, 255}, {0x0000, 0xffff, 255}, + {0x0000, 0xffff, 255}, {0x0000, 0xffff, 255}, {0x0000, 0xffff, 255}, + {0x0000, 0xffff, 255}, {0x0000, 0xffff, 255}, {0x0000, 0xffff, 255}}; + specification->calibration[1] = {{0x0000, 0xffff, 255}, {0x0000, 0xffff, 255}, {0x0000, 0xffff, 255}, + {0x0000, 0xffff, 255}, {0x0000, 0xffff, 255}, {0x0000, 0xffff, 255}, + {0x0000, 0xffff, 255}, {0x0000, 0xffff, 255}, {0x0000, 0xffff, 255}}; + specification->gain = {{0, 0.08}, {2, 0.16}, {3, 0.40}, {0, 0.80}, {2, 1.60}, + {3, 4.00}, {0, 8.00}, {2, 16.00}, {3, 40.00}}; + specification->sampleSize = 8; + specification->specialTriggerChannels = {{"EXT", -2}}; } -void ModelDSO2250::applyRequirements(HantekDsoControl *dsoControl) const { +void ModelDSO2250::applyRequirements(DsoCommandQueue *dsoControl) const { dsoControl->addCommand(new BulkForceTrigger(), false); dsoControl->addCommand(new BulkCaptureStart(), false); dsoControl->addCommand(new BulkTriggerEnabled(), false); diff --git a/openhantek/src/hantekdso/models/modelDSO2250.h b/openhantek/src/hantekdso/models/modelDSO2250.h index aa81b294..7762fba6 100644 --- a/openhantek/src/hantekdso/models/modelDSO2250.h +++ b/openhantek/src/hantekdso/models/modelDSO2250.h @@ -2,11 +2,10 @@ #include "dsomodel.h" -class HantekDsoControl; -using namespace Hantek; +class DsoControl; struct ModelDSO2250 : public DSOModel { static const int ID = 0x2250; ModelDSO2250(); - void applyRequirements(HantekDsoControl* dsoControl) const override; + void applyRequirements(DsoCommandQueue* dsoControl) const override; }; diff --git a/openhantek/src/hantekdso/models/modelDSO5200.cpp b/openhantek/src/hantekdso/models/modelDSO5200.cpp index 9bdef90b..337b75ad 100644 --- a/openhantek/src/hantekdso/models/modelDSO5200.cpp +++ b/openhantek/src/hantekdso/models/modelDSO5200.cpp @@ -1,39 +1,40 @@ #include "modelDSO5200.h" -#include "hantekprotocol/bulkStructs.h" -#include "hantekprotocol/controlStructs.h" -#include "hantekdsocontrol.h" +#include "dsocommandqueue.h" +#include "modelspecification.h" using namespace Hantek; static ModelDSO5200 modelInstance; static ModelDSO5200A modelInstance2; -static void initSpecifications(Dso::ControlSpecification& specification) { - specification.cmdSetRecordLength = BulkCode::DSETBUFFER; - specification.cmdSetChannels = BulkCode::ESETTRIGGERORSAMPLERATE; - specification.cmdSetSamplerate = BulkCode::CSETTRIGGERORSAMPLERATE; - specification.cmdSetTrigger = BulkCode::ESETTRIGGERORSAMPLERATE; - specification.cmdSetPretrigger = BulkCode::ESETTRIGGERORSAMPLERATE; +static void initSpecifications(Dso::ModelSpec *specification) { + specification->cmdSetRecordLength = BulkCode::DSETBUFFER; + specification->cmdSetChannels = BulkCode::ESETTRIGGERORSAMPLERATE; + specification->cmdSetSamplerate = BulkCode::CSETTRIGGERORSAMPLERATE; + specification->cmdSetTrigger = BulkCode::ESETTRIGGERORSAMPLERATE; + specification->cmdSetPretrigger = BulkCode::ESETTRIGGERORSAMPLERATE; - specification.samplerate.single.base = 100e6; - specification.samplerate.single.max = 125e6; - specification.samplerate.single.maxDownsampler = 131072; - specification.samplerate.single.recordLengths = {UINT_MAX, 10240, 14336}; - specification.samplerate.multi.base = 200e6; - specification.samplerate.multi.max = 250e6; - specification.samplerate.multi.maxDownsampler = 131072; - specification.samplerate.multi.recordLengths = {UINT_MAX, 20480, 28672}; - specification.bufferDividers = { 1000 , 1 , 1 }; - /// \todo Use calibration data to get the DSO-5200(A) sample ranges - specification.voltageLimit[0] = { 368 , 454 , 908 , 368 , 454 , 908 , 368 , 454 , 908 }; - specification.voltageLimit[1] = { 368 , 454 , 908 , 368 , 454 , 908 , 368 , 454 , 908 }; - specification.gain = { {1,0.16} , {0,0.40} , {0,0.80} , {1,1.60} , - {0,4.00} , {0,8.00} , {1,16.0} , {0,40.0} , {0,80.0} }; - specification.sampleSize = 10; - specification.specialTriggerChannels = {{"EXT", -2}, {"EXT/10", -3}}; // 3, 4 + specification->normalSamplerate.base = 100e6; + specification->normalSamplerate.max = 125e6; + specification->normalSamplerate.maxDownsampler = 131072; + specification->normalSamplerate.recordLengths = {{UINT_MAX, 1000}, {10240, 1}, {14336, 1}}; + specification->fastrateSamplerate.base = 200e6; + specification->fastrateSamplerate.max = 250e6; + specification->fastrateSamplerate.maxDownsampler = 131072; + specification->fastrateSamplerate.recordLengths = {{UINT_MAX, 1000}, {20480, 1}, {28672, 1}}; + specification->calibration[0] = {{0x0000, 0xffff, 368}, {0x0000, 0xffff, 454}, {0x0000, 0xffff, 908}, + {0x0000, 0xffff, 368}, {0x0000, 0xffff, 454}, {0x0000, 0xffff, 908}, + {0x0000, 0xffff, 368}, {0x0000, 0xffff, 454}, {0x0000, 0xffff, 908}}; + specification->calibration[1] = {{0x0000, 0xffff, 368}, {0x0000, 0xffff, 454}, {0x0000, 0xffff, 908}, + {0x0000, 0xffff, 368}, {0x0000, 0xffff, 454}, {0x0000, 0xffff, 908}, + {0x0000, 0xffff, 368}, {0x0000, 0xffff, 454}, {0x0000, 0xffff, 908}}; + specification->gain = {{1, 0.16}, {0, 0.40}, {0, 0.80}, {1, 1.60}, {0, 4.00}, + {0, 8.00}, {1, 16.0}, {0, 40.0}, {0, 80.0}}; + specification->sampleSize = 10; + specification->specialTriggerChannels = {{"EXT", -2}, {"EXT/10", -3}}; // 3, 4 } -static void _applyRequirements(HantekDsoControl *dsoControl) { +static void _applyRequirements(DsoCommandQueue *dsoControl) { dsoControl->addCommand(new BulkForceTrigger(), false); dsoControl->addCommand(new BulkCaptureStart(), false); dsoControl->addCommand(new BulkTriggerEnabled(), false); @@ -49,20 +50,16 @@ static void _applyRequirements(HantekDsoControl *dsoControl) { dsoControl->addCommand(new ControlSetRelays(), false); } -ModelDSO5200::ModelDSO5200() : DSOModel(ID, 0x04b5, 0x5200, 0x04b4, 0x5200, "dso5200x86", "DSO-5200", - Dso::ControlSpecification(2)) { +ModelDSO5200::ModelDSO5200() + : DSOModel(ID, 0x04b5, 0x5200, 0x04b4, 0x5200, "dso5200x86", "DSO-5200", new Dso::ModelSpec(2)) { initSpecifications(specification); } -void ModelDSO5200::applyRequirements(HantekDsoControl *dsoControl) const { - _applyRequirements(dsoControl); -} +void ModelDSO5200::applyRequirements(DsoCommandQueue *dsoControl) const { _applyRequirements(dsoControl); } -ModelDSO5200A::ModelDSO5200A() : DSOModel(ID, 0x04b5, 0x520a, 0x04b4, 0x520a, "dso5200ax86", "DSO-5200A", - Dso::ControlSpecification(2)) { +ModelDSO5200A::ModelDSO5200A() + : DSOModel(ID, 0x04b5, 0x520a, 0x04b4, 0x520a, "dso5200ax86", "DSO-5200A", new Dso::ModelSpec(2)) { initSpecifications(specification); } -void ModelDSO5200A::applyRequirements(HantekDsoControl *dsoControl) const { - _applyRequirements(dsoControl); -} +void ModelDSO5200A::applyRequirements(DsoCommandQueue *dsoControl) const { _applyRequirements(dsoControl); } diff --git a/openhantek/src/hantekdso/models/modelDSO5200.h b/openhantek/src/hantekdso/models/modelDSO5200.h index e5c81cc5..0bb6b53f 100644 --- a/openhantek/src/hantekdso/models/modelDSO5200.h +++ b/openhantek/src/hantekdso/models/modelDSO5200.h @@ -2,17 +2,16 @@ #include "dsomodel.h" -class HantekDsoControl; -using namespace Hantek; +class DsoControl; struct ModelDSO5200 : public DSOModel { static const int ID = 0x5200; ModelDSO5200(); - void applyRequirements(HantekDsoControl* dsoControl) const override; + void applyRequirements(DsoCommandQueue *dsoControl) const override; }; struct ModelDSO5200A : public DSOModel { static const int ID = 0x5200; ModelDSO5200A(); - void applyRequirements(HantekDsoControl* dsoControl) const override; + void applyRequirements(DsoCommandQueue* dsoControl) const override; }; diff --git a/openhantek/src/hantekdso/models/modelDSO6022.cpp b/openhantek/src/hantekdso/models/modelDSO6022.cpp index 5ab11e1a..5ea01585 100644 --- a/openhantek/src/hantekdso/models/modelDSO6022.cpp +++ b/openhantek/src/hantekdso/models/modelDSO6022.cpp @@ -1,67 +1,66 @@ #include "modelDSO6022.h" +#include "dsocommandqueue.h" +#include "modelspecification.h" #include "usb/usbdevice.h" -#include "hantekprotocol/controlStructs.h" -#include "hantekdsocontrol.h" using namespace Hantek; static ModelDSO6022BE modelInstance; static ModelDSO6022BL modelInstance2; -static void initSpecifications(Dso::ControlSpecification& specification) { +static void initSpecifications(Dso::ModelSpec *specification) { // 6022xx do not support any bulk commands - specification.useControlNoBulk = true; - specification.isSoftwareTriggerDevice = true; - specification.isFixedSamplerateDevice = true; - specification.supportsCaptureState = false; - specification.supportsOffset = false; - specification.supportsCouplingRelays = false; + specification->useControlNoBulk = true; + specification->isSoftwareTriggerDevice = true; + specification->isFixedSamplerateDevice = true; + specification->supportsCaptureState = false; + specification->supportsOffset = false; + specification->supportsCouplingRelays = false; + specification->supportsFastRate = false; - specification.samplerate.single.base = 1e6; - specification.samplerate.single.max = 48e6; - specification.samplerate.single.maxDownsampler = 10; - specification.samplerate.single.recordLengths = {UINT_MAX, 10240}; - specification.samplerate.multi.base = 1e6; - specification.samplerate.multi.max = 48e6; - specification.samplerate.multi.maxDownsampler = 10; - specification.samplerate.multi.recordLengths = {UINT_MAX, 20480}; - specification.bufferDividers = { 1000 , 1 , 1 }; + specification->normalSamplerate.base = 1e6; + specification->normalSamplerate.max = 48e6; + specification->normalSamplerate.maxDownsampler = 10; + specification->normalSamplerate.recordLengths = {{10240, 1}}; + specification->fastrateSamplerate.base = 1e6; + specification->fastrateSamplerate.max = 48e6; + specification->fastrateSamplerate.maxDownsampler = 10; + specification->fastrateSamplerate.recordLengths = {{20480, 1}}; // This data was based on testing and depends on Divider. - specification.voltageLimit[0] = { 25 , 51 , 103 , 206 , 412 , 196 , 392 , 784 , 1000 }; - specification.voltageLimit[1] = { 25 , 51 , 103 , 206 , 412 , 196 , 392 , 784 , 1000 }; - // Divider. Tested and calculated results are different! - specification.gain = { {10,0.08} , {10,0.16} , {10,0.40} , {10,0.80} , - {10,1.60} , {2,4.00} , {2,8.00} , {2,16.00} , {1,40.00} }; - specification.fixedSampleRates = { {10,1e5} , {20,2e5} , {50,5e5} , {1,1e6} , {2,2e6} , {4,4e6} , {8,8e6} , - {16,16e6} , {24,24e6} , {48,48e6} }; - specification.sampleSize = 8; + specification->calibration[0] = {{0x0000, 0xfd, 10}, {0x0000, 0xfd, 20}, {0x0000, 0xfd, 49}, + {0x0000, 0xfd, 99}, {0x0000, 0xfd, 198}, {0x0000, 0xfd, 400}, + {0x0000, 0xfd, 800}, {0x0000, 0xfd, 1600}}; + specification->calibration[1] = {{0x0000, 0xfd, 10}, {0x0000, 0xfd, 20}, {0x0000, 0xfd, 49}, + {0x0000, 0xfd, 99}, {0x0000, 0xfd, 198}, {0x0000, 0xfd, 400}, + {0x0000, 0xfd, 800}, {0x0000, 0xfd, 1600}}; + specification->gain = {{10, 0.08}, {10, 0.16}, {10, 0.40}, {10, 0.80}, + {10, 1.60}, {2, 4.00}, {2, 8.00}, {2, 16.00}}; + specification->fixedSampleRates = {{10, 1e5}, {20, 2e5}, {50, 5e5}, {1, 1e6}, {2, 2e6}, + {4, 4e6}, {8, 8e6}, {16, 16e6}, {24, 24e6}, {48, 48e6}}; + specification->sampleSize = 8; - specification.couplings = {Dso::Coupling::DC}; - specification.triggerModes = {Dso::TriggerMode::HARDWARE_SOFTWARE, Dso::TriggerMode::SINGLE}; - specification.fixedUSBinLength = 16384; + specification->couplings = {Dso::Coupling::DC}; + specification->triggerModes = {Dso::TriggerMode::HARDWARE_SOFTWARE, Dso::TriggerMode::SINGLE}; + specification->fixedUSBinLength = 16384; } -void applyRequirements_(HantekDsoControl *dsoControl) { - dsoControl->addCommand(new ControlAcquireHardData()); - dsoControl->addCommand(new ControlSetTimeDIV()); - dsoControl->addCommand(new ControlSetVoltDIV_CH2()); - dsoControl->addCommand(new ControlSetVoltDIV_CH1()); +void applyRequirements_(DsoCommandQueue *dsoControl) { + dsoControl->addCommand(new ControlAcquireHardData(), false); + dsoControl->addCommand(new ControlSetTimeDIV(), false); + dsoControl->addCommand(new ControlSetVoltDIV_CH2(), false); + dsoControl->addCommand(new ControlSetVoltDIV_CH1(), false); } -ModelDSO6022BE::ModelDSO6022BE() : DSOModel(ID, 0x04b5, 0x6022, 0x04b4, 0x6022, "dso6022be", "DSO-6022BE", - Dso::ControlSpecification(2)) { +ModelDSO6022BE::ModelDSO6022BE() + : DSOModel(ID, 0x04b5, 0x6022, 0x04b4, 0x6022, "dso6022be", "DSO-6022BE", new Dso::ModelSpec(2)) { initSpecifications(specification); } -void ModelDSO6022BE::applyRequirements(HantekDsoControl *dsoControl) const { - applyRequirements_(dsoControl); -} +void ModelDSO6022BE::applyRequirements(DsoCommandQueue *dsoControl) const { applyRequirements_(dsoControl); } -ModelDSO6022BL::ModelDSO6022BL() : DSOModel(ID, 0x04b5, 0x602a, 0x04b4, 0x602a, "dso6022bl", "DSO-6022BL", - Dso::ControlSpecification(2)) { +ModelDSO6022BL::ModelDSO6022BL() + : DSOModel(ID, 0x04b5, 0x602a, 0x04b4, 0x602a, "dso6022bl", "DSO-6022BL", new Dso::ModelSpec(2)) { initSpecifications(specification); } -void ModelDSO6022BL::applyRequirements(HantekDsoControl *dsoControl) const { - applyRequirements_(dsoControl); -} +void ModelDSO6022BL::applyRequirements(DsoCommandQueue *dsoControl) const { applyRequirements_(dsoControl); } diff --git a/openhantek/src/hantekdso/models/modelDSO6022.h b/openhantek/src/hantekdso/models/modelDSO6022.h index b83b850d..22a6c610 100644 --- a/openhantek/src/hantekdso/models/modelDSO6022.h +++ b/openhantek/src/hantekdso/models/modelDSO6022.h @@ -2,17 +2,16 @@ #include "dsomodel.h" -class HantekDsoControl; -using namespace Hantek; +class DsoControl; struct ModelDSO6022BE : public DSOModel { static const int ID = 0x6022; ModelDSO6022BE(); - virtual void applyRequirements(HantekDsoControl* dsoControl) const override; + virtual void applyRequirements(DsoCommandQueue *dsoControl) const override; }; struct ModelDSO6022BL : public DSOModel { static const int ID = 0x6022; ModelDSO6022BL(); - virtual void applyRequirements(HantekDsoControl* dsoControl) const override; + virtual void applyRequirements(DsoCommandQueue* dsoControl) const override; }; diff --git a/openhantek/src/hantekdso/modelspecification.cpp b/openhantek/src/hantekdso/modelspecification.cpp new file mode 100644 index 00000000..5431d8ec --- /dev/null +++ b/openhantek/src/hantekdso/modelspecification.cpp @@ -0,0 +1,5 @@ +// SPDX-License-Identifier: GPL-2.0+ + +#include "hantekdso/modelspecification.h" + +Dso::ModelSpec::ModelSpec(unsigned channels) : channels(channels) { calibration.resize(channels); } diff --git a/openhantek/src/hantekdso/modelspecification.h b/openhantek/src/hantekdso/modelspecification.h new file mode 100644 index 00000000..657c65fc --- /dev/null +++ b/openhantek/src/hantekdso/modelspecification.h @@ -0,0 +1,143 @@ +// SPDX-License-Identifier: GPL-2.0+ + +#pragma once + +#include "enums.h" +#include "hantekprotocol/codes.h" +#include "hantekprotocol/controlStructs.h" +#include "hantekprotocol/definitions.h" +#include "hantekprotocol/types.h" +#include + +#include +#include + +namespace Dso { +constexpr static unsigned ROLL_RECORDLEN = UINT_MAX; + +struct RecordLength { + unsigned recordLength; ///< Record length, ROLL_RECORDLEN means rolling + unsigned bufferDivider; ///< Samplerate dividers for record lengths + inline RecordLength(unsigned recordLength, unsigned bufferDivider) + : recordLength(recordLength), bufferDivider(bufferDivider) {} + RecordLength() = default; +}; + +/// \brief Stores the samplerate limits for calculations. +struct ControlSamplerateLimits { + double base; ///< The base for sample rate calculations + double max; ///< The maximum sample rate + unsigned maxDownsampler; ///< The maximum downsampling ratio + std::vector recordLengths; ///< Available record lengths + + inline double minSamplerate(RecordLengthID id) const { + return base / maxDownsampler / recordLengths[id].recordLength; + } + inline double samplerate(RecordLengthID id, unsigned downsampler) const { + return base / downsampler / recordLengths[id].recordLength; + } + inline double samplerate(RecordLengthID id, double recordTime) const { + return recordLengths[id].bufferDivider / recordTime; + } + + inline double maxSamplerate(RecordLengthID id) const { return max / recordLengths[id].bufferDivider; } + + inline unsigned computeDownsampler(RecordLengthID id, double samplerate) const { + return unsigned(base / recordLengths[id].bufferDivider / samplerate); + } +}; + +struct ControlSpecificationGainLevel { + /// The index of the selected gain on the hardware + unsigned char gainIdentificator; + /// Available voltage steps in V/screenheight + double gain; +}; + +struct FixedSampleRate { + unsigned char id; ///< hardware id + double samplerate; +}; + +struct SpecialTriggerChannel { + std::string name; + int hardwareID; +}; + +/// \brief Stores the specifications of the currently connected device. +struct ModelSpec { + ModelSpec(unsigned channels); + const ChannelID channels; + + // Interface + Hantek::BulkCode cmdSetChannels = Hantek::BulkCode::INVALID; ///< Command for setting used channels + Hantek::BulkCode cmdSetSamplerate = Hantek::BulkCode::INVALID; ///< Command for samplerate settings + Hantek::BulkCode cmdSetRecordLength = Hantek::BulkCode::INVALID; ///< Command for buffer settings + Hantek::BulkCode cmdSetTrigger = Hantek::BulkCode::INVALID; ///< Command for trigger settings + Hantek::BulkCode cmdSetPretrigger = Hantek::BulkCode::INVALID; ///< Command for pretrigger settings + Hantek::BulkCode cmdForceTrigger = Hantek::BulkCode::FORCETRIGGER; ///< Command for forcing a trigger event + Hantek::BulkCode cmdCaptureStart = Hantek::BulkCode::STARTSAMPLING; ///< Command for starting the sampling + Hantek::BulkCode cmdTriggerEnabled = Hantek::BulkCode::ENABLETRIGGER; ///< Command for enabling the trigger + Hantek::BulkCode cmdGetData = Hantek::BulkCode::GETDATA; ///< Command for retrieve sample data + Hantek::BulkCode cmdGetCaptureState = Hantek::BulkCode::GETCAPTURESTATE; ///< Command for retrieve the capture state + Hantek::BulkCode cmdSetGain = Hantek::BulkCode::SETGAIN; ///< Command for setting the gain + + // Actual resolved commands based on the above interface + const Hantek::ControlBeginCommand beginCommandControl; + + // Limits + + /// The limits for multi channel mode + ControlSamplerateLimits normalSamplerate = {50e6, 50e6, 0, std::vector()}; + /// The limits for single channel mode + ControlSamplerateLimits fastrateSamplerate = {100e6, 100e6, 0, std::vector()}; + + unsigned char sampleSize; ///< Number of bits per sample + + /// For devices that support only fixed sample rates (isFixedSamplerateDevice=true) + std::vector fixedSampleRates; + + // Calibration + + struct GainStepCalibration { + double offsetCorrection = 0; + unsigned short offsetStart = 0x0000; + unsigned short offsetEnd = 0xffff; + double voltageLimit = 255.0; + inline GainStepCalibration(double offsetCorrection, unsigned short offsetStart, unsigned short offsetEnd, + double voltageLimit) + : offsetCorrection(offsetCorrection), offsetStart(offsetStart), offsetEnd(offsetEnd), + voltageLimit(voltageLimit) {} + inline GainStepCalibration(unsigned short offsetStart, unsigned short offsetEnd, double voltageLimit) + : offsetStart(offsetStart), offsetEnd(offsetEnd), voltageLimit(voltageLimit) {} + }; + + /// The sample values at the top of the screen + typedef std::vector gainStepCalibration; + + std::vector calibration; // Per channel + + /// Gain levels + std::vector gain; // Usually size==HANTEK_GAIN_STEPS + + // Features + std::vector specialTriggerChannels; + std::vector couplings = {Dso::Coupling::DC, Dso::Coupling::AC}; + std::vector triggerModes = {TriggerMode::HARDWARE_SOFTWARE, TriggerMode::WAIT_FORCE, + TriggerMode::SINGLE}; + bool isFixedSamplerateDevice = false; + bool isSoftwareTriggerDevice = false; + bool useControlNoBulk = false; + bool supportsCaptureState = true; + bool supportsOffset = true; + bool supportsCouplingRelays = true; + bool supportsFastRate = true; + int fixedUSBinLength = 0; + double testSignalAmplitude = 1.0; ///< Test signal amplitude in V. Usually 1V. + + inline int indexOfTriggerMode(TriggerMode mode) const { + return int(std::find(triggerModes.begin(), triggerModes.end(), mode) - triggerModes.begin()); + } +}; +} +Q_DECLARE_METATYPE(Dso::RecordLength); diff --git a/openhantek/src/hantekdso/readme.md b/openhantek/src/hantekdso/readme.md index 1789507d..70bb303c 100644 --- a/openhantek/src/hantekdso/readme.md +++ b/openhantek/src/hantekdso/readme.md @@ -1,20 +1,37 @@ # Content -This directory contains the heart of OpenHantek, the `HantekDSOControl` class -and all model definitions. - -## HantekDSOControl -The `HantekDSOControl` class manages all device settings (gain, offsets, channels, etc) -and outputs `DSOSamples` via `getLastSamples()`. Observers are notified of a new set of -available samples via the signal `samplesAvailable()`. -Current device settings are stored in the `controlsettings` field and retriveable with the -corresponding getter `getDeviceSettings()`. - -`HantekDSOControl` may only contain state fields to realize the fetch samples / modify settings loop. - -## Model -A model needs a `ControlSpecification`, which -describes what specific Hantek protocol commands are to be used. All known -models are specified in the subdirectory `models`. +This directory contains the heart of OpenHantek, the `DSOControl` class and all model definitions. + +## DsoControl and DsoLoop + +This is the core logic class that ties together the USBDevice object, the protocol parts, the sample-fetch +loop (DsoLoop) and command queue (DsoCommandQueue). Device settings can only be done through this class, +but are stored separately in the DeviceSettings object. + +DsoLoop is responsible for fetching new samples from the Dso at the right time, converting it to a normalized +range [-1,1] of values and provide the result via `getLastSamples()`. Observers are notified of a new set of +available samples via the signal `samplesAvailable()`. The DsoSamples structure that is used for result retrieving +is allocated only once and will be overwritten in each loop step, it is therefore necessary to copy the result. +The structure provides a mutex to synchronize concurrent threads (usually the post processing thread and the +DsoControl thread). + +## DsoCommandQueue + +DsoControl inherits from DsoCommandQueue and uses the provided two command queues for usb-control and usb-bulk +commands. Commands like setGain() are not performed directly but in a batched way. +The first step of each fetch-sample loop is to send all pending/queued commands via `sendPendingCommands()`. + +DsoCommandQueue requires a model to "register" all necessary commands beforehand via `addCommand(cmd)`. The command +can be altered later via `modifyCommand(id)`. Because commands are not applied directly, only the last modification +before a new loop cycle is going to begin, will actually be send to the device. + +## DSO Model + +A model needs a `Dso::ModelSpec` (files: modelspecification.h/cpp), which +describes what specific Hantek protocol commands are to be used and what capabilities are supported. All known +models are specified in the subdirectory `models`. A new model inherits from `DSOModel` and implements +`applyRequirements(DsoCommandQueue)`. Within this method all necessary commands are registered via +DsoCommandQueue::addCommand. In the constructor of the model, the field `specification` is used to describe the models +capabilites. # Namespace Relevant classes in here are in the `DSO` namespace. diff --git a/openhantek/src/hantekdso/states.h b/openhantek/src/hantekdso/states.h index a60af63a..9fb816a0 100644 --- a/openhantek/src/hantekdso/states.h +++ b/openhantek/src/hantekdso/states.h @@ -4,8 +4,6 @@ namespace Hantek { -////////////////////////////////////////////////////////////////////////////// -/// \enum RollState /// \brief The states of the roll cycle (Since capture state isn't valid). enum class RollState : int { STARTSAMPLING = 0, ///< Start sampling @@ -16,8 +14,6 @@ enum class RollState : int { _COUNT // Used for mod operator }; -////////////////////////////////////////////////////////////////////////////// -/// \enum CaptureState hantek/types.h /// \brief The different capture states which the oscilloscope returns. enum CaptureState { CAPTURE_WAITING = 0, ///< The scope is waiting for a trigger event @@ -27,5 +23,4 @@ enum CaptureState { CAPTURE_READY5200 = 7, ///< Sampling data is available (DSO-5200/DSO-5200A) CAPTURE_ERROR = 1000 }; - } diff --git a/openhantek/src/hantekprotocol/bulkStructs.cpp b/openhantek/src/hantekprotocol/bulkStructs.cpp index 62603948..33fa0793 100644 --- a/openhantek/src/hantekprotocol/bulkStructs.cpp +++ b/openhantek/src/hantekprotocol/bulkStructs.cpp @@ -1,6 +1,6 @@ // SPDX-License-Identifier: GPL-2.0+ -#include "bulkcode.h" +#include "codes.h" #include "bulkStructs.h" #include "definitions.h" diff --git a/openhantek/src/hantekprotocol/bulkcode.h b/openhantek/src/hantekprotocol/codes.h similarity index 77% rename from openhantek/src/hantekprotocol/bulkcode.h rename to openhantek/src/hantekprotocol/codes.h index 8224fba0..00d48951 100644 --- a/openhantek/src/hantekprotocol/bulkcode.h +++ b/openhantek/src/hantekprotocol/codes.h @@ -1,8 +1,145 @@ #pragma once +#include #include namespace Hantek { +Q_NAMESPACE + +/// \brief All supported control commands. + +/// CONTROL_VALUE [MODEL_DSO2090, MODEL_DSO2150, MODEL_DSO2250, MODEL_DSO5200, MODEL_DSO5200A, MODEL_DSO6022] +///

+/// The 0xa2 control read/write command gives access to a ControlValue. +///

+/// +/// CONTROL_GETSPEED [MODEL_DSO2090, MODEL_DSO2150, MODEL_DSO2250, MODEL_DSO5200, MODEL_DSO5200A, +/// MODEL_DSO6022] +///

+/// The 0xb2 control read command gets the speed level of the USB +/// connection: +/// +/// +/// +/// +/// +/// +/// +/// +/// +/// +/// +/// +/// +///
ConnectionSpeed0x000x000x000x000x000x000x000x000x00
+///

+/// +/// CONTROL_BEGINCOMMAND [MODEL_DSO2090, MODEL_DSO2150, MODEL_DSO2250, MODEL_DSO5200, MODEL_DSO5200A] +///

+/// The 0xb3 control write command is sent before any bulk command: +/// +/// +/// +/// +/// +/// +/// +/// +/// +/// +/// +/// +/// +///
0x0fBulkIndexBulkIndexBulkIndex0x000x000x000x000x000x00
+///

+/// +/// CONTROL_SETOFFSET [MODEL_DSO2090, MODEL_DSO2150, MODEL_DSO2250, MODEL_DSO5200, MODEL_DSO5200A] +///

+/// The 0xb4 control write command sets the channel offsets: +/// +/// +/// +/// +/// +/// +/// +/// +/// +///
Ch1Offset[1]Ch1Offset[0]Ch2Offset[1]Ch2Offset[0]TriggerOffset[1]TriggerOffset[0]
+/// +/// +/// +/// +/// +/// +/// +/// +/// +/// +/// +/// +/// +/// +///
0x000x000x000x000x000x000x000x000x000x000x00
+///

+/// +/// CONTROL_SETRELAYS [MODEL_DSO2090, MODEL_DSO2150, MODEL_DSO2250, MODEL_DSO5200, MODEL_DSO5200A] +///

+/// The 0xb5 control write command sets the internal relays: +/// +/// +/// +/// +/// +/// +/// +///
0x000x04 ^ (Ch1Gain < 1 V)0x08 ^ (Ch1Gain < 100 mV)0x02 ^ (Ch1Coupling == DC)
+/// +/// +/// +/// +/// +/// +/// +///
0x20 ^ (Ch2Gain < 1 V)0x40 ^ (Ch2Gain < 100 mV)0x10 ^ (Ch2Coupling == DC)0x01 ^ (Trigger == EXT)
+/// +/// +/// +/// +/// +/// +/// +/// +/// +/// +/// +/// +///
0x000x000x000x000x000x000x000x000x00
+///

+///

+/// The limits are <= instead of < for the 10 bit models, since those +/// support voltages up to 10 V. +///

+/// +/// CONTROL_SETVOLTDIV_CH1 CH1 voltage div setting (6022BE/BL) +/// +/// CONTROL_SETVOLTDIV_CH2 CH2 voltage div setting (6022BE/BL) +/// +/// CONTROL_SETTIMEDIV Time divisor setting (6022BE/BL) +/// +/// CONTROL_ACQUIIRE_HARD_DATA Request sample data (6022BE/BL) +enum class ControlCode : uint8_t { + VALUE = 0xa2, + GETSPEED = 0xb2, + BEGINCOMMAND = 0xb3, + SETOFFSET = 0xb4, + SETRELAYS = 0xb5, + SETVOLTDIV_CH1 = 0xe0, + SETVOLTDIV_CH2 = 0xe1, + SETTIMEDIV = 0xe2, + ACQUIRE_DATA = 0xe3 +}; +Q_ENUM_NS(ControlCode) /// \brief All supported bulk commands. /// Indicies given in square brackets specify byte numbers in little endian @@ -458,8 +595,8 @@ enum class BulkCode : uint8_t { ESETTRIGGERORSAMPLERATE = 0x0e, FSETBUFFER = 0x0f, - GETCAPTURESTATE_RESPONSE=0xfe, - INVALID=0xff + GETCAPTURESTATE_RESPONSE = 0xfe, + INVALID = 0xff }; - +Q_ENUM_NS(BulkCode) } diff --git a/openhantek/src/hantekprotocol/controlStructs.cpp b/openhantek/src/hantekprotocol/controlStructs.cpp index 23164d18..bd9e0b4d 100644 --- a/openhantek/src/hantekprotocol/controlStructs.cpp +++ b/openhantek/src/hantekprotocol/controlStructs.cpp @@ -9,32 +9,32 @@ namespace Hantek { ControlBeginCommand::ControlBeginCommand(BulkIndex index) - : ControlCommand(Hantek::ControlCode::CONTROL_BEGINCOMMAND, 10) { + : ControlCommand(Hantek::ControlCode::BEGINCOMMAND, 10) { data()[0] = 0x0f; data()[1] = (uint8_t)index; } -ControlGetSpeed::ControlGetSpeed() : ControlCommand(Hantek::ControlCode::CONTROL_GETSPEED, 10) {} +ControlGetSpeed::ControlGetSpeed() : ControlCommand(Hantek::ControlCode::GETSPEED, 10) {} ConnectionSpeed ControlGetSpeed::getSpeed() { return (ConnectionSpeed)data()[0]; } -ControlSetOffset::ControlSetOffset() : ControlCommand(ControlCode::CONTROL_SETOFFSET, 17) {} +ControlSetOffset::ControlSetOffset() : ControlCommand(ControlCode::SETOFFSET, 17) {} ControlSetOffset::ControlSetOffset(uint16_t channel1, uint16_t channel2, uint16_t trigger) - : ControlCommand(ControlCode::CONTROL_SETOFFSET, 17) { - this->setChannel(0, channel1); - this->setChannel(1, channel2); - this->setTrigger(trigger); + : ControlCommand(ControlCode::SETOFFSET, 17) { + this->setOffset(0, channel1); + this->setOffset(1, channel2); + this->setTriggerLevel(trigger); } -uint16_t ControlSetOffset::getChannel(ChannelID channel) { +uint16_t ControlSetOffset::offset(ChannelID channel) { if (channel == 0) return ((data()[0] & 0x0f) << 8) | data()[1]; else return ((data()[2] & 0x0f) << 8) | data()[3]; } -void ControlSetOffset::setChannel(ChannelID channel, uint16_t offset) { +void ControlSetOffset::setOffset(ChannelID channel, uint16_t offset) { if (channel == 0) { data()[0] = (uint8_t)(offset >> 8); data()[1] = (uint8_t)offset; @@ -44,16 +44,16 @@ void ControlSetOffset::setChannel(ChannelID channel, uint16_t offset) { } } -uint16_t ControlSetOffset::getTrigger() { return ((data()[4] & 0x0f) << 8) | data()[5]; } +uint16_t ControlSetOffset::triggerLevel() { return ((data()[4] & 0x0f) << 8) | data()[5]; } -void ControlSetOffset::setTrigger(uint16_t level) { +void ControlSetOffset::setTriggerLevel(uint16_t level) { data()[4] = (uint8_t)(level >> 8); data()[5] = (uint8_t)level; } ControlSetRelays::ControlSetRelays(bool ch1Below1V, bool ch1Below100mV, bool ch1CouplingDC, bool ch2Below1V, bool ch2Below100mV, bool ch2CouplingDC, bool triggerExt) - : ControlCommand(ControlCode::CONTROL_SETRELAYS, 17) { + : ControlCommand(ControlCode::SETRELAYS, 17) { this->setBelow1V(0, ch1Below1V); this->setBelow100mV(0, ch1Below100mV); this->setCoupling(0, ch1CouplingDC); @@ -109,28 +109,28 @@ bool ControlSetRelays::getTrigger() { return (data()[7] & 0x01) == 0x00; } void ControlSetRelays::setTrigger(bool ext) { data()[7] = ext ? 0xfe : 0x01; } -ControlSetVoltDIV_CH1::ControlSetVoltDIV_CH1() : ControlCommand(ControlCode::CONTROL_SETVOLTDIV_CH1, 1) { +ControlSetVoltDIV_CH1::ControlSetVoltDIV_CH1() : ControlCommand(ControlCode::SETVOLTDIV_CH1, 1) { this->setDiv(5); } void ControlSetVoltDIV_CH1::setDiv(uint8_t val) { data()[0] = val; } -ControlSetVoltDIV_CH2::ControlSetVoltDIV_CH2() : ControlCommand(ControlCode::CONTROL_SETVOLTDIV_CH2, 1) { +ControlSetVoltDIV_CH2::ControlSetVoltDIV_CH2() : ControlCommand(ControlCode::SETVOLTDIV_CH2, 1) { this->setDiv(5); } void ControlSetVoltDIV_CH2::setDiv(uint8_t val) { data()[0] = val; } -ControlSetTimeDIV::ControlSetTimeDIV() : ControlCommand(ControlCode::CONTROL_SETTIMEDIV, 1) { this->setDiv(1); } +ControlSetTimeDIV::ControlSetTimeDIV() : ControlCommand(ControlCode::SETTIMEDIV, 1) { this->setDiv(1); } void ControlSetTimeDIV::setDiv(uint8_t val) { data()[0] = val; } -ControlAcquireHardData::ControlAcquireHardData() : ControlCommand(ControlCode::CONTROL_ACQUIIRE_HARD_DATA, 1) { +ControlAcquireHardData::ControlAcquireHardData() : ControlCommand(ControlCode::ACQUIRE_DATA, 1) { data()[0] = 0x01; } ControlGetLimits::ControlGetLimits(size_t channels) - : ControlCommand(ControlCode::CONTROL_VALUE, 1), offsetLimit(new OffsetsPerGainStep[channels]) { + : ControlCommand(ControlCode::VALUE, 1), offsetLimit(new OffsetsPerGainStep[channels]) { value = (uint8_t)ControlValue::VALUE_OFFSETLIMITS; data()[0] = 0x01; } diff --git a/openhantek/src/hantekprotocol/controlStructs.h b/openhantek/src/hantekprotocol/controlStructs.h index fe12d0f9..87f59905 100644 --- a/openhantek/src/hantekprotocol/controlStructs.h +++ b/openhantek/src/hantekprotocol/controlStructs.h @@ -1,6 +1,6 @@ #pragma once -#include "controlcode.h" +#include "codes.h" #include "controlcommand.h" #include "types.h" #include "usb/usbdevicedefinitions.h" @@ -9,7 +9,6 @@ #include namespace Hantek { -struct OffsetsPerGainStep; /// \enum BulkIndex /// \brief Can be set by CONTROL_BEGINCOMMAND, maybe it allows multiple commands @@ -51,17 +50,17 @@ struct ControlSetOffset : public ControlCommand { /// \brief Get the offset for the given channel. /// \param channel The channel whose offset should be returned. /// \return The channel offset value. - uint16_t getChannel(ChannelID channel); + uint16_t offset(ChannelID channel); /// \brief Set the offset for the given channel. /// \param channel The channel that should be set. /// \param offset The new channel offset value. - void setChannel(ChannelID channel, uint16_t offset); + void setOffset(ChannelID channel, uint16_t offset); /// \brief Get the trigger level. /// \return The trigger level value. - uint16_t getTrigger(); + uint16_t triggerLevel(); /// \brief Set the trigger level. /// \param level The new trigger level value. - void setTrigger(uint16_t level); + void setTriggerLevel(uint16_t level); }; struct ControlSetRelays : public ControlCommand { @@ -129,8 +128,18 @@ struct ControlAcquireHardData : public ControlCommand { }; struct ControlGetLimits : public ControlCommand { + constexpr static unsigned HANTEK_GAIN_STEPS = 9; +#pragma pack(push, 1) + struct Offset { + unsigned short start; + unsigned short end; + }; + struct OffsetsPerGainStep { + Offset step[HANTEK_GAIN_STEPS]; + }; +#pragma pack(pop) + std::unique_ptr offsetLimit; ControlGetLimits(size_t channels); - inline uint8_t *offsetLimitData() { return (uint8_t *)offsetLimit.get(); } }; } diff --git a/openhantek/src/hantekprotocol/controlcode.h b/openhantek/src/hantekprotocol/controlcode.h deleted file mode 100644 index 3653f9cc..00000000 --- a/openhantek/src/hantekprotocol/controlcode.h +++ /dev/null @@ -1,140 +0,0 @@ -#pragma once - -#include - -namespace Hantek { - -/// \brief All supported control commands. - -/// CONTROL_VALUE [MODEL_DSO2090, MODEL_DSO2150, MODEL_DSO2250, MODEL_DSO5200, MODEL_DSO5200A, MODEL_DSO6022] -///

-/// The 0xa2 control read/write command gives access to a ControlValue. -///

-/// -/// CONTROL_GETSPEED [MODEL_DSO2090, MODEL_DSO2150, MODEL_DSO2250, MODEL_DSO5200, MODEL_DSO5200A, MODEL_DSO6022] -///

-/// The 0xb2 control read command gets the speed level of the USB -/// connection: -/// -/// -/// -/// -/// -/// -/// -/// -/// -/// -/// -/// -/// -///
ConnectionSpeed0x000x000x000x000x000x000x000x000x00
-///

-/// -/// CONTROL_BEGINCOMMAND [MODEL_DSO2090, MODEL_DSO2150, MODEL_DSO2250, MODEL_DSO5200, MODEL_DSO5200A] -///

-/// The 0xb3 control write command is sent before any bulk command: -/// -/// -/// -/// -/// -/// -/// -/// -/// -/// -/// -/// -/// -///
0x0fBulkIndexBulkIndexBulkIndex0x000x000x000x000x000x00
-///

-/// -/// CONTROL_SETOFFSET [MODEL_DSO2090, MODEL_DSO2150, MODEL_DSO2250, MODEL_DSO5200, MODEL_DSO5200A] -///

-/// The 0xb4 control write command sets the channel offsets: -/// -/// -/// -/// -/// -/// -/// -/// -/// -///
Ch1Offset[1]Ch1Offset[0]Ch2Offset[1]Ch2Offset[0]TriggerOffset[1]TriggerOffset[0]
-/// -/// -/// -/// -/// -/// -/// -/// -/// -/// -/// -/// -/// -/// -///
0x000x000x000x000x000x000x000x000x000x000x00
-///

-/// -/// CONTROL_SETRELAYS [MODEL_DSO2090, MODEL_DSO2150, MODEL_DSO2250, MODEL_DSO5200, MODEL_DSO5200A] -///

-/// The 0xb5 control write command sets the internal relays: -/// -/// -/// -/// -/// -/// -/// -///
0x000x04 ^ (Ch1Gain < 1 V)0x08 ^ (Ch1Gain < 100 mV)0x02 ^ (Ch1Coupling == DC)
-/// -/// -/// -/// -/// -/// -/// -///
0x20 ^ (Ch2Gain < 1 V)0x40 ^ (Ch2Gain < 100 mV)0x10 ^ (Ch2Coupling == DC)0x01 ^ (Trigger == EXT)
-/// -/// -/// -/// -/// -/// -/// -/// -/// -/// -/// -/// -///
0x000x000x000x000x000x000x000x000x00
-///

-///

-/// The limits are <= instead of < for the 10 bit models, since those -/// support voltages up to 10 V. -///

-/// -/// CONTROL_SETVOLTDIV_CH1 CH1 voltage div setting (6022BE/BL) -/// -/// CONTROL_SETVOLTDIV_CH2 CH2 voltage div setting (6022BE/BL) -/// -/// CONTROL_SETTIMEDIV Time divisor setting (6022BE/BL) -/// -/// CONTROL_ACQUIIRE_HARD_DATA Request sample data (6022BE/BL) -enum class ControlCode : uint8_t { - CONTROL_VALUE = 0xa2, - CONTROL_GETSPEED = 0xb2, - CONTROL_BEGINCOMMAND = 0xb3, - CONTROL_SETOFFSET = 0xb4, - CONTROL_SETRELAYS = 0xb5, - CONTROL_SETVOLTDIV_CH1 = 0xe0, - CONTROL_SETVOLTDIV_CH2 = 0xe1, - CONTROL_SETTIMEDIV = 0xe2, - CONTROL_ACQUIIRE_HARD_DATA = 0xe3 -}; - -} diff --git a/openhantek/src/hantekprotocol/definitions.h b/openhantek/src/hantekprotocol/definitions.h index 11b5f6ed..38308b0e 100644 --- a/openhantek/src/hantekprotocol/definitions.h +++ b/openhantek/src/hantekprotocol/definitions.h @@ -6,8 +6,6 @@ #include #include -#define HANTEK_GAIN_STEPS 9 - namespace Hantek { /// \enum UsedChannels /// \brief The enabled channels. @@ -37,10 +35,6 @@ struct Offset { unsigned short end = 0xffff; }; -struct OffsetsPerGainStep { - Offset step[HANTEK_GAIN_STEPS]; -}; - /// \struct FilterBits /// \brief The bits for BULK::SETFILTER. struct FilterBits { diff --git a/openhantek/src/hantekprotocol/types.h b/openhantek/src/hantekprotocol/types.h index 238c8e17..18be2dfa 100644 --- a/openhantek/src/hantekprotocol/types.h +++ b/openhantek/src/hantekprotocol/types.h @@ -1,6 +1,16 @@ // SPDX-License-Identifier: GPL-2.0+ #pragma once +#include +#include +using namespace std::literals::chrono_literals; typedef unsigned RecordLengthID; typedef unsigned ChannelID; + +using Samples = double; +Samples operator""_S(long double); ///< User literal for samples + +using Voltage = double; + +using Seconds = std::chrono::duration; diff --git a/openhantek/src/main.cpp b/openhantek/src/main.cpp index 8c2513c1..2ed2fd67 100644 --- a/openhantek/src/main.cpp +++ b/openhantek/src/main.cpp @@ -5,6 +5,7 @@ #include #include #include +#include #include #include @@ -17,19 +18,20 @@ #include "viewconstants.h" // DSO core logic +#include "dsocontrol.h" +#include "dsoloop.h" #include "dsomodel.h" -#include "hantekdsocontrol.h" #include "usb/usbdevice.h" // Post processing #include "post/graphgenerator.h" #include "post/mathchannelgenerator.h" #include "post/postprocessing.h" +#include "post/selfcalibration.h" #include "post/spectrumgenerator.h" // Exporter #include "exporting/exportcsv.h" -#include "exporting/exporterprocessor.h" #include "exporting/exporterregistry.h" #include "exporting/exportimage.h" #include "exporting/exportprint.h" @@ -39,46 +41,12 @@ #include "mainwindow.h" #include "selectdevice/selectsupporteddevice.h" -// OpenGL setup -#include "glscope.h" - #ifndef VERSION #error "You need to run the cmake buildsystem!" #endif using namespace Hantek; -/// \brief Initialize the device with the current settings. -void applySettingsToDevice(HantekDsoControl *dsoControl, DsoSettingsScope *scope, - const Dso::ControlSpecification *spec) { - bool mathUsed = scope->anyUsed(spec->channels); - for (ChannelID channel = 0; channel < spec->channels; ++channel) { - dsoControl->setCoupling(channel, scope->coupling(channel, spec)); - dsoControl->setGain(channel, scope->gain(channel) * DIVS_VOLTAGE); - dsoControl->setOffset(channel, (scope->voltage[channel].offset / DIVS_VOLTAGE) + 0.5); - dsoControl->setTriggerLevel(channel, scope->voltage[channel].trigger); - dsoControl->setChannelUsed(channel, mathUsed | scope->anyUsed(channel)); - } - - if (scope->horizontal.samplerateSource == DsoSettingsScopeHorizontal::Samplerrate) - dsoControl->setSamplerate(scope->horizontal.samplerate); - else - dsoControl->setRecordTime(scope->horizontal.timebase * DIVS_TIME); - - if (dsoControl->getAvailableRecordLengths().empty()) - dsoControl->setRecordLength(scope->horizontal.recordLength); - else { - auto recLenVec = dsoControl->getAvailableRecordLengths(); - ptrdiff_t index = std::distance(recLenVec.begin(), - std::find(recLenVec.begin(), recLenVec.end(), scope->horizontal.recordLength)); - dsoControl->setRecordLength(index < 0 ? 1 : (unsigned)index); - } - dsoControl->setTriggerMode(scope->trigger.mode); - dsoControl->setPretriggerPosition(scope->trigger.position * scope->horizontal.timebase * DIVS_TIME); - dsoControl->setTriggerSlope(scope->trigger.slope); - dsoControl->setTriggerSource(scope->trigger.special, scope->trigger.source); -} - /// \brief Initialize resources and translations and show the main window. int main(int argc, char *argv[]) { //////// Set application information //////// @@ -87,9 +55,8 @@ int main(int argc, char *argv[]) { QCoreApplication::setApplicationName("OpenHantek"); QCoreApplication::setApplicationVersion(VERSION); QCoreApplication::setAttribute(Qt::AA_UseHighDpiPixmaps, true); -#if (QT_VERSION >= QT_VERSION_CHECK(5, 6, 0)) QCoreApplication::setAttribute(Qt::AA_EnableHighDpiScaling, true); -#endif + QCoreApplication::setAttribute(Qt::AA_ShareOpenGLContexts, false); bool useGles = false; { @@ -102,8 +69,22 @@ int main(int argc, char *argv[]) { p.process(parserApp); useGles = p.isSet(useGlesOption); } - - GlScope::fixOpenGLversion(useGles ? QSurfaceFormat::OpenGLES : QSurfaceFormat::OpenGL); + // Prefer full desktop OpenGL without fixed pipeline + QSurfaceFormat format; + format.setSamples(4); // Antia-Aliasing, Multisampling + format.setDepthBufferSize(24); + format.setStencilBufferSize(8); + format.setProfile(QSurfaceFormat::CoreProfile); + if (useGles || QOpenGLContext::openGLModuleType() == QOpenGLContext::LibGLES) { + format.setVersion(2, 0); + format.setRenderableType(QSurfaceFormat::OpenGLES); + QCoreApplication::setAttribute(Qt::AA_UseOpenGLES, true); + } else { + format.setVersion(3, 2); + format.setRenderableType(QSurfaceFormat::OpenGL); + QCoreApplication::setAttribute(Qt::AA_UseOpenGLES, false); + } + QSurfaceFormat::setDefaultFormat(format); QApplication openHantekApplication(argc, argv); @@ -133,28 +114,29 @@ int main(int argc, char *argv[]) { return -1; } + //////// Create settings object //////// + Settings::DsoSettings settings(device->getModel()->spec()); + //////// Create DSO control object and move it to a separate thread //////// QThread dsoControlThread; dsoControlThread.setObjectName("dsoControlThread"); - HantekDsoControl dsoControl(device.get()); + DsoControl dsoControl(device.get(), settings.deviceSettings); + settings.load(dsoControl.channelUsage()); dsoControl.moveToThread(&dsoControlThread); - QObject::connect(&dsoControlThread, &QThread::started, &dsoControl, &HantekDsoControl::run); - QObject::connect(&dsoControl, &HantekDsoControl::communicationError, QCoreApplication::instance(), + QObject::connect(&dsoControlThread, &QThread::started, &dsoControl, &DsoControl::start); + QObject::connect(&dsoControl, &DsoControl::communicationError, QCoreApplication::instance(), &QCoreApplication::quit); QObject::connect(device.get(), &USBDevice::deviceDisconnected, QCoreApplication::instance(), &QCoreApplication::quit); - //////// Create settings object //////// - DsoSettings settings(device->getModel()->spec()); + SelfCalibration selfCalibration(&dsoControl); //////// Create exporters //////// - ExporterRegistry exportRegistry(device->getModel()->spec(), &settings); + Exporter::Registry exportRegistry(device->getModel()->spec(), &settings); - ExporterCSV exporterCSV; - ExporterImage exportImage; - ExporterPrint exportPrint; - - ExporterProcessor samplesToExportRaw(&exportRegistry); + Exporter::CSV exporterCSV; + Exporter::Image exportImage; + Exporter::Print exportPrint; exportRegistry.registerExporter(&exporterCSV); exportRegistry.registerExporter(&exportImage); @@ -163,37 +145,34 @@ int main(int argc, char *argv[]) { //////// Create post processing objects //////// QThread postProcessingThread; postProcessingThread.setObjectName("postProcessingThread"); - PostProcessing postProcessing(settings.scope.countChannels()); + PostProcessing::Executor postProcessing(&settings.scope); - SpectrumGenerator spectrumGenerator(&settings.scope, &settings.post); - MathChannelGenerator mathchannelGenerator(&settings.scope, device->getModel()->spec()->channels); - GraphGenerator graphGenerator(&settings.scope, device->getModel()->spec()->isSoftwareTriggerDevice); + PostProcessing::SpectrumGenerator spectrumGenerator(&settings.scope, &settings.post); + PostProcessing::MathChannelGenerator mathchannelGenerator(&settings.scope); + PostProcessing::GraphGenerator graphGenerator(&settings.scope, settings.deviceSettings.get(), + dsoControl.channelUsage()); - postProcessing.registerProcessor(&samplesToExportRaw); + postProcessing.registerProcessor(&selfCalibration); postProcessing.registerProcessor(&mathchannelGenerator); postProcessing.registerProcessor(&spectrumGenerator); postProcessing.registerProcessor(&graphGenerator); postProcessing.moveToThread(&postProcessingThread); - QObject::connect(&dsoControl, &HantekDsoControl::samplesAvailable, &postProcessing, &PostProcessing::input); - QObject::connect(&postProcessing, &PostProcessing::processingFinished, &exportRegistry, &ExporterRegistry::input, - Qt::DirectConnection); + QObject::connect(&dsoControl, &DsoControl::samplesAvailable, &postProcessing, &PostProcessing::Executor::input); + QObject::connect(&postProcessing, &PostProcessing::Executor::processingFinished, &exportRegistry, + &Exporter::Registry::input, Qt::DirectConnection); //////// Create main window //////// iconFont->initFontAwesome(); - MainWindow openHantekMainWindow(&dsoControl, &settings, &exportRegistry); - QObject::connect(&postProcessing, &PostProcessing::processingFinished, &openHantekMainWindow, + MainWindow openHantekMainWindow(&dsoControl, &settings, &exportRegistry, &selfCalibration); + QObject::connect(&postProcessing, &PostProcessing::Executor::processingFinished, &openHantekMainWindow, &MainWindow::showNewData); - QObject::connect(&exportRegistry, &ExporterRegistry::exporterProgressChanged, &openHantekMainWindow, - &MainWindow::exporterProgressChanged); - QObject::connect(&exportRegistry, &ExporterRegistry::exporterStatusChanged, &openHantekMainWindow, + QObject::connect(&exportRegistry, &Exporter::Registry::exporterStatusChanged, &openHantekMainWindow, &MainWindow::exporterStatusChanged); openHantekMainWindow.show(); - applySettingsToDevice(&dsoControl, &settings.scope, device->getModel()->spec()); - //////// Start DSO thread and go into GUI main loop - dsoControl.enableSampling(true); + dsoControl.loopControl()->enableSampling(true); postProcessingThread.start(); dsoControlThread.start(); int res = openHantekApplication.exec(); @@ -205,7 +184,10 @@ int main(int argc, char *argv[]) { postProcessingThread.quit(); postProcessingThread.wait(10000); - if (context && device != nullptr) { libusb_exit(context); } + if (context && device != nullptr) { + device.reset(); + libusb_exit(context); + } return res; } diff --git a/openhantek/src/mainwindow.cpp b/openhantek/src/mainwindow.cpp index 3c277a4d..bb27a39f 100644 --- a/openhantek/src/mainwindow.cpp +++ b/openhantek/src/mainwindow.cpp @@ -1,31 +1,40 @@ #include "mainwindow.h" -#include "iconfont/QtAwesome.h" #include "ui_mainwindow.h" -#include "HorizontalDock.h" -#include "SpectrumDock.h" -#include "TriggerDock.h" -#include "VoltageDock.h" -#include "dockwindows.h" +#include + +#include +#include +#include +#include +#include + +#include "docks/DebugDock.h" +#include "docks/HorizontalDock.h" +#include "docks/TriggerDock.h" +#include "docks/VoltageOrSpectrumDock.h" +#include "docks/dockwindows.h" +#include "docks/gainAdjustDock.h" + +#include "configdialog/configdialog.h" -#include "configdialog.h" -#include "dockwindows.h" -#include "dsomodel.h" -#include "dsowidget.h" #include "exporting/exporterinterface.h" #include "exporting/exporterregistry.h" -#include "hantekdsocontrol.h" + +#include "hantekdso/dsocontrol.h" +#include "hantekdso/dsoloop.h" +#include "hantekdso/dsomodel.h" + +#include "iconfont/QtAwesome.h" +#include "settings/settings.h" #include "usb/usbdevice.h" #include "viewconstants.h" +#include "widgets/dsowidget.h" -#include "settings.h" - -#include -#include -#include +using namespace std; -MainWindow::MainWindow(HantekDsoControl *dsoControl, DsoSettings *settings, ExporterRegistry *exporterRegistry, - QWidget *parent) +MainWindow::MainWindow(DsoControl *dsoControl, Settings::DsoSettings *settings, Exporter::Registry *exporterRegistry, + SelfCalibration *selfCalibration, QWidget *parent) : QMainWindow(parent), ui(new Ui::MainWindow), mSettings(settings), exporterRegistry(exporterRegistry) { ui->setupUi(this); ui->actionSave->setIcon(iconFont->icon(fa::save)); @@ -36,163 +45,81 @@ MainWindow::MainWindow(HantekDsoControl *dsoControl, DsoSettings *settings, Expo std::make_pair("text-off", QChar(fa::play)), std::make_pair("text-active-off", QChar(fa::play))})); ui->actionSettings->setIcon(iconFont->icon(fa::gear)); - ui->actionManualCommand->setIcon(iconFont->icon(fa::edit)); ui->actionDigital_phosphor->setIcon(QIcon(":/images/digitalphosphor.svg")); - ui->actionZoom->setIcon(iconFont->icon(fa::crop)); + ui->actionAddMarker->setIcon(iconFont->icon(fa::searchplus)); + ui->actionRemoveMarker->setIcon(iconFont->icon(fa::trash)); + ui->actionReport_an_issue->setIcon(iconFont->icon(fa::bug)); // Window title setWindowIcon(QIcon(":openhantek.png")); - setWindowTitle( - tr("OpenHantek - Device %1 - Renderer %2") - .arg(QString::fromStdString(dsoControl->getDevice()->getModel()->name)) - .arg(QSurfaceFormat::defaultFormat().renderableType() == QSurfaceFormat::OpenGL ? "OpenGL" : "OpenGL ES")); +#ifdef DEBUG + const char *titleText = "OpenHantek (Debug Mode) - Device %1 - Renderer %2"; +#else + const char *titleText = "OpenHantek - Device %1 - Renderer %2"; +#endif + const QString modelName = QString::fromStdString(dsoControl->getDevice()->getModel()->name); + const QString rendererName = + QSurfaceFormat::defaultFormat().renderableType() == QSurfaceFormat::OpenGL ? "OpenGL" : "OpenGL ES"; + setWindowTitle(tr(titleText).arg(modelName).arg(rendererName)); -#if (QT_VERSION >= QT_VERSION_CHECK(5, 6, 0)) setDockOptions(dockOptions() | QMainWindow::GroupedDragging); -#endif for (auto *exporter : *exporterRegistry) { QAction *action = new QAction(exporter->icon(), exporter->name(), this); - action->setCheckable(exporter->type() == ExporterInterface::Type::ContinousExport); + action->setShortcut(exporter->shortcut()); + action->setCheckable(exporter->type() == Exporter::ExporterInterface::Type::ContinousExport); connect(action, &QAction::triggered, [exporter, exporterRegistry](bool checked) { - exporterRegistry->setExporterEnabled( - exporter, exporter->type() == ExporterInterface::Type::ContinousExport ? checked : true); + if (exporter->type() == Exporter::ExporterInterface::Type::ContinousExport && !checked) { + exporterRegistry->stopContinous(exporter); + } else + exporterRegistry->exportNow(exporter); }); ui->menuExport->addAction(action); + ui->toolBar->insertAction(ui->toolBar->actions()[0], action); } - DsoSettingsScope *scope = &(mSettings->scope); - const Dso::ControlSpecification *spec = dsoControl->getDevice()->getModel()->spec(); + Settings::Scope *scope = &(mSettings->scope); + Settings::View *view = &(mSettings->view); registerDockMetaTypes(); // Docking windows // Create dock windows before the dso widget, they fix messed up settings - HorizontalDock *horizontalDock; - TriggerDock *triggerDock; - SpectrumDock *spectrumDock; - VoltageDock *voltageDock; - horizontalDock = new HorizontalDock(scope, this); - triggerDock = new TriggerDock(scope, spec, this); - spectrumDock = new SpectrumDock(scope, this); - voltageDock = new VoltageDock(scope, spec, this); + GainAdjustDock *gainAdjustDock = new GainAdjustDock(dsoControl, selfCalibration, this); + DebugDock *debugDock = new DebugDock(dsoControl, this); + HorizontalDock *horizontalDock = new HorizontalDock(scope, dsoControl, this); + TriggerDock *triggerDock = new TriggerDock(scope, dsoControl, this); + VoltageOrSpectrumDock *spectrumDock = new VoltageOrSpectrumDock(true, scope, dsoControl, this); + VoltageOrSpectrumDock *voltageDock = new VoltageOrSpectrumDock(false, scope, dsoControl, this); addDockWidget(Qt::RightDockWidgetArea, horizontalDock); addDockWidget(Qt::RightDockWidgetArea, triggerDock); addDockWidget(Qt::RightDockWidgetArea, voltageDock); addDockWidget(Qt::RightDockWidgetArea, spectrumDock); + addDockWidget(Qt::LeftDockWidgetArea, debugDock); + addDockWidget(Qt::LeftDockWidgetArea, gainAdjustDock); + + gainAdjustDock->hide(); + + ui->actionDocks->setMenu(createPopupMenu()); + +#ifdef DEBUG + debugDock->show(); +#else + debugDock->hide(); +#endif restoreGeometry(mSettings->mainWindowGeometry); restoreState(mSettings->mainWindowState); // Central oszilloscope widget - dsoWidget = new DsoWidget(&mSettings->scope, &mSettings->view, spec); + dsoWidget = new DsoWidget(&mSettings->scope, &mSettings->view, dsoControl); setCentralWidget(dsoWidget); - - // Command field inside the status bar - QLineEdit *commandEdit = new QLineEdit(this); - commandEdit->hide(); - - statusBar()->addPermanentWidget(commandEdit, 1); - - connect(ui->actionManualCommand, &QAction::toggled, [this, commandEdit](bool checked) { - commandEdit->setVisible(checked); - if (checked) commandEdit->setFocus(); - }); - - connect(commandEdit, &QLineEdit::returnPressed, [this, commandEdit, dsoControl]() { - Dso::ErrorCode errorCode = dsoControl->stringCommand(commandEdit->text()); - commandEdit->clear(); - this->ui->actionManualCommand->setChecked(false); - if (errorCode != Dso::ErrorCode::NONE) statusBar()->showMessage(tr("Invalid command"), 3000); - }); - - // Connect general signals - connect(dsoControl, &HantekDsoControl::statusMessage, statusBar(), &QStatusBar::showMessage); - - // Connect signals to DSO controller and widget - connect(horizontalDock, &HorizontalDock::samplerateChanged, [dsoControl, this]() { - dsoControl->setSamplerate(mSettings->scope.horizontal.samplerate); - this->dsoWidget->updateSamplerate(mSettings->scope.horizontal.samplerate); - }); - connect(horizontalDock, &HorizontalDock::timebaseChanged, [dsoControl, this]() { - dsoControl->setRecordTime(mSettings->scope.horizontal.timebase * DIVS_TIME); - this->dsoWidget->updateTimebase(mSettings->scope.horizontal.timebase); - }); - connect(horizontalDock, &HorizontalDock::frequencybaseChanged, dsoWidget, &DsoWidget::updateFrequencybase); - connect(horizontalDock, &HorizontalDock::recordLengthChanged, - [dsoControl](unsigned long recordLength) { dsoControl->setRecordLength(recordLength); }); - - connect(dsoControl, &HantekDsoControl::recordTimeChanged, - [this, settings, horizontalDock, dsoControl](double duration) { - if (settings->scope.horizontal.samplerateSource == DsoSettingsScopeHorizontal::Samplerrate && - settings->scope.horizontal.recordLength != UINT_MAX) { - // The samplerate was set, let's adapt the timebase accordingly - settings->scope.horizontal.timebase = horizontalDock->setTimebase(duration / DIVS_TIME); - } - - // The trigger position should be kept at the same place but the timebase has - // changed - dsoControl->setPretriggerPosition(settings->scope.trigger.position * - settings->scope.horizontal.timebase * DIVS_TIME); - - this->dsoWidget->updateTimebase(settings->scope.horizontal.timebase); - }); - connect(dsoControl, &HantekDsoControl::samplerateChanged, [this, horizontalDock](double samplerate) { - if (mSettings->scope.horizontal.samplerateSource == DsoSettingsScopeHorizontal::Duration && - mSettings->scope.horizontal.recordLength != UINT_MAX) { - // The timebase was set, let's adapt the samplerate accordingly - mSettings->scope.horizontal.samplerate = samplerate; - horizontalDock->setSamplerate(samplerate); - dsoWidget->updateSamplerate(samplerate); - } - }); - - connect(triggerDock, &TriggerDock::modeChanged, dsoControl, &HantekDsoControl::setTriggerMode); - connect(triggerDock, &TriggerDock::modeChanged, dsoWidget, &DsoWidget::updateTriggerMode); - connect(triggerDock, &TriggerDock::sourceChanged, dsoControl, &HantekDsoControl::setTriggerSource); - connect(triggerDock, &TriggerDock::sourceChanged, dsoWidget, &DsoWidget::updateTriggerSource); - connect(triggerDock, &TriggerDock::slopeChanged, dsoControl, &HantekDsoControl::setTriggerSlope); - connect(triggerDock, &TriggerDock::slopeChanged, dsoWidget, &DsoWidget::updateTriggerSlope); - connect(dsoWidget, &DsoWidget::triggerPositionChanged, dsoControl, &HantekDsoControl::setPretriggerPosition); - connect(dsoWidget, &DsoWidget::triggerLevelChanged, dsoControl, &HantekDsoControl::setTriggerLevel); - - auto usedChanged = [this, dsoControl, spec](ChannelID channel, bool used) { - if (channel >= (unsigned int)mSettings->scope.voltage.size()) return; - - bool mathUsed = mSettings->scope.anyUsed(spec->channels); - - // Normal channel, check if voltage/spectrum or math channel is used - if (channel < spec->channels) dsoControl->setChannelUsed(channel, mathUsed | mSettings->scope.anyUsed(channel)); - // Math channel, update all channels - else if (channel == spec->channels) { - for (ChannelID c = 0; c < spec->channels; ++c) - dsoControl->setChannelUsed(c, mathUsed | mSettings->scope.anyUsed(c)); - } - }; - connect(voltageDock, &VoltageDock::usedChanged, usedChanged); - connect(spectrumDock, &SpectrumDock::usedChanged, usedChanged); - - connect(voltageDock, &VoltageDock::couplingChanged, dsoControl, &HantekDsoControl::setCoupling); - connect(voltageDock, &VoltageDock::couplingChanged, dsoWidget, &DsoWidget::updateVoltageCoupling); - connect(voltageDock, &VoltageDock::modeChanged, dsoWidget, &DsoWidget::updateMathMode); - connect(voltageDock, &VoltageDock::gainChanged, [this, dsoControl, spec](ChannelID channel, double gain) { - if (channel >= spec->channels) return; - - dsoControl->setGain(channel, mSettings->scope.gain(channel) * DIVS_VOLTAGE); - }); - connect(voltageDock, &VoltageDock::gainChanged, dsoWidget, &DsoWidget::updateVoltageGain); - connect(dsoWidget, &DsoWidget::offsetChanged, [this, dsoControl, spec](ChannelID channel) { - if (channel >= spec->channels) return; - dsoControl->setOffset(channel, (mSettings->scope.voltage[channel].offset / DIVS_VOLTAGE) + 0.5); - }); - - connect(voltageDock, &VoltageDock::usedChanged, dsoWidget, &DsoWidget::updateVoltageUsed); - connect(spectrumDock, &SpectrumDock::usedChanged, dsoWidget, &DsoWidget::updateSpectrumUsed); - connect(spectrumDock, &SpectrumDock::magnitudeChanged, dsoWidget, &DsoWidget::updateSpectrumMagnitude); + connect(dsoWidget, &DsoWidget::requestStatusText, statusBar(), + [this](const QString &text) { statusBar()->showMessage(text, 1200); }); // Started/stopped signals from oscilloscope - connect(dsoControl, &HantekDsoControl::samplingStatusChanged, [this, dsoControl](bool enabled) { + connect(dsoControl, &DsoControl::samplingStatusChanged, [this, dsoControl](bool enabled) { QSignalBlocker blocker(this->ui->actionSampling); if (enabled) { this->ui->actionSampling->setText(tr("&Stop")); @@ -203,19 +130,13 @@ MainWindow::MainWindow(HantekDsoControl *dsoControl, DsoSettings *settings, Expo } this->ui->actionSampling->setChecked(enabled); }); - connect(this->ui->actionSampling, &QAction::triggered, dsoControl, &HantekDsoControl::enableSampling); - this->ui->actionSampling->setChecked(dsoControl->isSampling()); + connect(this->ui->actionSampling, &QAction::triggered, dsoControl->loopControl(), &DsoLoop::enableSampling); + this->ui->actionSampling->setChecked(dsoControl->loopControl()->isSampling()); - connect(dsoControl, &HantekDsoControl::availableRecordLengthsChanged, horizontalDock, - &HorizontalDock::setAvailableRecordLengths); - connect(dsoControl, &HantekDsoControl::samplerateLimitsChanged, horizontalDock, - &HorizontalDock::setSamplerateLimits); - connect(dsoControl, &HantekDsoControl::samplerateSet, horizontalDock, &HorizontalDock::setSamplerateSteps); - - connect(ui->actionOpen, &QAction::triggered, [this]() { + connect(ui->actionOpen, &QAction::triggered, [this, dsoControl]() { QString fileName = QFileDialog::getOpenFileName(this, tr("Open file"), "", tr("Settings (*.ini)")); if (!fileName.isEmpty()) { - if (mSettings->setFilename(fileName)) { mSettings->load(); } + if (mSettings->setFilename(fileName)) { mSettings->load(dsoControl->channelUsage()); } } }); @@ -246,26 +167,47 @@ MainWindow::MainWindow(HantekDsoControl *dsoControl, DsoSettings *settings, Expo }); connect(this->ui->actionDigital_phosphor, &QAction::toggled, [this](bool enabled) { - mSettings->view.digitalPhosphor = enabled; + mSettings->view.setDigitalPhosphor(enabled, mSettings->view.digitalPhosphorDepth()); - if (mSettings->view.digitalPhosphor) + if (mSettings->view.digitalPhosphor()) this->ui->actionDigital_phosphor->setStatusTip(tr("Disable fading of previous graphs")); else this->ui->actionDigital_phosphor->setStatusTip(tr("Enable fading of previous graphs")); }); - this->ui->actionDigital_phosphor->setChecked(mSettings->view.digitalPhosphor); + this->ui->actionDigital_phosphor->setChecked(mSettings->view.digitalPhosphor()); + + connect(ui->actionAddMarker, &QAction::triggered, [this, view]() { + Settings::MarkerAndZoom v; + v.markerRect = QRectF(-1.0, -DIVS_VOLTAGE / 2, 2.0, DIVS_VOLTAGE); + Settings::ZoomViewSettings &z = view->zoomviews; + unsigned markerID = 0; + if (z.size()) + // Use highest unique id and add 1. + + markerID = max_element(z.begin(), z.end(), + [](Settings::ZoomViewSettings::value_type &a, + Settings::ZoomViewSettings::value_type &b) { return a.first < b.first; }) + ->first + + 1; + z.insert(markerID, v); + }); - connect(ui->actionZoom, &QAction::toggled, [this](bool enabled) { - mSettings->view.zoom = enabled; + connect(ui->actionRemoveMarker, &QAction::triggered, + [this, view]() { view->zoomviews.removeMarker(view->zoomviews.activeMarker()); }); + connect(&view->zoomviews, &Settings::ZoomViewSettings::activeMarkerChanged, this, + [this](int activeMarker) { ui->actionRemoveMarker->setEnabled(activeMarker != -1); }); + connect(&view->zoomviews, &Settings::ZoomViewSettings::markerChanged, this, + [this](int activeMarker) { ui->actionRemoveMarker->setEnabled(activeMarker != -1); }); - if (mSettings->view.zoom) - this->ui->actionZoom->setStatusTip(tr("Hide magnified scope")); - else - this->ui->actionZoom->setStatusTip(tr("Show magnified scope")); + connect(ui->actionReport_an_issue, &QAction::triggered, [this]() { + QMessageBox::about( + this, tr("Report an issue - V%1").arg(VERSION), + tr("

Please remember, this is a non-paid open source software.

" + "

Help us by providing meaningful bug-reports. Don't forget to mention your operating system, version " + "and as much details as possible
" + "https://github.com/OpenHantek/openhantek

")); - this->dsoWidget->updateZoom(enabled); }); - ui->actionZoom->setChecked(mSettings->view.zoom); connect(ui->actionAbout, &QAction::triggered, [this]() { QMessageBox::about( @@ -277,16 +219,6 @@ MainWindow::MainWindow(HantekDsoControl *dsoControl, DsoSettings *settings, Expo "https://github.com/OpenHantek/openhantek

")); }); - - if (mSettings->scope.horizontal.samplerateSource == DsoSettingsScopeHorizontal::Samplerrate) - dsoWidget->updateSamplerate(mSettings->scope.horizontal.samplerate); - else - dsoWidget->updateTimebase(mSettings->scope.horizontal.timebase); - - for (ChannelID channel = 0; channel < spec->channels; ++channel) { - this->dsoWidget->updateVoltageUsed(channel, mSettings->scope.voltage[channel].used); - this->dsoWidget->updateSpectrumUsed(channel, mSettings->scope.spectrum[channel].used); - } } MainWindow::~MainWindow() { delete ui; } @@ -297,8 +229,6 @@ void MainWindow::exporterStatusChanged(const QString &exporterName, const QStrin ui->statusbar->showMessage(tr("%1: %2").arg(exporterName).arg(status)); } -void MainWindow::exporterProgressChanged() { exporterRegistry->checkForWaitingExporters(); } - /// \brief Save the settings before exiting. /// \param event The close event that should be handled. void MainWindow::closeEvent(QCloseEvent *event) { diff --git a/openhantek/src/mainwindow.h b/openhantek/src/mainwindow.h index d17b7ab8..c6cb7599 100644 --- a/openhantek/src/mainwindow.h +++ b/openhantek/src/mainwindow.h @@ -4,18 +4,22 @@ #include class SpectrumGenerator; -class HantekDsoControl; +class DsoControl; +namespace Settings { class DsoSettings; -class ExporterRegistry; +} class DsoWidget; class HorizontalDock; class TriggerDock; class SpectrumDock; -class VoltageDock; - +class VoltageOrSpectrumDock; +namespace Exporter { +class Registry; +} namespace Ui { class MainWindow; } +class SelfCalibration; /// \brief The main window of the application. /// The main window contains the classic oszilloscope-screen and the gui @@ -24,13 +28,12 @@ class MainWindow : public QMainWindow { Q_OBJECT public: - explicit MainWindow(HantekDsoControl *dsoControl, DsoSettings *mSettings, ExporterRegistry *exporterRegistry, - QWidget *parent = 0); + explicit MainWindow(DsoControl *dsoControl, Settings::DsoSettings *mSettings, + Exporter::Registry *exporterRegistry, SelfCalibration *selfCalibration, QWidget *parent = 0); ~MainWindow(); public slots: void showNewData(std::shared_ptr data); void exporterStatusChanged(const QString &exporterName, const QString &status); - void exporterProgressChanged(); protected: void closeEvent(QCloseEvent *event) override; @@ -42,6 +45,6 @@ class MainWindow : public QMainWindow { DsoWidget *dsoWidget; // Settings used for the whole program - DsoSettings *mSettings; - ExporterRegistry *exporterRegistry; + Settings::DsoSettings *mSettings; + Exporter::Registry *exporterRegistry; }; diff --git a/openhantek/src/mainwindow.ui b/openhantek/src/mainwindow.ui index e9eb479d..528c6ee2 100644 --- a/openhantek/src/mainwindow.ui +++ b/openhantek/src/mainwindow.ui @@ -38,8 +38,8 @@ &View - - + + @@ -77,14 +77,15 @@ false - - - + + + + @@ -106,11 +107,17 @@ Save as ... + + Ctrl+Shift+S + Exit + + Ctrl+Q + @@ -120,28 +127,13 @@ Digital phosphor - - - true - - - Zoom - - - - - Docking windows - - - - - Toolbars - - About + + Ctrl+A + @@ -159,12 +151,27 @@ Space - - - true + + + Add marker/zoom view + + - Manual command + Docks + + + + + Report an issue + + + + + Remove Marker + + + Remove current marker and zoomview diff --git a/openhantek/src/post/enums.cpp b/openhantek/src/post/enums.cpp new file mode 100644 index 00000000..73249933 --- /dev/null +++ b/openhantek/src/post/enums.cpp @@ -0,0 +1,70 @@ +// SPDX-License-Identifier: GPL-2.0+ + +#include "postprocessingsettings.h" + +#include "settings/scopesettings.h" +#include +#include + +namespace PostProcessing { + +QString mathModeString(MathMode mode, const ::Settings::Channel *first, const ::Settings::Channel *second) { + switch (mode) { + case MathMode::ADD: + return QCoreApplication::tr("%1 + %2").arg(first->name(), second->name()); + case MathMode::SUBSTRACT: + return QCoreApplication::tr("%1 - %2").arg(first->name(), second->name()); + case MathMode::MULTIPLY: + return QCoreApplication::tr("%1 * %2").arg(first->name(), second->name()); + } + return QString(); +} + +QString mathModeString(MathMode mode) { + switch (mode) { + case MathMode::ADD: + return "+"; + case MathMode::SUBSTRACT: + return "-"; + case MathMode::MULTIPLY: + return "*"; + } + return QString(); +} + +QString windowFunctionString(WindowFunction window) { + switch (window) { + case WindowFunction::RECTANGULAR: + return QCoreApplication::tr("Rectangular"); + case WindowFunction::HAMMING: + return QCoreApplication::tr("Hamming"); + case WindowFunction::HANN: + return QCoreApplication::tr("Hann"); + case WindowFunction::COSINE: + return QCoreApplication::tr("Cosine"); + case WindowFunction::LANCZOS: + return QCoreApplication::tr("Lanczos"); + case WindowFunction::BARTLETT: + return QCoreApplication::tr("Bartlett"); + case WindowFunction::TRIANGULAR: + return QCoreApplication::tr("Triangular"); + case WindowFunction::GAUSS: + return QCoreApplication::tr("Gauss"); + case WindowFunction::BARTLETTHANN: + return QCoreApplication::tr("Bartlett-Hann"); + case WindowFunction::BLACKMAN: + return QCoreApplication::tr("Blackman"); + // case WindowFunction::WINDOW_KAISER: + // return QCoreApplication::tr("Kaiser"); + case WindowFunction::NUTTALL: + return QCoreApplication::tr("Nuttall"); + case WindowFunction::BLACKMANHARRIS: + return QCoreApplication::tr("Blackman-Harris"); + case WindowFunction::BLACKMANNUTTALL: + return QCoreApplication::tr("Blackman-Nuttall"); + case WindowFunction::FLATTOP: + return QCoreApplication::tr("Flat top"); + } + return QString(); +} +} // end namespace PostProcessing diff --git a/openhantek/src/post/enums.h b/openhantek/src/post/enums.h new file mode 100644 index 00000000..d4387648 --- /dev/null +++ b/openhantek/src/post/enums.h @@ -0,0 +1,56 @@ +// SPDX-License-Identifier: GPL-2.0+ + +#pragma once + +#include + +namespace Settings { +class Channel; +} + +namespace PostProcessing { +Q_NAMESPACE + +/// \enum MathMode +/// \brief The different math modes for the math-channel. +enum class MathMode { ADD, SUBSTRACT, MULTIPLY }; +Q_ENUM_NS(MathMode) + +/// \enum WindowFunction +/// \brief The supported window functions. +/// These are needed for spectrum analysis and are applied to the sample values +/// before calculating the DFT. +enum class WindowFunction { + RECTANGULAR, ///< Rectangular window (aka Dirichlet) + HAMMING, ///< Hamming window + HANN, ///< Hann window + COSINE, ///< Cosine window (aka Sine) + LANCZOS, ///< Lanczos window (aka Sinc) + BARTLETT, ///< Bartlett window (Endpoints == 0) + TRIANGULAR, ///< Triangular window (Endpoints != 0) + GAUSS, ///< Gauss window (simga = 0.4) + BARTLETTHANN, ///< Bartlett-Hann window + BLACKMAN, ///< Blackman window (alpha = 0.16) + // KAISER, ///< Kaiser window (alpha = 3.0) + NUTTALL, ///< Nuttall window, cont. first deriv. + BLACKMANHARRIS, ///< Blackman-Harris window + BLACKMANNUTTALL, ///< Blackman-Nuttall window + FLATTOP ///< Flat top window +}; +Q_ENUM_NS(WindowFunction) + +/// \brief Return string representation of the given math mode. +/// \param mode The ::MathMode that should be returned as string. +/// \return The string that should be used in labels etc. +QString mathModeString(MathMode mode, const ::Settings::Channel *first, const ::Settings::Channel *second); + +/// \brief Return string representation of the given math mode. +/// \param mode The ::MathMode that should be returned as string. +/// \return The string that should be used in labels etc. +QString mathModeString(MathMode mode); + +/// \brief Return string representation of the given dft window function. +/// \param window The ::WindowFunction that should be returned as string. +/// \return The string that should be used in labels etc. +QString windowFunctionString(WindowFunction window); +} diff --git a/openhantek/src/post/graphgenerator.cpp b/openhantek/src/post/graphgenerator.cpp index 4d04e773..4b362e60 100644 --- a/openhantek/src/post/graphgenerator.cpp +++ b/openhantek/src/post/graphgenerator.cpp @@ -4,32 +4,19 @@ #include #include +#include "hantekdso/modelspecification.h" #include "post/graphgenerator.h" #include "post/ppresult.h" #include "post/softwaretrigger.h" -#include "hantekdso/controlspecification.h" #include "scopesettings.h" #include "utils/printutils.h" #include "viewconstants.h" -static const SampleValues &useSpecSamplesOf(ChannelID channel, const PPresult *result, - const DsoSettingsScope *scope) { - static SampleValues emptyDefault; - if (!scope->spectrum[channel].used || !result->data(channel)) return emptyDefault; - return result->data(channel)->spectrum; -} - -static const SampleValues &useVoltSamplesOf(ChannelID channel, const PPresult *result, - const DsoSettingsScope *scope) { - static SampleValues emptyDefault; - if (!scope->voltage[channel].used || !result->data(channel)) return emptyDefault; - return result->data(channel)->voltage; -} - -GraphGenerator::GraphGenerator(const DsoSettingsScope *scope, bool isSoftwareTriggerDevice) - : scope(scope), isSoftwareTriggerDevice(isSoftwareTriggerDevice) {} +namespace PostProcessing { -bool GraphGenerator::isReady() const { return ready; } +GraphGenerator::GraphGenerator(const ::Settings::Scope *scope, const Dso::DeviceSettings *deviceSettings, + const Dso::ChannelUsage *channelUsage) + : m_scope(scope), m_deviceSettings(deviceSettings), m_channelUsage(channelUsage) {} void GraphGenerator::generateGraphsTYvoltage(PPresult *result) { unsigned preTrigSamples = 0; @@ -37,144 +24,147 @@ void GraphGenerator::generateGraphsTYvoltage(PPresult *result) { unsigned swTriggerStart = 0; // check trigger point for software trigger - if (isSoftwareTriggerDevice && scope->trigger.source < result->channelCount()) - std::tie(preTrigSamples, postTrigSamples, swTriggerStart) = SoftwareTrigger::compute(result, scope); + if (m_deviceSettings->spec->isSoftwareTriggerDevice && + m_deviceSettings->trigger.source() < m_deviceSettings->voltage.size()) + std::tie(preTrigSamples, postTrigSamples, swTriggerStart) = + SoftwareTrigger::compute(result, m_deviceSettings, m_scope, m_channelUsage); result->softwareTriggerTriggered = postTrigSamples > preTrigSamples; - result->vaChannelVoltage.resize(scope->voltage.size()); - for (ChannelID channel = 0; channel < scope->voltage.size(); ++channel) { - ChannelGraph &target = result->vaChannelVoltage[channel]; - const SampleValues &samples = useVoltSamplesOf(channel, result, scope); + for (DataChannel &channelData : *result) { + ChannelGraph &target = channelData.voltage.graph; + const std::vector &source = channelData.voltage.sample; // Check if this channel is used and available at the data analyzer - if (samples.sample.empty()) { + if (source.empty() || !channelData.channelSettings->visible()) { // Delete all vector arrays target.clear(); continue; } // Check if the sample count has changed - size_t sampleCount = samples.sample.size(); + size_t sampleCount = source.size(); if (sampleCount > 500000) { qWarning() << "Sample count too high!"; throw new std::runtime_error("Sample count too high!"); } - sampleCount -= (swTriggerStart - preTrigSamples); - size_t neededSize = sampleCount * 2; - // Set size directly to avoid reallocations - target.reserve(neededSize); - - // What's the horizontal distance between sampling points? - float horizontalFactor = (float)(samples.interval / scope->horizontal.timebase); - - // Fill vector array - std::vector::const_iterator dataIterator = samples.sample.begin(); - const float gain = (float)scope->gain(channel); - const float offset = (float)scope->voltage[channel].offset; - const float invert = scope->voltage[channel].inverted ? -1.0f : 1.0f; - - std::advance(dataIterator, swTriggerStart - preTrigSamples); + const unsigned offSamples = unsigned(swTriggerStart - preTrigSamples); + sampleCount -= offSamples; + // Set size directly to avoid reallocations + target.resize(sampleCount); + + // Data samples are in [0,1] (as long as the voltageLimits are set correctly). + // The offset needs to be applied now, as well as the gain. + const float timeFactor = + float(channelData.voltage.interval * DIVS_TIME * 2 / m_deviceSettings->samplerate().timebase); + const float offY = (float)channelData.channelSettings->voltage()->offset() * DIVS_VOLTAGE / 2; + const float offX = -DIVS_TIME / 2; + const int invert = channelData.channelSettings->inverted() ? -1.0 : 1.0; + const float gain = invert / channelData.channelSettings->gain(); + +#pragma omp parallel for for (unsigned int position = 0; position < sampleCount; ++position) { - target.push_back(QVector3D(position * horizontalFactor - DIVS_TIME / 2, - (float)*(dataIterator++) / gain * invert + offset, 0.0)); + const float v = (float)source[position + offSamples]; + target[position] = (QVector3D(position * timeFactor + offX, v * gain + offY, 0.0)); } } } void GraphGenerator::generateGraphsTYspectrum(PPresult *result) { - ready = true; - result->vaChannelSpectrum.resize(scope->spectrum.size()); - for (ChannelID channel = 0; channel < scope->voltage.size(); ++channel) { - ChannelGraph &target = result->vaChannelSpectrum[channel]; - const SampleValues &samples = useSpecSamplesOf(channel, result, scope); + for (DataChannel &channelData : *result) { + ChannelGraph &target = channelData.spectrum.graph; + const std::vector &source = channelData.spectrum.sample; // Check if this channel is used and available at the data analyzer - if (samples.sample.empty()) { + if (source.empty()) { // Delete all vector arrays target.clear(); continue; } // Check if the sample count has changed - size_t sampleCount = samples.sample.size(); + size_t sampleCount = source.size(); if (sampleCount > 500000) { qWarning() << "Sample count too high!"; throw new std::runtime_error("Sample count too high!"); } - size_t neededSize = sampleCount * 2; // Set size directly to avoid reallocations - target.reserve(neededSize); + target.resize(sampleCount); // What's the horizontal distance between sampling points? - float horizontalFactor = (float)(samples.interval / scope->horizontal.frequencybase); + const float timeFactor = (float)(channelData.spectrum.interval / m_scope->frequencybase()); - // Fill vector array - std::vector::const_iterator dataIterator = samples.sample.begin(); - const float magnitude = (float)scope->spectrum[channel].magnitude; - const float offset = (float)scope->spectrum[channel].offset; + const float magnitude = (float)channelData.channelSettings->spectrum()->magnitude(); + const float offY = (float)channelData.channelSettings->spectrum()->offset() * DIVS_VOLTAGE / 2; + const float offX = -DIVS_TIME / 2; +#pragma omp parallel for for (unsigned int position = 0; position < sampleCount; ++position) { - target.push_back(QVector3D(position * horizontalFactor - DIVS_TIME / 2, - (float)*(dataIterator++) / magnitude + offset, 0.0)); + const float v = (float)source[position]; + target[position] = (QVector3D(position * timeFactor + offX, v / magnitude + offY, 0.0)); } } } void GraphGenerator::process(PPresult *data) { - if (scope->horizontal.format == Dso::GraphFormat::TY) { - ready = true; + if (m_scope->format() == Dso::GraphFormat::TY) { generateGraphsTYspectrum(data); generateGraphsTYvoltage(data); } else - generateGraphsXY(data, scope); + generateGraphsXY(data, m_scope); } -void GraphGenerator::generateGraphsXY(PPresult *result, const DsoSettingsScope *scope) { - result->vaChannelVoltage.resize(scope->voltage.size()); +void GraphGenerator::generateGraphsXY(PPresult *result, const ::Settings::Scope *scope) { + ChannelID xChannel; + DataChannel *lastChannel = nullptr; - // Delete all spectrum graphs - for (ChannelGraph &data : result->vaChannelSpectrum) data.clear(); + for (DataChannel &channelData : *result) { + // Delete all spectrum graphs + channelData.spectrum.graph.clear(); + channelData.voltage.graph.clear(); - // Generate voltage graphs for pairs of channels - for (ChannelID channel = 0; channel < scope->voltage.size(); channel += 2) { - // We need pairs of channels. - if (channel + 1 == scope->voltage.size()) { - result->vaChannelVoltage[channel].clear(); + // Generate voltage graphs for pairs of channels + if (!lastChannel) { + lastChannel = &channelData; + xChannel = channelData.channelID; continue; } - const ChannelID xChannel = channel; - const ChannelID yChannel = channel + 1; + DataChannel *thisChannel = &channelData; - const SampleValues &xSamples = useVoltSamplesOf(xChannel, result, scope); - const SampleValues &ySamples = useVoltSamplesOf(yChannel, result, scope); + ChannelGraph &target = lastChannel->voltage.graph; + const std::vector &xSamples = lastChannel->voltage.sample; + const std::vector &ySamples = thisChannel->voltage.sample; + const ::Settings::Channel *xSettings = lastChannel->channelSettings.get(); + const ::Settings::Channel *ySettings = thisChannel->channelSettings.get(); // The channels need to be active - if (!xSamples.sample.size() || !ySamples.sample.size()) { - result->vaChannelVoltage[channel].clear(); - result->vaChannelVoltage[channel + 1].clear(); + if (!xSamples.size() || !ySamples.size()) { + lastChannel->voltage.graph.clear(); + thisChannel->voltage.graph.clear(); continue; } // Check if the sample count has changed - const size_t sampleCount = std::min(xSamples.sample.size(), ySamples.sample.size()); - ChannelGraph &drawLines = result->vaChannelVoltage[channel]; - drawLines.reserve(sampleCount * 2); + const size_t sampleCount = std::min(xSamples.size(), ySamples.size()); + target.resize(sampleCount * 2); // Fill vector array - std::vector::const_iterator xIterator = xSamples.sample.begin(); - std::vector::const_iterator yIterator = ySamples.sample.begin(); - const double xGain = scope->gain(xChannel); - const double yGain = scope->gain(yChannel); - const double xOffset = scope->voltage[xChannel].offset; - const double yOffset = scope->voltage[yChannel].offset; - const double xInvert = scope->voltage[xChannel].inverted ? -1.0 : 1.0; - const double yInvert = scope->voltage[yChannel].inverted ? -1.0 : 1.0; - + const double xGain = m_deviceSettings->spec->gain[xSettings->voltage()->gainStepIndex()].gain; + const double yGain = m_deviceSettings->spec->gain[ySettings->voltage()->gainStepIndex()].gain; + const double xOffset = ((float)xSettings->voltage()->offset() / DIVS_VOLTAGE) + 0.5f; + const double yOffset = ((float)ySettings->voltage()->offset() / DIVS_VOLTAGE) + 0.5f; + const double xInvert = xSettings->inverted() ? -1.0 : 1.0; + const double yInvert = ySettings->inverted() ? -1.0 : 1.0; + +#pragma omp parallel for for (unsigned int position = 0; position < sampleCount; ++position) { - drawLines.push_back(QVector3D((float)(*(xIterator++) / xGain * xInvert + xOffset), - (float)(*(yIterator++) / yGain * yInvert + yOffset), 0.0)); + target[position] = (QVector3D((float)(xSamples[position] / xGain * xInvert + xOffset), + (float)(ySamples[position] / yGain * yInvert + yOffset), 0.0)); } + + // Wait for another pair of channels + lastChannel = nullptr; } } +} diff --git a/openhantek/src/post/graphgenerator.h b/openhantek/src/post/graphgenerator.h index 1642a896..73eaef73 100644 --- a/openhantek/src/post/graphgenerator.h +++ b/openhantek/src/post/graphgenerator.h @@ -11,32 +11,37 @@ #include "hantekprotocol/types.h" #include "processor.h" -struct DsoSettingsScope; +namespace Settings { +class Scope; +} class PPresult; namespace Dso { -struct ControlSpecification; +class DeviceSettings; +class ChannelUsage; +struct ModelSpec; } +namespace PostProcessing { /// \brief Generates ready to be used vertex arrays class GraphGenerator : public QObject, public Processor { Q_OBJECT public: - GraphGenerator(const DsoSettingsScope *scope, bool isSoftwareTriggerDevice); - void generateGraphsXY(PPresult *result, const DsoSettingsScope *scope); - - bool isReady() const; + GraphGenerator(const ::Settings::Scope *m_scope, const Dso::DeviceSettings *m_deviceSettings, + const Dso::ChannelUsage *channelUsage); + void generateGraphsXY(PPresult *result, const ::Settings::Scope *m_scope); private: void generateGraphsTYvoltage(PPresult *result); void generateGraphsTYspectrum(PPresult *result); private: - bool ready = false; - const DsoSettingsScope *scope; - const bool isSoftwareTriggerDevice; + const ::Settings::Scope *m_scope; + const Dso::DeviceSettings *m_deviceSettings; + const Dso::ChannelUsage *m_channelUsage; // Processor interface - private: + private: virtual void process(PPresult *) override; }; +} diff --git a/openhantek/src/post/mathchannelgenerator.cpp b/openhantek/src/post/mathchannelgenerator.cpp index 233fa494..27679f16 100644 --- a/openhantek/src/post/mathchannelgenerator.cpp +++ b/openhantek/src/post/mathchannelgenerator.cpp @@ -1,47 +1,53 @@ +// SPDX-License-Identifier: GPL-2.0+ + #include "mathchannelgenerator.h" -#include "scopesettings.h" -#include "post/postprocessingsettings.h" #include "enums.h" +#include "post/postprocessingsettings.h" +#include "scopesettings.h" +#include "utils/getwithdefault.h" -MathChannelGenerator::MathChannelGenerator(const DsoSettingsScope *scope, unsigned physicalChannels) - : physicalChannels(physicalChannels), scope(scope) {} +namespace PostProcessing { + +MathChannelGenerator::MathChannelGenerator(const ::Settings::Scope *scope) : scope(scope) {} MathChannelGenerator::~MathChannelGenerator() {} void MathChannelGenerator::process(PPresult *result) { - bool channelsHaveData = !result->data(0)->voltage.sample.empty() && !result->data(1)->voltage.sample.empty(); - if (!channelsHaveData) return; - - for (ChannelID channel = physicalChannels; channel < result->channelCount(); ++channel) { - DataChannel *const channelData = result->modifyData(channel); - + for (std::pair> item : scope->channels()) { + ::Settings::Channel *channel = item.second.get(); // Math channel enabled? - if (!scope->voltage[channel].used && !scope->spectrum[channel].used) continue; + if (!channel->isMathChannel() || !channel->anyVisible()) continue; - // Set sampling interval - channelData->voltage.interval = result->data(0)->voltage.interval; + const ::Settings::MathChannel *mathChannel = static_cast(channel); + if (mathChannel->firstID() == ::Settings::Channel::INVALID || + mathChannel->secondID() == ::Settings::Channel::INVALID) + continue; + + SampleValues &targetVoltage = result->addChannel(channel->channelID(), false, item.second)->voltage; + const SampleValues &firstSampleValues = result->data(mathChannel->firstID())->voltage; + const std::vector &firstChannel = firstSampleValues.sample; + const std::vector &secondChannel = result->data(mathChannel->secondID())->voltage.sample; // Resize the sample vector - std::vector &resultData = channelData->voltage.sample; - resultData.resize(std::min(result->data(0)->voltage.sample.size(), result->data(1)->voltage.sample.size())); + targetVoltage.interval = firstSampleValues.interval; + std::vector &resultData = targetVoltage.sample; + resultData.resize(std::min(firstChannel.size(), secondChannel.size())); // Calculate values and write them into the sample buffer - std::vector::const_iterator ch1Iterator = result->data(0)->voltage.sample.begin(); - std::vector::const_iterator ch2Iterator = result->data(1)->voltage.sample.begin(); - for (std::vector::iterator it = resultData.begin(); it != resultData.end(); ++it) { - switch (Dso::getMathMode(scope->voltage[physicalChannels])) { - case Dso::MathMode::ADD_CH1_CH2: - *it = *ch1Iterator + *ch2Iterator; - break; - case Dso::MathMode::SUB_CH2_FROM_CH1: - *it = *ch1Iterator - *ch2Iterator; - break; - case Dso::MathMode::SUB_CH1_FROM_CH2: - *it = *ch2Iterator - *ch1Iterator; - break; - } - ++ch1Iterator; - ++ch2Iterator; + switch (mathChannel->mathMode()) { + case PostProcessing::MathMode::ADD: + // #pragma omp parallel for + for (unsigned int i = 0; i < resultData.size(); ++i) resultData[i] = firstChannel[i] + secondChannel[i]; + break; + case PostProcessing::MathMode::SUBSTRACT: + // #pragma omp parallel for + for (unsigned int i = 0; i < resultData.size(); ++i) resultData[i] = firstChannel[i] - secondChannel[i]; + break; + case PostProcessing::MathMode::MULTIPLY: + // #pragma omp parallel for + for (unsigned int i = 0; i < resultData.size(); ++i) resultData[i] = firstChannel[i] * secondChannel[i]; + break; } } } +} diff --git a/openhantek/src/post/mathchannelgenerator.h b/openhantek/src/post/mathchannelgenerator.h index 7195e15a..f776e081 100644 --- a/openhantek/src/post/mathchannelgenerator.h +++ b/openhantek/src/post/mathchannelgenerator.h @@ -4,16 +4,21 @@ #include "processor.h" -struct DsoSettingsScope; +namespace Settings { +class Scope; +} class PPresult; +namespace PostProcessing { + class MathChannelGenerator : public Processor { public: - MathChannelGenerator(const DsoSettingsScope *scope, unsigned physicalChannels); + MathChannelGenerator(const ::Settings::Scope *scope); virtual ~MathChannelGenerator(); virtual void process(PPresult *) override; private: - const unsigned physicalChannels; - const DsoSettingsScope *scope; + const ::Settings::Scope *scope; }; + +} diff --git a/openhantek/src/post/postprocessing.cpp b/openhantek/src/post/postprocessing.cpp index b78f7b33..8e1db694 100644 --- a/openhantek/src/post/postprocessing.cpp +++ b/openhantek/src/post/postprocessing.cpp @@ -1,29 +1,62 @@ +// SPDX-License-Identifier: GPL-2.0+ + #include "postprocessing.h" +#include "settings/scopesettings.h" +#include + +namespace PostProcessing { -PostProcessing::PostProcessing(unsigned channelCount) : channelCount(channelCount) { +Executor::Executor(const ::Settings::Scope *scope) : m_scope(scope) { qRegisterMetaType>(); + // Call constructor for all datapool ppresults. + for (unsigned i = 0; i < DATAPOOLSIZE; ++i) { new (&resultPool[i]) PPresult(); } } -void PostProcessing::registerProcessor(Processor *processor) { processors.push_back(processor); } +void Executor::registerProcessor(Processor *processor) { processors.push_back(processor); } -void PostProcessing::convertData(const DSOsamples *source, PPresult *destination) { +inline void convertData(const DSOsamples *source, PPresult *destination, const ::Settings::Scope *scope) { QReadLocker locker(&source->lock); - for (ChannelID channel = 0; channel < source->data.size(); ++channel) { - const std::vector &rawChannelData = source->data.at(channel); + for (unsigned index = 0; index < source->channelCount(); ++index) { + auto &v = source->data[index]; - if (rawChannelData.empty()) { continue; } + if (v.id == (unsigned)-1 || v.empty()) { continue; } - DataChannel *const channelData = destination->modifyData(channel); + // We also create a new shared_ptr reference to the underlying channelsettings. This way we do not get + // suprises if the user removes a math_channel during processing. + DataChannel *const channelData = destination->addChannel(v.id, true, scope->channel(v.id)); channelData->voltage.interval = 1.0 / source->samplerate; - channelData->voltage.sample = rawChannelData; + // Data copy + channelData->voltage.sample = v; + channelData->maxVoltage = v.maxVoltage; + channelData->minVoltage = v.minVoltage; + channelData->maxRaw = v.maxRaw; + channelData->minRaw = v.minRaw; } } -void PostProcessing::input(const DSOsamples *data) { - currentData.reset(new PPresult(channelCount)); - convertData(data, currentData.get()); - for (Processor *p : processors) p->process(currentData.get()); - std::shared_ptr res = std::move(currentData); - emit processingFinished(res); +void Executor::input(const DSOsamples *data) { + PPresult *result = nullptr; + // Find a free PPresult in the PPresult pool + for (unsigned i = 0; i < DATAPOOLSIZE; ++i) { + PPresult *p = &resultPool[i]; + if (!p->inUse) { + p->softwareTriggerTriggered = false; + p->removeNonDeviceChannels(); + p->inUse = true; + result = p; + break; + } + } + // Nothing found: abort + if (!result) { + qWarning() << "Sampleset skipped. Too busy!"; + return; + } + + convertData(data, result, m_scope); + for (Processor *p : processors) p->process(result); + // Create a shared_ptr with a custom "delete" function that resets inUse. + emit processingFinished(std::shared_ptr(result, [](PPresult *r) { r->inUse = false; })); +} } diff --git a/openhantek/src/post/postprocessing.h b/openhantek/src/post/postprocessing.h index 56e4f7ea..c6d7d23a 100644 --- a/openhantek/src/post/postprocessing.h +++ b/openhantek/src/post/postprocessing.h @@ -10,17 +10,21 @@ #include -struct DsoSettingsScope; +namespace Settings { +class Scope; +} + +namespace PostProcessing { /** * Manages all post processing processors. Register another processor with `registerProcessor(p)`. * All processors, in the order of insertion, will process the input data, given by `input(data)`. * The final result will be made available via the `processingFinished` signal. */ -class PostProcessing : public QObject { +class Executor : public QObject { Q_OBJECT public: - PostProcessing(unsigned channelCount); + Executor(const ::Settings::Scope* scope); /** * Adds a new processor that is called when a new input arrived. The order of the processors is * imporant. The first added processor will be called first. This class does not take ownership @@ -29,15 +33,14 @@ class PostProcessing : public QObject { */ void registerProcessor(Processor *processor); - private: - /// A new `PPresult` is created for each new input. We need to know the channel size. - const unsigned channelCount; /// The list of processors. Processors are not memory managed by this class. std::vector processors; - /// - std::unique_ptr currentData; - static void convertData(const DSOsamples *source, PPresult *destination); + enum { DATAPOOLSIZE = 10 }; + /// Result pool. We want to reduce allocations/deallocations and reuse PPresult structures + PPresult resultPool[DATAPOOLSIZE]; + + const ::Settings::Scope* m_scope; public slots: /** * Start processing new data. The actual data may be processed in another thread if you have moved @@ -45,8 +48,8 @@ class PostProcessing : public QObject { * @param data */ void input(const DSOsamples *data); -signals: + signals: void processingFinished(std::shared_ptr result); }; - +} Q_DECLARE_METATYPE(std::shared_ptr) diff --git a/openhantek/src/post/postprocessingsettings.cpp b/openhantek/src/post/postprocessingsettings.cpp index 7fef5f95..10704d94 100644 --- a/openhantek/src/post/postprocessingsettings.cpp +++ b/openhantek/src/post/postprocessingsettings.cpp @@ -1,63 +1,24 @@ -#include "postprocessingsettings.h" - -#include -#include +// SPDX-License-Identifier: GPL-2.0+ -namespace Dso { +#include "postprocessingsettings.h" +#include "utils/enumhelper.h" -Enum MathModeEnum; -Enum WindowFunctionEnum; +#include -/// \brief Return string representation of the given math mode. -/// \param mode The ::MathMode that should be returned as string. -/// \return The string that should be used in labels etc. -QString mathModeString(MathMode mode) { - switch (mode) { - case MathMode::ADD_CH1_CH2: - return QCoreApplication::tr("CH1 + CH2"); - case MathMode::SUB_CH2_FROM_CH1: - return QCoreApplication::tr("CH1 - CH2"); - case MathMode::SUB_CH1_FROM_CH2: - return QCoreApplication::tr("CH2 - CH1"); - } - return QString(); -} -/// \brief Return string representation of the given dft window function. -/// \param window The ::WindowFunction that should be returned as string. -/// \return The string that should be used in labels etc. -QString windowFunctionString(WindowFunction window) { - switch (window) { - case WindowFunction::RECTANGULAR: - return QCoreApplication::tr("Rectangular"); - case WindowFunction::HAMMING: - return QCoreApplication::tr("Hamming"); - case WindowFunction::HANN: - return QCoreApplication::tr("Hann"); - case WindowFunction::COSINE: - return QCoreApplication::tr("Cosine"); - case WindowFunction::LANCZOS: - return QCoreApplication::tr("Lanczos"); - case WindowFunction::BARTLETT: - return QCoreApplication::tr("Bartlett"); - case WindowFunction::TRIANGULAR: - return QCoreApplication::tr("Triangular"); - case WindowFunction::GAUSS: - return QCoreApplication::tr("Gauss"); - case WindowFunction::BARTLETTHANN: - return QCoreApplication::tr("Bartlett-Hann"); - case WindowFunction::BLACKMAN: - return QCoreApplication::tr("Blackman"); - // case WindowFunction::WINDOW_KAISER: - // return QCoreApplication::tr("Kaiser"); - case WindowFunction::NUTTALL: - return QCoreApplication::tr("Nuttall"); - case WindowFunction::BLACKMANHARRIS: - return QCoreApplication::tr("Blackman-Harris"); - case WindowFunction::BLACKMANNUTTALL: - return QCoreApplication::tr("Blackman-Nuttall"); - case WindowFunction::FLATTOP: - return QCoreApplication::tr("Flat top"); - } - return QString(); +namespace PostProcessing { +void SettingsIO::read(QSettings *store, Settings &post) { + store->beginGroup("postprocessing"); + post.m_spectrumLimit = store->value("imageSize", post.m_spectrumLimit).toDouble(); + post.m_spectrumReference = store->value("exportSizeBytes", post.m_spectrumReference).toUInt(); + post.m_spectrumWindow = loadForEnum(store, "spectrumWindow", post.m_spectrumWindow); + store->endGroup(); } + +void SettingsIO::write(QSettings *store, const Settings &post) { + store->beginGroup("postprocessing"); + store->setValue("spectrumLimit", post.m_spectrumLimit); + store->setValue("spectrumReference", post.m_spectrumReference); + store->setValue("spectrumWindow", enumName(post.m_spectrumWindow)); + store->endGroup(); } +} // end namespace PostProcessing diff --git a/openhantek/src/post/postprocessingsettings.h b/openhantek/src/post/postprocessingsettings.h index 0ed3d4f1..d4b4fef7 100644 --- a/openhantek/src/post/postprocessingsettings.h +++ b/openhantek/src/post/postprocessingsettings.h @@ -1,49 +1,30 @@ -#pragma once +// SPDX-License-Identifier: GPL-2.0+ -#include "utils/enumclass.h" -#include -namespace Dso { +#pragma once -/// \enum MathMode -/// \brief The different math modes for the math-channel. -enum class MathMode : unsigned { ADD_CH1_CH2, SUB_CH2_FROM_CH1, SUB_CH1_FROM_CH2 }; -extern Enum MathModeEnum; +#include "enums.h" -template -inline MathMode getMathMode(T& t) { return (MathMode)t.couplingOrMathIndex; } +class QSettings; +class DsoConfigAnalysisPage; -/// \enum WindowFunction -/// \brief The supported window functions. -/// These are needed for spectrum analysis and are applied to the sample values -/// before calculating the DFT. -enum class WindowFunction : int { - RECTANGULAR, ///< Rectangular window (aka Dirichlet) - HAMMING, ///< Hamming window - HANN, ///< Hann window - COSINE, ///< Cosine window (aka Sine) - LANCZOS, ///< Lanczos window (aka Sinc) - BARTLETT, ///< Bartlett window (Endpoints == 0) - TRIANGULAR, ///< Triangular window (Endpoints != 0) - GAUSS, ///< Gauss window (simga = 0.4) - BARTLETTHANN, ///< Bartlett-Hann window - BLACKMAN, ///< Blackman window (alpha = 0.16) - // KAISER, ///< Kaiser window (alpha = 3.0) - NUTTALL, ///< Nuttall window, cont. first deriv. - BLACKMANHARRIS, ///< Blackman-Harris window - BLACKMANNUTTALL, ///< Blackman-Nuttall window - FLATTOP ///< Flat top window -}; -extern Enum WindowFunctionEnum; +namespace PostProcessing { +struct Settings { + friend class ::DsoConfigAnalysisPage; + friend struct SettingsIO; -QString mathModeString(MathMode mode); -QString windowFunctionString(WindowFunction window); -} + inline ::PostProcessing::WindowFunction spectrumWindow() const { return m_spectrumWindow; } + inline double spectrumReference() const { return m_spectrumReference; } + inline double spectrumLimit() const { return m_spectrumLimit; } -Q_DECLARE_METATYPE(Dso::MathMode) -Q_DECLARE_METATYPE(Dso::WindowFunction) + private: + ::PostProcessing::WindowFunction m_spectrumWindow = // + ::PostProcessing::WindowFunction::HANN; ///< Window function for DFT + double m_spectrumReference = 0.0; ///< Reference level for spectrum in dBm + double m_spectrumLimit = -20.0; ///< Minimum magnitude of the spectrum (Avoids peaks) +}; -struct DsoSettingsPostProcessing { - Dso::WindowFunction spectrumWindow = Dso::WindowFunction::HANN; ///< Window function for DFT - double spectrumReference = 0.0; ///< Reference level for spectrum in dBm - double spectrumLimit = -20.0; ///< Minimum magnitude of the spectrum (Avoids peaks) +struct SettingsIO { + static void read(QSettings *io, Settings &post); + static void write(QSettings *io, const Settings &post); }; +} diff --git a/openhantek/src/post/ppresult.cpp b/openhantek/src/post/ppresult.cpp index ae323f96..e21a28c5 100644 --- a/openhantek/src/post/ppresult.cpp +++ b/openhantek/src/post/ppresult.cpp @@ -1,33 +1,30 @@ // SPDX-License-Identifier: GPL-2.0+ #include "ppresult.h" +#include "utils/getwithdefault.h" #include #include -PPresult::PPresult(unsigned int channelCount) { analyzedData.resize(channelCount); } +PPresult::PPresult() : inUse(false) {} -const DataChannel *PPresult::data(ChannelID channel) const { - if (channel >= this->analyzedData.size()) return 0; - - return &this->analyzedData[(size_t)channel]; +const DataChannel *PPresult::data(ChannelID channelID) const { + auto it = analyzedData.find(channelID); + if (it == analyzedData.end()) return nullptr; + return &it->second; } -DataChannel *PPresult::modifyData(ChannelID channel) { return &this->analyzedData[(size_t)channel]; } - -unsigned int PPresult::sampleCount() const { return (unsigned)analyzedData[0].voltage.sample.size(); } - -unsigned int PPresult::channelCount() const { return (unsigned)analyzedData.size(); } - -double DataChannel::computeAmplitude() const { - double minimalVoltage, maximalVoltage; - minimalVoltage = maximalVoltage = voltage.sample[0]; - - for (unsigned int position = 1; position < voltage.sample.size(); ++position) { - if (voltage.sample[position] < minimalVoltage) - minimalVoltage = voltage.sample[position]; - else if (voltage.sample[position] > maximalVoltage) - maximalVoltage = voltage.sample[position]; - } +DataChannel *PPresult::modifyData(ChannelID channelID) { + auto it = analyzedData.find(channelID); + if (it == analyzedData.end()) return nullptr; + return &it->second; +} - return maximalVoltage - minimalVoltage; +DataChannel *PPresult::addChannel(ChannelID channelID, bool deviceChannel, + std::shared_ptr channelSettings) { + auto it = analyzedData.find(channelID); + if (it != analyzedData.end()) return &it->second; + analyzedData.insert(std::make_pair(channelID, DataChannel(channelID, deviceChannel, channelSettings))); + return &analyzedData.find(channelID)->second; } + +unsigned int PPresult::sampleCount() const { return (unsigned)analyzedData.begin()->second.voltage.sample.size(); } diff --git a/openhantek/src/post/ppresult.h b/openhantek/src/post/ppresult.h index 16b80221..4bfe5e75 100644 --- a/openhantek/src/post/ppresult.h +++ b/openhantek/src/post/ppresult.h @@ -2,50 +2,90 @@ #pragma once -#include #include +#include -#include #include "hantekprotocol/types.h" +#include "settings/scopechannel.h" +#include "utils/getwithdefault.h" +#include +#include + +typedef std::vector ChannelGraph; /// \brief Struct for a array of sample values. struct SampleValues { std::vector sample; ///< Vector holding the sampling data double interval = 0.0; ///< The interval between two sample values + ChannelGraph graph; }; /// \brief Struct for the analyzed data. struct DataChannel { - SampleValues voltage; ///< The time-domain voltage levels (V) - SampleValues spectrum; ///< The frequency-domain power levels (dB) + SampleValues voltage; ///< The time-domain voltage levels (V) + SampleValues spectrum; ///< The frequency-domain power levels (dB) + std::shared_ptr channelSettings; - double frequency = 0.0; ///< The frequency of the signal - // Calculate peak-to-peak voltage - double computeAmplitude() const; + ChannelID channelID; + double frequency = 0.0; ///< The frequency of the signal + double minVoltage = 0.0; ///< minimum voltage + double maxVoltage = 0.0; /// &channelSettings) + : channelSettings(channelSettings), channelID(channelID), deviceChannel(deviceChannel) {} }; -typedef std::vector ChannelGraph; -typedef std::vector ChannelsGraphs; - /// Post processing results class PPresult { public: - PPresult(unsigned int channelCount); + PPresult(); + PPresult(const PPresult &) = delete; + using DataChannelMap = std::map; + using iterator = map_iterator; + using const_iterator = map_iterator; /// \brief Returns the analyzed data. /// \param channel Channel, whose data should be returned. - const DataChannel *data(ChannelID channel) const; + const DataChannel *data(ChannelID channelID) const; + /// \brief Returns the analyzed data. The data structure can be modifed. /// \param channel Channel, whose data should be returned. - DataChannel *modifyData(ChannelID channel); + DataChannel *modifyData(ChannelID channelID); + + /// Add another channel (generated by a post processor) to the result + DataChannel *addChannel(ChannelID channelID, bool deviceChannel, + std::shared_ptr channelSettings); + + /// Channels in this post processing result + unsigned channelCount() const { return (unsigned)analyzedData.size(); } + /// \return The maximum sample count of the last analyzed data. This assumes there is at least one channel. unsigned int sampleCount() const; - unsigned int channelCount() const; + + /// Removes all channels that are not device channels (for instance math channels) + void removeNonDeviceChannels() { + for (auto it = analyzedData.begin(); it != analyzedData.end(); ++it) { + if (!it->second.deviceChannel) it = analyzedData.erase(it); + } + } + + iterator begin() { return make_map_iterator(analyzedData.begin()); } + iterator end() { return make_map_iterator(analyzedData.end()); } + const_iterator begin() const { return make_map_const_iterator(analyzedData.begin()); } + const_iterator end() const { return make_map_const_iterator(analyzedData.end()); } + + /// If inUse is false, this PPresult is ready to be used from the PPresult data pool. + /// When aquired from the data pool, inUse will be set to true. + /// As soon as the last consumer is done with it (shared_ptr delete function called), + /// inUse will be set to false again. + std::atomic inUse; bool softwareTriggerTriggered = false; - ChannelsGraphs vaChannelSpectrum; - ChannelsGraphs vaChannelVoltage; private: - std::vector analyzedData; ///< The analyzed data for each channel + DataChannelMap analyzedData; ///< The analyzed data for each channel }; diff --git a/openhantek/src/post/processor.h b/openhantek/src/post/processor.h index 71da37b8..5bf8da65 100644 --- a/openhantek/src/post/processor.h +++ b/openhantek/src/post/processor.h @@ -1,8 +1,14 @@ +// SPDX-License-Identifier: GPL-2.0+ + #pragma once #include "ppresult.h" +namespace PostProcessing { + class Processor { public: virtual void process(PPresult*) = 0; }; + +} diff --git a/openhantek/src/post/readme.md b/openhantek/src/post/readme.md index 4d03070f..6fdc2a13 100644 --- a/openhantek/src/post/readme.md +++ b/openhantek/src/post/readme.md @@ -7,4 +7,4 @@ This directory contains post processing algorithms, namely # Dependency * Files in this directory depend on structs in the `hantekprotocol` folder. -* Classes in here probably depend on the user settings (../viewsetting.h, ../scopesetting.h) +* Classes in here probably depend on user settings (../settings/) diff --git a/openhantek/src/post/selfcalibration.cpp b/openhantek/src/post/selfcalibration.cpp new file mode 100644 index 00000000..c0be1b2f --- /dev/null +++ b/openhantek/src/post/selfcalibration.cpp @@ -0,0 +1,67 @@ +// SPDX-License-Identifier: GPL-2.0+ + +#include "selfcalibration.h" +#include "dsocontrol.h" + +SelfCalibration::SelfCalibration(DsoControl *dsocontrol) + : m_dsocontrol(dsocontrol), m_spec(const_cast(dsocontrol->specification())) {} + +void SelfCalibration::start(ChannelID channelID) { + if (m_isRunning) return; + m_isRunning = true; + m_currentHardwareGainStep = 0; + m_isFirstSet = true; + m_channelID = channelID; + emit runningChanged(m_isRunning); +} + +void SelfCalibration::cancel() { + if (!m_isRunning) return; + m_isRunning = false; + emit runningChanged(m_isRunning); +} + +void SelfCalibration::process(PPresult *data) { + if (!m_isRunning) return; + if (m_currentHardwareGainStep > m_spec->gain.size()) { cancel(); } + + if (m_dsocontrol->deviceSettings()->voltage[m_channelID]->gainStepIndex() != m_currentHardwareGainStep) { + if (m_dsocontrol->setGain(m_channelID, m_currentHardwareGainStep, true) != Dso::ErrorCode::NONE) { cancel(); } + return; + } + + const DataChannel *channelData = data->data(0); + QString task = valueToString(m_spec->gain[m_currentHardwareGainStep].gain, Unit::VOLTS); + + if (m_isFirstSet) { + m_isFirstSet = false; + minRaw = channelData->minRaw; + maxRaw = channelData->maxRaw; + minVoltage = channelData->minVoltage; + maxVoltage = channelData->maxVoltage; + emit progress((m_currentHardwareGainStep * 2 + 0) / m_spec->gain.size() * 2, task); + return; + } + m_isFirstSet = true; + emit progress((m_currentHardwareGainStep * 2 + 1) / m_spec->gain.size() * 2, task); + + // Compute average value + minRaw = uint16_t(unsigned(minRaw + channelData->minRaw) / 2); + maxRaw = uint16_t(unsigned(maxRaw + channelData->maxRaw) / 2); + minVoltage = (minVoltage + channelData->minVoltage) / 2; + maxVoltage = (maxVoltage + channelData->maxVoltage) / 2; + + // Compute offset + double &oldOffset = m_spec->calibration[m_channelID][m_currentHardwareGainStep].offsetCorrection; + oldOffset = minVoltage; + + // Compute gain limit / normalise factor. + // double samplePoint = (v / limit - harwareOffset) * gainStep - offsetCorrection; + const double hardwareGain = m_spec->gain[m_currentHardwareGainStep].gain; + double newLimit = hardwareGain * (maxRaw - minRaw); + double &oldLimiz = m_spec->calibration[m_channelID][m_currentHardwareGainStep].voltageLimit; + oldLimiz = newLimit; + + ++m_currentHardwareGainStep; + if (m_currentHardwareGainStep >= m_spec->gain.size()) { cancel(); } +} diff --git a/openhantek/src/post/selfcalibration.h b/openhantek/src/post/selfcalibration.h new file mode 100644 index 00000000..55b3361a --- /dev/null +++ b/openhantek/src/post/selfcalibration.h @@ -0,0 +1,38 @@ +// SPDX-License-Identifier: GPL-2.0+ + +#pragma once +#include "processor.h" +#include + +class DsoControl; +namespace Dso { +struct ModelSpec; +} + +class SelfCalibration : public QObject, public PostProcessing::Processor { + Q_OBJECT + public: + SelfCalibration(DsoControl *m_dsocontrol); + void start(ChannelID channelID); + void cancel(); + + protected: + virtual void process(PPresult *) override; + + unsigned m_currentHardwareGainStep = 0; + bool m_isRunning = false; + DsoControl *m_dsocontrol; + Dso::ModelSpec *m_spec; + bool m_isFirstSet; + ChannelID m_channelID; + + // We use two sample-sets to build an average. Save values of first sample-set. + double minVoltage; + double maxVoltage; + uint16_t minRaw; + uint16_t maxRaw; + + signals: + void runningChanged(bool m_isRunning); + void progress(double progress, const QString &task); +}; diff --git a/openhantek/src/post/softwaretrigger.cpp b/openhantek/src/post/softwaretrigger.cpp index 19ecc964..493e8e2f 100644 --- a/openhantek/src/post/softwaretrigger.cpp +++ b/openhantek/src/post/softwaretrigger.cpp @@ -1,27 +1,47 @@ +// SPDX-License-Identifier: GPL-2.0+ + #include "post/softwaretrigger.h" +#include "channelusage.h" #include "post/ppresult.h" #include "scopesettings.h" -#include "viewconstants.h" #include "utils/printutils.h" +#include "viewconstants.h" + +namespace PostProcessing { +typedef bool (*opcmp)(double, double, double); +typedef bool (*smplcmp)(double, double); + +static opcmp posOpcmp = [](double value, double level, double prev) { return value > level && prev <= level; }; +static smplcmp posSmplcmp = [](double sampleK, double value) { return sampleK >= value; }; + +static opcmp negOpcmp = [](double value, double level, double prev) { return value < level && prev >= level; }; +static smplcmp negSmplcmp = [](double sampleK, double value) { return sampleK < value; }; SoftwareTrigger::PrePostStartTriggerSamples SoftwareTrigger::compute(const PPresult *data, - const DsoSettingsScope *scope) -{ + const Dso::DeviceSettings *control, + const Settings::Scope *scope, + const Dso::ChannelUsage *channelUsage) { unsigned int preTrigSamples = 0; unsigned int postTrigSamples = 0; unsigned int swTriggerStart = 0; - ChannelID channel = scope->trigger.source; + ChannelID channel = control->trigger.source(); // Trigger channel not in use - if (!scope->voltage[channel].used || !data->data(channel) || - data->data(channel)->voltage.sample.empty()) + if (!channelUsage->isUsed(channel) || !data->data(channel) || data->data(channel)->voltage.sample.empty()) return PrePostStartTriggerSamples(preTrigSamples, postTrigSamples, swTriggerStart); - const std::vector& samples = data->data(channel)->voltage.sample; - double level = scope->voltage[channel].trigger; + const double gain = scope->channel(channel)->gain(); + const std::vector &samples = data->data(channel)->voltage.sample; + // Trigger level is in range [-1,1] -> map to [-DIV_VOLTAGE/2,DIV_VOLTAGE/2] + double level = control->voltage[channel]->triggerLevel() * DIVS_VOLTAGE / 2; + // The raw signal is in range [-1,1] as well but the user may have applied a gain factor other than 1V + // and is using an offset. We need to compensate and adjust the level by the current gain factor and offset. + level -= control->voltage[channel]->offset() * DIVS_VOLTAGE / 2; + level *= gain; size_t sampleCount = samples.size(); - double timeDisplay = scope->horizontal.timebase * DIVS_TIME; - double samplesDisplay = timeDisplay * scope->horizontal.samplerate; + // Not the entire wave is visible at a time, only a DIVS_TIME part + double timeDisplay = control->samplerate().timebase / DIVS_TIME; + double samplesDisplay = timeDisplay * control->samplerate().samplerate; if (samplesDisplay >= sampleCount) { // For sure not enough samples to adjust for jitter. @@ -30,34 +50,34 @@ SoftwareTrigger::PrePostStartTriggerSamples SoftwareTrigger::compute(const PPres // 2: Change trigger mode to auto // 3: Ignore samples // For now #3 is chosen - timestampDebug(QString("Too few samples to make a steady " - "picture. Decrease sample rate")); + timestampDebug(QString("Too few samples to make a steady picture. Decrease sample rate")); return PrePostStartTriggerSamples(preTrigSamples, postTrigSamples, swTriggerStart); } - preTrigSamples = (unsigned)(scope->trigger.position * samplesDisplay); + preTrigSamples = (unsigned)(control->trigger.position() * samplesDisplay * DIVS_TIME); postTrigSamples = (unsigned)sampleCount - ((unsigned)samplesDisplay - preTrigSamples); double prev; - bool (*opcmp)(double,double,double); - bool (*smplcmp)(double,double); - if (scope->trigger.slope == Dso::Slope::Positive) { + bool (*opcmp_)(double, double, double); + bool (*smplcmp_)(double, double); + + if (control->trigger.slope() == Dso::Slope::Positive) { prev = INT_MAX; - opcmp = [](double value, double level, double prev) { return value > level && prev <= level;}; - smplcmp = [](double sampleK, double value) { return sampleK >= value;}; + opcmp_ = posOpcmp; + smplcmp_ = posSmplcmp; } else { prev = INT_MIN; - opcmp = [](double value, double level, double prev) { return value < level && prev >= level;}; - smplcmp = [](double sampleK, double value) { return sampleK < value;}; + opcmp_ = negOpcmp; + smplcmp_ = negSmplcmp; } for (unsigned int i = preTrigSamples; i < postTrigSamples; i++) { - double value = samples[i]; - if (opcmp(value, level, prev)) { + const double value = samples[i]; + if (opcmp_(value, level, prev)) { unsigned rising = 0; - for (unsigned int k = i + 1; k < i + scope->trigger.swTriggerSampleSet && k < sampleCount; k++) { - if (smplcmp(samples[k], value)) { rising++; } + for (unsigned int k = i + 1; k < i + control->trigger.swTriggerSampleSet() && k < sampleCount; k++) { + if (smplcmp_(samples[k], value)) { rising++; } } - if (rising > scope->trigger.swTriggerThreshold) { + if (rising > control->trigger.swTriggerThreshold()) { swTriggerStart = i; break; } @@ -71,3 +91,4 @@ SoftwareTrigger::PrePostStartTriggerSamples SoftwareTrigger::compute(const PPres } return PrePostStartTriggerSamples(preTrigSamples, postTrigSamples, swTriggerStart); } +} diff --git a/openhantek/src/post/softwaretrigger.h b/openhantek/src/post/softwaretrigger.h index 0ed730a2..4cd8407d 100644 --- a/openhantek/src/post/softwaretrigger.h +++ b/openhantek/src/post/softwaretrigger.h @@ -1,22 +1,31 @@ +// SPDX-License-Identifier: GPL-2.0+ + #pragma once #include -struct DsoSettingsScope; +namespace Settings { +class Scope; +} +namespace Dso { +class DeviceSettings; +class ChannelUsage; +} class PPresult; +namespace PostProcessing { /** - * Contains software trigger algorithms. At the moment this works on the analysed data of the - * DataAnalyser class. - * TODO Should work on the raw data within HantekDsoControl + * Contains software trigger algorithm */ class SoftwareTrigger { public: typedef std::tuple PrePostStartTriggerSamples; /** * @brief Computes a software trigger point. - * @param data Analysed data from the + * @param data Post processing sample set * @param scope Scope settings * @return Returns a tuple of positions [preTrigger, postTrigger, startTrigger] */ - static PrePostStartTriggerSamples compute(const PPresult *data, const DsoSettingsScope *scope); + static PrePostStartTriggerSamples compute(const PPresult *data, const Dso::DeviceSettings *control, + const Settings::Scope *scope, const Dso::ChannelUsage *channelUsage); }; +} diff --git a/openhantek/src/post/spectrumgenerator.cpp b/openhantek/src/post/spectrumgenerator.cpp index 48dde97b..207e6721 100644 --- a/openhantek/src/post/spectrumgenerator.cpp +++ b/openhantek/src/post/spectrumgenerator.cpp @@ -11,12 +11,14 @@ #include "spectrumgenerator.h" -#include "glscope.h" +#include "postprocessingsettings.h" #include "settings.h" #include "utils/printutils.h" +namespace PostProcessing { + /// \brief Analyzes the data from the dso. -SpectrumGenerator::SpectrumGenerator(const DsoSettingsScope *scope, const DsoSettingsPostProcessing *postprocessing) +SpectrumGenerator::SpectrumGenerator(const ::Settings::Scope *scope, const Settings *postprocessing) : scope(scope), postprocessing(postprocessing) {} SpectrumGenerator::~SpectrumGenerator() { @@ -25,40 +27,38 @@ SpectrumGenerator::~SpectrumGenerator() { void SpectrumGenerator::process(PPresult *result) { // Calculate frequencies and spectrums - for (ChannelID channel = 0; channel < result->channelCount(); ++channel) { - DataChannel *const channelData = result->modifyData(channel); - - if (channelData->voltage.sample.empty()) { + for (DataChannel &channelData : *result) { + if (channelData.voltage.sample.empty() || !scope->channel(channelData.channelID)->spectrum()->visible()) { // Clear unused channels - channelData->spectrum.interval = 0; - channelData->spectrum.sample.clear(); + channelData.spectrum.interval = 0; + channelData.spectrum.sample.clear(); continue; } // Calculate new window - size_t sampleCount = channelData->voltage.sample.size(); - if (!lastWindowBuffer || lastWindow != postprocessing->spectrumWindow || lastRecordLength != sampleCount) { + size_t sampleCount = channelData.voltage.sample.size(); + if (!lastWindowBuffer || lastWindow != postprocessing->spectrumWindow() || lastRecordLength != sampleCount) { if (lastWindowBuffer) fftw_free(lastWindowBuffer); lastWindowBuffer = fftw_alloc_real(sampleCount); lastRecordLength = (unsigned)sampleCount; unsigned int windowEnd = lastRecordLength - 1; - lastWindow = postprocessing->spectrumWindow; + lastWindow = postprocessing->spectrumWindow(); - switch (postprocessing->spectrumWindow) { - case Dso::WindowFunction::HAMMING: + switch (postprocessing->spectrumWindow()) { + case PostProcessing::WindowFunction::HAMMING: for (unsigned int windowPosition = 0; windowPosition < lastRecordLength; ++windowPosition) *(lastWindowBuffer + windowPosition) = 0.54 - 0.46 * cos(2.0 * M_PI * windowPosition / windowEnd); break; - case Dso::WindowFunction::HANN: + case PostProcessing::WindowFunction::HANN: for (unsigned int windowPosition = 0; windowPosition < lastRecordLength; ++windowPosition) *(lastWindowBuffer + windowPosition) = 0.5 * (1.0 - cos(2.0 * M_PI * windowPosition / windowEnd)); break; - case Dso::WindowFunction::COSINE: + case PostProcessing::WindowFunction::COSINE: for (unsigned int windowPosition = 0; windowPosition < lastRecordLength; ++windowPosition) *(lastWindowBuffer + windowPosition) = sin(M_PI * windowPosition / windowEnd); break; - case Dso::WindowFunction::LANCZOS: + case PostProcessing::WindowFunction::LANCZOS: for (unsigned int windowPosition = 0; windowPosition < lastRecordLength; ++windowPosition) { double sincParameter = (2.0 * windowPosition / windowEnd - 1.0) * M_PI; if (sincParameter == 0) @@ -67,65 +67,65 @@ void SpectrumGenerator::process(PPresult *result) { *(lastWindowBuffer + windowPosition) = sin(sincParameter) / sincParameter; } break; - case Dso::WindowFunction::BARTLETT: + case PostProcessing::WindowFunction::BARTLETT: for (unsigned int windowPosition = 0; windowPosition < lastRecordLength; ++windowPosition) *(lastWindowBuffer + windowPosition) = 2.0 / windowEnd * (windowEnd / 2 - std::abs((double)(windowPosition - windowEnd / 2.0))); break; - case Dso::WindowFunction::TRIANGULAR: + case PostProcessing::WindowFunction::TRIANGULAR: for (unsigned int windowPosition = 0; windowPosition < lastRecordLength; ++windowPosition) *(lastWindowBuffer + windowPosition) = 2.0 / lastRecordLength * (lastRecordLength / 2 - std::abs((double)(windowPosition - windowEnd / 2.0))); break; - case Dso::WindowFunction::GAUSS: { + case PostProcessing::WindowFunction::GAUSS: { double sigma = 0.4; for (unsigned int windowPosition = 0; windowPosition < lastRecordLength; ++windowPosition) *(lastWindowBuffer + windowPosition) = exp(-0.5 * pow(((windowPosition - windowEnd / 2) / (sigma * windowEnd / 2)), 2)); } break; - case Dso::WindowFunction::BARTLETTHANN: + case PostProcessing::WindowFunction::BARTLETTHANN: for (unsigned int windowPosition = 0; windowPosition < lastRecordLength; ++windowPosition) *(lastWindowBuffer + windowPosition) = 0.62 - 0.48 * std::abs((double)(windowPosition / windowEnd - 0.5)) - 0.38 * cos(2.0 * M_PI * windowPosition / windowEnd); break; - case Dso::WindowFunction::BLACKMAN: { + case PostProcessing::WindowFunction::BLACKMAN: { double alpha = 0.16; for (unsigned int windowPosition = 0; windowPosition < lastRecordLength; ++windowPosition) *(lastWindowBuffer + windowPosition) = (1 - alpha) / 2 - 0.5 * cos(2.0 * M_PI * windowPosition / windowEnd) + alpha / 2 * cos(4.0 * M_PI * windowPosition / windowEnd); } break; - // case Dso::WindowFunction::WINDOW_KAISER: + // case PostProcessing::WindowFunction::WINDOW_KAISER: // TODO WINDOW_KAISER // double alpha = 3.0; // for(unsigned int windowPosition = 0; windowPosition < // lastRecordLength; ++windowPosition) //*(window + windowPosition) = ; // break; - case Dso::WindowFunction::NUTTALL: + case PostProcessing::WindowFunction::NUTTALL: for (unsigned int windowPosition = 0; windowPosition < lastRecordLength; ++windowPosition) *(lastWindowBuffer + windowPosition) = 0.355768 - 0.487396 * cos(2 * M_PI * windowPosition / windowEnd) + 0.144232 * cos(4 * M_PI * windowPosition / windowEnd) - 0.012604 * cos(6 * M_PI * windowPosition / windowEnd); break; - case Dso::WindowFunction::BLACKMANHARRIS: + case PostProcessing::WindowFunction::BLACKMANHARRIS: for (unsigned int windowPosition = 0; windowPosition < lastRecordLength; ++windowPosition) *(lastWindowBuffer + windowPosition) = 0.35875 - 0.48829 * cos(2 * M_PI * windowPosition / windowEnd) + 0.14128 * cos(4 * M_PI * windowPosition / windowEnd) - 0.01168 * cos(6 * M_PI * windowPosition / windowEnd); break; - case Dso::WindowFunction::BLACKMANNUTTALL: + case PostProcessing::WindowFunction::BLACKMANNUTTALL: for (unsigned int windowPosition = 0; windowPosition < lastRecordLength; ++windowPosition) *(lastWindowBuffer + windowPosition) = 0.3635819 - 0.4891775 * cos(2 * M_PI * windowPosition / windowEnd) + 0.1365995 * cos(4 * M_PI * windowPosition / windowEnd) - 0.0106411 * cos(6 * M_PI * windowPosition / windowEnd); break; - case Dso::WindowFunction::FLATTOP: + case PostProcessing::WindowFunction::FLATTOP: for (unsigned int windowPosition = 0; windowPosition < lastRecordLength; ++windowPosition) *(lastWindowBuffer + windowPosition) = 1.0 - 1.93 * cos(2 * M_PI * windowPosition / windowEnd) + 1.29 * cos(4 * M_PI * windowPosition / windowEnd) - @@ -139,26 +139,26 @@ void SpectrumGenerator::process(PPresult *result) { } // Set sampling interval - channelData->spectrum.interval = 1.0 / channelData->voltage.interval / sampleCount; + channelData.spectrum.interval = 1.0 / channelData.voltage.interval / sampleCount; // Number of real/complex samples unsigned int dftLength = sampleCount / 2; // Reallocate memory for samples if the sample count has changed - channelData->spectrum.sample.resize(sampleCount); + channelData.spectrum.sample.resize(sampleCount); // Create sample buffer and apply window std::unique_ptr windowedValues = std::unique_ptr(new double[sampleCount]); for (unsigned int position = 0; position < sampleCount; ++position) - windowedValues[position] = lastWindowBuffer[position] * channelData->voltage.sample[position]; + windowedValues[position] = lastWindowBuffer[position] * channelData.voltage.sample[position]; { // Do discrete real to half-complex transformation /// \todo Check if record length is multiple of 2 /// \todo Reuse plan and use FFTW_MEASURE to get fastest algorithm fftw_plan fftPlan = fftw_plan_r2r_1d(sampleCount, windowedValues.get(), - &channelData->spectrum.sample.front(), FFTW_R2HC, FFTW_ESTIMATE); + &channelData.spectrum.sample.front(), FFTW_R2HC, FFTW_ESTIMATE); fftw_execute(fftPlan); fftw_destroy_plan(fftPlan); } @@ -169,16 +169,16 @@ void SpectrumGenerator::process(PPresult *result) { // Real values unsigned int position; double correctionFactor = 1.0 / dftLength / dftLength; - conjugateComplex[0] = (channelData->spectrum.sample[0] * channelData->spectrum.sample[0]) * correctionFactor; + conjugateComplex[0] = (channelData.spectrum.sample[0] * channelData.spectrum.sample[0]) * correctionFactor; for (position = 1; position < dftLength; ++position) conjugateComplex[position] = - (channelData->spectrum.sample[position] * channelData->spectrum.sample[position] + - channelData->spectrum.sample[sampleCount - position] * - channelData->spectrum.sample[sampleCount - position]) * + (channelData.spectrum.sample[position] * channelData.spectrum.sample[position] + + channelData.spectrum.sample[sampleCount - position] * + channelData.spectrum.sample[sampleCount - position]) * correctionFactor; // Complex values, all zero for autocorrelation conjugateComplex[dftLength] = - (channelData->spectrum.sample[dftLength] * channelData->spectrum.sample[dftLength]) * correctionFactor; + (channelData.spectrum.sample[dftLength] * channelData.spectrum.sample[dftLength]) * correctionFactor; for (++position; position < sampleCount; ++position) conjugateComplex[position] = 0; // Do half-complex to real inverse transformation @@ -204,24 +204,23 @@ void SpectrumGenerator::process(PPresult *result) { // Calculate the frequency in Hz if (peakPosition) - channelData->frequency = 1.0 / (channelData->voltage.interval * peakPosition); + channelData.frequency = 1.0 / (channelData.voltage.interval * peakPosition); else - channelData->frequency = 0; + channelData.frequency = 0; // Finally calculate the real spectrum if we want it - if (scope->spectrum[channel].used) { - // Convert values into dB (Relative to the reference level) - double offset = 60 - postprocessing->spectrumReference - 20 * log10(dftLength); - double offsetLimit = postprocessing->spectrumLimit - postprocessing->spectrumReference; - for (std::vector::iterator spectrumIterator = channelData->spectrum.sample.begin(); - spectrumIterator != channelData->spectrum.sample.end(); ++spectrumIterator) { - double value = 20 * log10(fabs(*spectrumIterator)) + offset; - - // Check if this value has to be limited - if (offsetLimit > value) value = offsetLimit; - - *spectrumIterator = value; - } + // Convert values into dB (Relative to the reference level) + double offset = 60 - postprocessing->spectrumReference() - 20 * log10(dftLength); + double offsetLimit = postprocessing->spectrumLimit() - postprocessing->spectrumReference(); + for (std::vector::iterator spectrumIterator = channelData.spectrum.sample.begin(); + spectrumIterator != channelData.spectrum.sample.end(); ++spectrumIterator) { + double value = 20 * log10(fabs(*spectrumIterator)) + offset; + + // Check if this value has to be limited + if (offsetLimit > value) value = offsetLimit; + + *spectrumIterator = value; } } } +} diff --git a/openhantek/src/post/spectrumgenerator.h b/openhantek/src/post/spectrumgenerator.h index d0f4710d..543084e0 100644 --- a/openhantek/src/post/spectrumgenerator.h +++ b/openhantek/src/post/spectrumgenerator.h @@ -8,29 +8,36 @@ #include #include -#include "ppresult.h" #include "dsosamples.h" +#include "ppresult.h" #include "utils/printutils.h" -#include "postprocessingsettings.h" +#include "enums.h" #include "processor.h" +namespace Settings { +class Scope; class DsoSettings; -struct DsoSettingsScope; +} + +namespace PostProcessing { +struct Settings; /// \brief Analyzes the data from the dso. /// Calculates the spectrum and various data about the signal and saves the /// time-/frequencysteps between two values. class SpectrumGenerator : public Processor { public: - SpectrumGenerator(const DsoSettingsScope* scope, const DsoSettingsPostProcessing* postprocessing); + SpectrumGenerator(const ::Settings::Scope *scope, const Settings *postprocessing); virtual ~SpectrumGenerator(); virtual void process(PPresult *data) override; private: - const DsoSettingsScope* scope; - const DsoSettingsPostProcessing* postprocessing; - unsigned int lastRecordLength = 0; ///< The record length of the previously analyzed data - Dso::WindowFunction lastWindow = (Dso::WindowFunction)-1; ///< The previously used dft window function + const ::Settings::Scope *scope; + const Settings *postprocessing; + unsigned int lastRecordLength = 0; ///< The record length of the previously analyzed data + PostProcessing::WindowFunction lastWindow = + (PostProcessing::WindowFunction)-1; ///< The previously used dft window function double *lastWindowBuffer = nullptr; }; +} diff --git a/openhantek/src/scopesettings.h b/openhantek/src/scopesettings.h deleted file mode 100644 index daa50ce1..00000000 --- a/openhantek/src/scopesettings.h +++ /dev/null @@ -1,80 +0,0 @@ -// SPDX-License-Identifier: GPL-2.0+ - -#pragma once - -#include - -#include "hantekdso/controlspecification.h" -#include "hantekdso/enums.h" -#include "hantekprotocol/definitions.h" -#include - -#define MARKER_COUNT 2 ///< Number of markers -#define MARKER_STEP (DIVS_TIME / 100.0) - -/// \brief Holds the settings for the horizontal axis. -struct DsoSettingsScopeHorizontal { - Dso::GraphFormat format = Dso::GraphFormat::TY; ///< Graph drawing mode of the scope - double frequencybase = 1e3; ///< Frequencybase in Hz/div - double marker[MARKER_COUNT] = {-1.0, 1.0}; ///< Marker positions in div - bool marker_visible[MARKER_COUNT] = {true, true}; - - unsigned int recordLength = 0; ///< Sample count - - /// TODO Use ControlSettingsSamplerateTarget - double timebase = 1e-3; ///< Timebase in s/div - double samplerate = 1e6; ///< The samplerate of the oscilloscope in S - enum SamplerateSource { Samplerrate, Duration } samplerateSource = Samplerrate; -}; - -/// \brief Holds the settings for the trigger. -/// TODO Use ControlSettingsTrigger -struct DsoSettingsScopeTrigger { - Dso::TriggerMode mode = Dso::TriggerMode::HARDWARE_SOFTWARE; ///< Automatic, normal or single trigger - double position = 0.0; ///< Horizontal position for pretrigger - Dso::Slope slope = Dso::Slope::Positive; ///< Rising or falling edge causes trigger - unsigned int source = 0; ///< Channel that is used as trigger source - bool special = false; ///< true if the trigger source is not a standard channel - unsigned swTriggerThreshold = 7; ///< Software trigger, threshold - unsigned swTriggerSampleSet = 11; ///< Software trigger, sample set -}; - -/// \brief Holds the settings for the spectrum analysis. -struct DsoSettingsScopeSpectrum { - ChannelID channel; - double magnitude = 20.0; ///< The vertical resolution in dB/div - QString name; ///< Name of this channel - double offset = 0.0; ///< Vertical offset in divs - bool used = false; ///< true if the spectrum is turned on -}; - -/// \brief Holds the settings for the normal voltage graphs. -/// TODO Use ControlSettingsVoltage -struct DsoSettingsScopeVoltage { - double offset = 0.0; ///< Vertical offset in divs - double trigger = 0.0; ///< Trigger level in V - unsigned gainStepIndex = 6; ///< The vertical resolution in V/div (default = 1.0) - unsigned couplingOrMathIndex = 0; ///< Different index: coupling for real- and mode for math-channels - QString name; ///< Name of this channel - bool inverted = false; ///< true if the channel is inverted (mirrored on cross-axis) - bool used = false; ///< true if this channel is enabled -}; - -/// \brief Holds the settings for the oscilloscope. -struct DsoSettingsScope { - std::vector gainSteps = {1e-2, 2e-2, 5e-2, 1e-1, 2e-1, - 5e-1, 1e0, 2e0, 5e0}; ///< The selectable voltage gain steps in V/div - std::vector spectrum; ///< Spectrum analysis settings - std::vector voltage; ///< Settings for the normal graphs - DsoSettingsScopeHorizontal horizontal; ///< Settings for the horizontal axis - DsoSettingsScopeTrigger trigger; ///< Settings for the trigger - - double gain(unsigned channel) const { return gainSteps[voltage[channel].gainStepIndex]; } - bool anyUsed(ChannelID channel) { return voltage[channel].used | spectrum[channel].used; } - - Dso::Coupling coupling(ChannelID channel, const Dso::ControlSpecification *deviceSpecification) const { - return deviceSpecification->couplings[voltage[channel].couplingOrMathIndex]; - } - // Channels, including math channels - unsigned countChannels() const { return (unsigned)voltage.size(); } -}; diff --git a/openhantek/src/scopeview/glframe.cpp b/openhantek/src/scopeview/glframe.cpp new file mode 100644 index 00000000..45eaeab9 --- /dev/null +++ b/openhantek/src/scopeview/glframe.cpp @@ -0,0 +1,154 @@ +#include "glframe.h" + +using namespace Qt3DRender; +using namespace Qt3DExtras; +using namespace Qt3DCore; + +GlFrame::GlFrame(GlMouseDevice *mouse, const ScopeCoordinates *coordinates, const QColor *normal, const QColor *hover, + const QColor *pressed, const QColor *active, Observer *colorObserver, QMaterial *background, + QLayer *layer, Qt3DCore::QEntity *parent) + : Qt3DCore::QEntity(parent), GlMoveResizeSnap(&m_rect, &m_frameIndex, mouse, coordinates, this), + m_background(background), m_normal(normal), m_hover(hover), m_pressed(pressed), m_active(active) { + + static const float frameV[] = {0, 0, 0, 1, 0, 0, 1, 0, 0, 1, 1, 0, // lines + 1, 1, 0, 0, 1, 0, 0, 1, 0, 0, 0, 0, // lines + 0, 0, 1, 0, 0, 1, 0, 0, 1, 0, 0, 1, // normales + 0, 0, 1, 0, 0, 1, 0, 0, 1, 0, 0, 1}; // normales + + QByteArray frameGeometryTemplate; + frameGeometryTemplate.setRawData((char *)frameV, sizeof(frameV)); + + auto material = new Qt3DExtras::QPerVertexColorMaterial(this); + QObject::connect(colorObserver, &Observer::changed, this, &GlFrame::inputStateChanged); + addComponent(material); + + m_colorBuffer = new QBuffer(QBuffer::VertexBuffer, material); + m_colorBuffer->setUsage(QBuffer::DynamicDraw); + QByteArray t; + t.fill(0, sizeof(QVector4D) * 8); + m_colorBuffer->setData(t); + + const int numV = 8; + QBuffer *dataBuffer = new QBuffer(QBuffer::VertexBuffer, this); + dataBuffer->setData(frameGeometryTemplate); + const auto namePosition = QAttribute::defaultPositionAttributeName(); + auto attr = new QAttribute(dataBuffer, namePosition, QAttribute::Float, 3, numV, 0, 0); + const auto nameNormal = QAttribute::defaultNormalAttributeName(); + auto attrN = new QAttribute(dataBuffer, nameNormal, QAttribute::Float, 3, numV, numV * sizeof(QVector3D)); + const auto nameColor = QAttribute::defaultColorAttributeName(); + auto attrC = new QAttribute(m_colorBuffer, nameColor, QAttribute::Float, 4, numV); + auto mesh = new QGeometryRenderer(this); + auto geometry = new QGeometry(mesh); + geometry->addAttribute(attr); + geometry->addAttribute(attrN); + geometry->addAttribute(attrC); + mesh->setVertexCount((int)numV); + mesh->setPrimitiveType(QGeometryRenderer::Lines); + mesh->setGeometry(geometry); + addComponent(mesh); + + if (background) createBackground(); + + m_transform = new Qt3DCore::QTransform(this); + addComponent(m_transform); + if (layer) addComponent(layer); + + connect(moveResizeSignals(), &GlMoveResizeSnapSignals::clicked, this, [this]() { + if (m_isActivatable) { + m_isActive = true; + emit activated(frameIndex()); + } + }); +} + +GlFrame::~GlFrame() {} + +void GlFrame::setActive(bool enable) { + if (!m_isActivatable && enable) return; + m_isActive = enable; + inputStateChanged(); +} + +void GlFrame::setActivatable(bool enable) { + m_isActivatable = enable; + if (!enable) setActive(false); +} + +void GlFrame::updateRectangle(const QRectF &rect) { + m_rect = rect; + + const QRectF m = m_coordinates->scopeRect(); + if (m_rect.left() <= m.left()) m_rect.setLeft(m.left()); + if (m_rect.right() >= m.right()) m_rect.setRight(m.right()); + + if (m_rect.top() <= m.top()) m_rect.setTop(m.top()); + if (m_rect.bottom() >= m.bottom()) m_rect.setBottom(m.bottom()); + + if (m_rect.height() < 0.3) m_rect.setHeight(0.3); + if (m_rect.width() < 0.3) m_rect.setWidth(0.3); + + rectChanged(); + inputStateChanged(); + updateSnap(); +} + +inline QVector4D color2vec(const QColor &c) { + return QVector4D((float)c.redF(), (float)c.greenF(), (float)c.blueF(), (float)c.alphaF()); +} +void GlFrame::createBackground() { + Qt3DCore::QEntity *bgEntity = new QEntity(this); + + bgEntity->addComponent(m_background); + + static const QVector3D frameV[] = {QVector3D(0, 1, 0.1f), QVector3D(0, 0, 0.1f), QVector3D(1, 1, 0.1f), + QVector3D(1, 0, 0.1f), QVector3D(0, 0, 0.1f), // vertex; lines + QVector3D(0, 0, 1), QVector3D(0, 0, 1), QVector3D(0, 0, 1), + QVector3D(0, 0, 1), QVector3D(0, 0, 1)}; // normales + + QByteArray frameBgGeometry; + frameBgGeometry.setRawData((char *)frameV, sizeof(frameV)); + + const int numV = 5; + QBuffer *dataBuffer = new QBuffer(QBuffer::VertexBuffer, this); + dataBuffer->setData(frameBgGeometry); + const auto namePosition = QAttribute::defaultPositionAttributeName(); + auto attr = new QAttribute(dataBuffer, namePosition, QAttribute::Float, 3, numV, 0, 0); + const auto nameNormal = QAttribute::defaultNormalAttributeName(); + auto attrN = new QAttribute(dataBuffer, nameNormal, QAttribute::Float, 3, numV, numV * sizeof(QVector3D)); + auto mesh = new QGeometryRenderer(this); + auto geometry = new QGeometry(mesh); + geometry->addAttribute(attr); + geometry->addAttribute(attrN); + mesh->setVertexCount((int)numV); + mesh->setPrimitiveType(QGeometryRenderer::TriangleStrip); + mesh->setGeometry(geometry); + bgEntity->addComponent(mesh); + bgEntity->setParent(this); +} + +void GlFrame::rectChanged() { + m_transform->setScale3D(QVector3D((float)m_rect.width(), (float)m_rect.height(), 0)); + m_transform->setTranslation(QVector3D((float)m_rect.left(), (float)m_rect.top(), 0)); +} + +void GlFrame::inputStateChanged() { + const QVector4D color = color2vec(m_isActive ? *m_active : *m_normal); + const QVector4D colorHover = color2vec(m_isPressed ? *m_pressed : *m_hover); + + QByteArray colorB; + colorB.resize(sizeof(QVector4D) * 8); + QVector4D *colorBp = reinterpret_cast(colorB.data()); + *(colorBp++) = m_hoveredParts / EdgePositionFlags::Top ? colorHover : color; + *(colorBp++) = m_hoveredParts / EdgePositionFlags::Top ? colorHover : color; + + *(colorBp++) = m_hoveredParts / EdgePositionFlags::Right ? colorHover : color; + *(colorBp++) = m_hoveredParts / EdgePositionFlags::Right ? colorHover : color; + + *(colorBp++) = m_hoveredParts / EdgePositionFlags::Bottom ? colorHover : color; + *(colorBp++) = m_hoveredParts / EdgePositionFlags::Bottom ? colorHover : color; + + *(colorBp++) = m_hoveredParts / EdgePositionFlags::Left ? colorHover : color; + *(colorBp++) = m_hoveredParts / EdgePositionFlags::Left ? colorHover : color; + + m_colorBuffer->updateData(0, colorB); +} diff --git a/openhantek/src/scopeview/glframe.h b/openhantek/src/scopeview/glframe.h new file mode 100644 index 00000000..2f37ca9c --- /dev/null +++ b/openhantek/src/scopeview/glframe.h @@ -0,0 +1,107 @@ +#pragma once + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include + +#include "glmoveresizesnap.h" +#include "utils/observer.h" + +class GlFrame : public Qt3DCore::QEntity, public GlMoveResizeSnap { + Q_OBJECT + public: + /** + * @brief Creates a new frame entity + * @param m_mouse A mouse device, used for moving and resizing the frame + * @param normal The normal color + * @param hover The hovered color + * @param pressed The pressed color + * @param active The actived color + * @param colorObserver A color observer. Whenever the observer signals that a color changed, a redraw + * with the color object pointers is performed. + * @param m_coordinates A coordinate system + * @param background A background material. This object will takeover owenership. + * @param layer A layer that this entity and all subentities will be part of or null. + * @param parent A parent entity. + */ + explicit GlFrame(GlMouseDevice *mouse, const ScopeCoordinates *m_coordinates, const QColor *normal, + const QColor *hover, const QColor *pressed, const QColor *active, Observer *colorObserver, + Qt3DRender::QMaterial *background = nullptr, Qt3DRender::QLayer *layer = nullptr, + Qt3DCore::QEntity *parent = nullptr); + virtual ~GlFrame(); + + /** + * @brief (De-)Activate this frame. An activated frame is drawn with different colors + * @param enable + */ + void setActive(bool enable); + + /** + * @brief Make this frame activatable or not. + * @param enable + */ + void setActivatable(bool enable); + + /** + * @brief Updates the frame position and geometry. This will not emit a frameChanged() signal. + * @param rect Frame position and geometry + */ + void updateRectangle(const QRectF &rect); + + /** + * @return Returns the current position and geometry + */ + inline const QRectF *rect() const { return &m_rect; } + + /** + * @brief Sets a frame index. This is not necessary for the frame to function but may help + * in differentiating between multiple frames. The frame index is attachted to all signals. + * @param frameIndex + */ + inline void setFrameIndex(unsigned frameIndex) { m_frameIndex = frameIndex; } + + /** + * @brief Return the frame index. {@see setFrameIndex} + */ + inline unsigned frameIndex() { return m_frameIndex; } + + private: + // User provided data + Qt3DRender::QMaterial *m_background; + const QColor *m_normal; + const QColor *m_hover; + const QColor *m_pressed; + const QColor *m_active; + + // Qt3D + Qt3DCore::QTransform *m_transform; + Qt3DRender::QBuffer *m_colorBuffer; + + // State + QRectF m_rect; + bool m_isActive = false; + bool m_isActivatable = true; + unsigned m_frameIndex = 0; + + void createBackground(); + + protected: + virtual void rectChanged() override; + virtual void inputStateChanged() override; + + signals: + /** + * @brief Will be emitted when the user clicked on the frame, without resizing or moving it + * @param frameIndex The frame index + */ + void activated(unsigned frameIndex); +}; +Q_DECLARE_METATYPE(EdgePositionFlags); diff --git a/openhantek/src/scopeview/glicon.cpp b/openhantek/src/scopeview/glicon.cpp new file mode 100644 index 00000000..144db70b --- /dev/null +++ b/openhantek/src/scopeview/glicon.cpp @@ -0,0 +1,111 @@ +#include "glicon.h" + +#include +#include +#include +#include + +using namespace Qt3DRender; +using namespace Qt3DExtras; +using namespace Qt3DCore; + +#define MAX_WIDTH DIVS_TIME / 2 +#define MAX_HEIGHT DIVS_VOLTAGE / 2 + +class PaintIcon : public QPaintedTextureImage { + Q_OBJECT + public: + PaintIcon(const QIcon &icon, QIcon::Mode mode, Qt3DCore::QNode *parent = nullptr) + : QPaintedTextureImage(parent), m_icon(icon), m_mode(mode) { + setSize(QSize(256, 256)); + update(); + } + virtual ~PaintIcon() {} + void setMode(QIcon::Mode mode) { + m_mode = mode; + update(); + } + + private: + const QIcon m_icon; + QColor border; + QIcon::Mode m_mode = QIcon::Normal; + + protected: + virtual void paint(QPainter *painter) override { + int w = painter->device()->width(); + int h = painter->device()->height(); + + painter->setPen(QPen(QBrush(QColor(Qt::black)), 10)); + painter->setBrush(QBrush()); + + painter->drawEllipse(0, 0, w, h); + m_icon.paint(painter, 0, 0, w, h, Qt::AlignCenter, m_mode, QIcon::On); + } +}; +#include "glicon.moc" + +GlIcon::GlIcon(GlMouseDevice *mouse, const ScopeCoordinates *coordinates, const QColor *normal, const QColor *hover, + const QColor *pressed, Observer *colorObserver, const QIcon &icon, QLayer *layer, + Qt3DCore::QEntity *parent) + : Qt3DCore::QEntity(parent), GlMoveResizeSnap(&m_rect, 0, mouse, coordinates, this), m_coordinates(coordinates), + m_normal(normal), m_hover(hover), m_pressed(pressed) { + + // Move/Resize init + setResizable(false); + setMovable(false); + m_inputPriority = 1; + + auto te = new QEntity; + te->setParent(this); + if (parent) this->setParent(parent); + + QObject::connect(colorObserver, &Observer::changed, this, &GlIcon::inputStateChanged); + QObject::connect(m_coordinates, &ScopeCoordinates::rectChanged, this, [this]() { updateSize(); }); + + m_iconTexture = new PaintIcon(icon, QIcon::Normal); + m_material->diffuse()->addTextureImage(m_iconTexture); + + // We need a sub-entity, because QPlaneMesh is on the yz-axes, center-aligned + // and we need to rotate it to be on the xy-axes and translate it have its origin left/bottom + auto m = new QPlaneMesh; + m->setWidth(1); + m->setHeight(1); + te->addComponent(m); + te->addComponent(m_material); + auto tr = new Qt3DCore::QTransform(this); + tr->setRotationX(45); ///< Make it a xy plane + tr->setTranslation(QVector3D(0.5, 0.5, 0)); ///addComponent(tr); + + m_transform = new Qt3DCore::QTransform(this); + addComponent(m_transform); + if (layer) addComponent(layer); + updateSize(QSizeF(32, 32)); ///< 32x32px icon + inputStateChanged(); +} + +GlIcon::~GlIcon() { + m_material->diffuse()->removeTextureImage(m_iconTexture); + delete m_iconTexture; +} + +void GlIcon::updatePosition(const QPointF &position) { + m_rect.moveTo(position); + rectChanged(); +} + +void GlIcon::updateSize(QSizeF size) { + if (size.height() > 0.0f) m_sizeInScreenPixels = size; + m_rect.setWidth(m_coordinates->width((float)m_sizeInScreenPixels.width())); + m_rect.setHeight(m_coordinates->height((float)m_sizeInScreenPixels.height())); + m_transform->setScale3D(QVector3D((float)m_rect.width(), (float)m_rect.height(), 0.5)); +} + +void GlIcon::rectChanged() { m_transform->setTranslation(QVector3D((float)m_rect.left(), (float)m_rect.top(), 0)); } + +void GlIcon::inputStateChanged() { + const QColor *color = m_hoveredParts != EdgePositionFlags::None ? m_hover : (m_isPressed ? m_pressed : m_normal); + // m_iconTexture->setMode(m_isHovered ? QIcon::Selected : (m_isPressed ? QIcon::Active : QIcon::Normal)); + m_material->setAmbient(*color); +} diff --git a/openhantek/src/scopeview/glicon.h b/openhantek/src/scopeview/glicon.h new file mode 100644 index 00000000..f8332094 --- /dev/null +++ b/openhantek/src/scopeview/glicon.h @@ -0,0 +1,87 @@ +#pragma once + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include + +#include "glmoveresizesnap.h" +#include "utils/observer.h" +#include "utils/scopecoordinates.h" + +class PaintIcon; + +class GlIcon : public Qt3DCore::QEntity, public GlMoveResizeSnap { + Q_OBJECT + public: + /** + * @brief Creates a new frame entity + * @param m_mouse A mouse device, used for moving and resizing the frame + * @param m_coordinates A coordinate system + * @param normal The normal color + * @param hover The hovered color + * @param pressed The pressed color + * @param colorObserver A color observer. Whenever the observer signals that a color changed, a redraw + * with the color object pointers is performed. + * @param background A background material. This object will takeover owenership. + * @param layer A layer that this entity and all subentities will be part of or null. + * @param parent A parent entity. + */ + explicit GlIcon(GlMouseDevice *mouse, const ScopeCoordinates *m_coordinates, const QColor *normal, + const QColor *hover, const QColor *pressed, Observer *colorObserver, const QIcon &icon, + Qt3DRender::QLayer *layer = nullptr, Qt3DCore::QEntity *parent = nullptr); + virtual ~GlIcon(); + + /** + * @brief Updates the icon position. + * @param position Icon position in screen coordinates + */ + void updatePosition(const QPointF &position); + + /** + * @brief Updates the icon geometry. + * @param size Icon geometry in screen pixels + */ + void updateSize(QSizeF size = QSize()); + + /** + * @return Returns the current position and geometry + */ + inline const QRectF *rect() const { return &m_rect; } + + protected: + virtual void rectChanged() override; + virtual void inputStateChanged() override; + + private: + const ScopeCoordinates *m_coordinates; + + // Icon textures + PaintIcon *m_iconTexture; + + // Background + Qt3DExtras::QNormalDiffuseMapAlphaMaterial *m_material = new Qt3DExtras::QNormalDiffuseMapAlphaMaterial(this); + Qt3DCore::QTransform *m_transform; + const QColor *m_normal; + const QColor *m_hover; + const QColor *m_pressed; + + // State + QRectF m_rect; + QSizeF m_sizeInScreenPixels; +}; diff --git a/openhantek/src/scopeview/glmousedevice.cpp b/openhantek/src/scopeview/glmousedevice.cpp new file mode 100644 index 00000000..401a3a7e --- /dev/null +++ b/openhantek/src/scopeview/glmousedevice.cpp @@ -0,0 +1,6 @@ +#include "glmousedevice.h" + +GlMouseDevice::GlMouseDevice() +{ + +} diff --git a/openhantek/src/scopeview/glmousedevice.h b/openhantek/src/scopeview/glmousedevice.h new file mode 100644 index 00000000..f7182ce3 --- /dev/null +++ b/openhantek/src/scopeview/glmousedevice.h @@ -0,0 +1,52 @@ +#pragma once + +#include + +/** + * In Qt5.9+ we can use QObjectPicker. But for now we have a QMouseDevice + * that also keeps track which 3D object is focused. There can only be one focused + * object at a time. Objects can grab and release the focus. If an object with + * a higher priority (QlIcon in contrast to QlFrame for instance) request the focus, + * it will be stolen from the current focused object. + * + * GlIcon and GlFrame support this focus method. + */ +class GlMouseDevice : public Qt3DInput::QMouseDevice { + Q_OBJECT + public: + GlMouseDevice(); + inline void unsetFocusObject(QObject *focusObject) { + if (m_focusObject == focusObject) { + disconnect(m_focusObject, &QObject::destroyed, this, &GlMouseDevice::unsetFocusObject); + m_focusObject = nullptr; + m_focusObjectPriority = 0; + emit focusObjectChanged(m_focusObject); + } + } + inline bool grabFocus(QObject *focusObject, int priority = 0) { + if (m_focusObject == focusObject) return true; + + if (m_focusObject) { + if (priority > m_focusObjectPriority) { + unsetFocusObject(m_focusObject); + emit focusStoolen(); + m_focusObjectPriority = priority; + m_focusObject = focusObject; + return true; + } else + return false; + } else { + m_focusObject = focusObject; + connect(m_focusObject, &QObject::destroyed, this, &GlMouseDevice::unsetFocusObject); + emit focusObjectChanged(m_focusObject); + return true; + } + } + + private: + QObject *m_focusObject = nullptr; + int m_focusObjectPriority = 0; + signals: + void focusObjectChanged(void *m_focusObject); + void focusStoolen(); +}; diff --git a/openhantek/src/scopeview/glmoveresizesnap.cpp b/openhantek/src/scopeview/glmoveresizesnap.cpp new file mode 100644 index 00000000..ed6a3780 --- /dev/null +++ b/openhantek/src/scopeview/glmoveresizesnap.cpp @@ -0,0 +1,184 @@ +#include "glmoveresizesnap.h" +#include + +static const unsigned invalidID = INT_MAX; + +GlMoveResizeSnap::GlMoveResizeSnap(QRectF *rect, unsigned *id, GlMouseDevice *mouse, + const ScopeCoordinates *coordinates, Qt3DCore::QEntity *parent) + : m_coordinates(coordinates), m_id(id ? id : &invalidID), m_rect(rect), m_mouse(mouse) { + + qRegisterMetaType("HoveredPosition"); + QObject::connect(mouse, &QObject::destroyed, &signalEmitter, [this]() { m_mouse = nullptr; }); + + // Input handling. Create a mouseHandler. Disable it if another object has the focus + if (!mouse) return; + Qt3DInput::QMouseHandler *mouseHandler = new Qt3DInput::QMouseHandler(parent); + mouseHandler->setSourceDevice(mouse); + mouseHandler->setEnabled(true); + QObject::connect( + mouse, &GlMouseDevice::focusObjectChanged, &signalEmitter, + [this, mouseHandler](void *focusObject) { mouseHandler->setEnabled(focusObject == this || !focusObject); }); + QObject::connect(mouse, &GlMouseDevice::focusStoolen, &signalEmitter, [this]() { this->resetState(); }); + parent->addComponent(mouseHandler); + QObject::connect(mouseHandler, &Qt3DInput::QMouseHandler::positionChanged, &signalEmitter, + [this](Qt3DInput::QMouseEvent *e) { mouseMoved(e); }); + QObject::connect(mouseHandler, &Qt3DInput::QMouseHandler::pressed, &signalEmitter, + [this](Qt3DInput::QMouseEvent *e) { mouseClicked(e); }); + QObject::connect(mouseHandler, &Qt3DInput::QMouseHandler::released, &signalEmitter, + [this](Qt3DInput::QMouseEvent *e) { mouseClicked(e); }); +} + +GlMoveResizeSnap::~GlMoveResizeSnap() { + if (m_mouse) m_mouse->unsetFocusObject(&signalEmitter); +} + +void GlMoveResizeSnap::setAllowInteraction(bool enable) { + m_isInteractive = enable; + if (!enable) { + m_isPressed = false; + inputStateChanged(); + } +} + +void GlMoveResizeSnap::rectChanged() {} + +void GlMoveResizeSnap::inputStateChanged() {} + +void GlMoveResizeSnap::mouseMoved(Qt3DInput::QMouseEvent *event) { + if (!m_isInteractive) return; + const float newX = m_coordinates->x(event->x()); + const float newY = m_coordinates->y(event->y()); + + if (m_isPressed) { + handleMoveResize(newX, newY); + return; + } + + if (newX < m_rect->left() - m_coordinates->ratioX() * 8 || newX > m_rect->right() + m_coordinates->ratioX() * 8 || + newY < m_rect->top() - m_coordinates->ratioY() * 8 || newY > m_rect->bottom() + m_coordinates->ratioX() * 8) { + m_mouse->unsetFocusObject(&signalEmitter); + resetState(); + return; + } + + if (!m_mouse->grabFocus(&signalEmitter, m_inputPriority)) return; + + Qt::CursorShape cursorShape = Qt::ArrowCursor; + EdgePositionFlags newFlag = EdgePositionFlags::None; + if (std::abs(newX - m_rect->left()) < m_coordinates->ratioX() * 8) { + newFlag |= EdgePositionFlags::Left; + cursorShape = Qt::SizeHorCursor; + } + if (std::abs(newX - m_rect->right()) < m_coordinates->ratioX() * 8) { + newFlag |= EdgePositionFlags::Right; + cursorShape = Qt::SizeHorCursor; + } + if (std::abs(newY - m_rect->top()) < m_coordinates->ratioY() * 8) { + newFlag |= EdgePositionFlags::Top; + cursorShape = (cursorShape == Qt::SizeHorCursor) ? Qt::SizeBDiagCursor : Qt::SizeVerCursor; + } + if (std::abs(newY - m_rect->bottom()) < m_coordinates->ratioY() * 8) { + newFlag |= EdgePositionFlags::Bottom; + cursorShape = (cursorShape == Qt::SizeHorCursor) ? Qt::SizeBDiagCursor : Qt::SizeVerCursor; + } + if (newFlag == EdgePositionFlags::None) { + newFlag = EdgePositionFlags::Middle; + cursorShape = Qt::SizeAllCursor; + } + + if (m_hoveredParts != newFlag) { + m_hoveredParts = newFlag; + emit signalEmitter.requestMouseCursor(cursorShape); + inputStateChanged(); + emit signalEmitter.hovered(*m_id, m_hoveredParts); + } +} + +void GlMoveResizeSnap::mouseClicked(Qt3DInput::QMouseEvent *event) { + bool pressed = (unsigned)event->buttons() & Qt::LeftButton; + if (!pressed && m_isPressed) { + m_isPressed = false; + const float newX = m_coordinates->x(event->x()); + const float newY = m_coordinates->y(event->y()); + // Just a click + if (m_localGrabPos.x() == newX - (float)m_rect->x() && m_localGrabPos.y() == newY - (float)m_rect->y()) { + emit signalEmitter.clicked(*m_id); + } + inputStateChanged(); + } else if (pressed && m_hoveredParts != EdgePositionFlags::None) { + const float newX = m_coordinates->x(event->x()); + const float newY = m_coordinates->y(event->y()); + m_localGrabPos = QVector2D(newX - (float)m_rect->x(), newY - (float)m_rect->y()); + m_isPressed = true; + } +} + +void GlMoveResizeSnap::handleMoveResize(float newX, float newY) { + const QRectF &borderRect = m_coordinates->fixedScopeRect(); + // Handle move + if (m_isMovable && EdgePositionFlags::Middle == m_hoveredParts) { + float x = newX - m_localGrabPos.x(); + x = std::min(x, (float)borderRect.right() - (float)m_rect->width()); // c++17 cramp + x = std::max(x, (float)borderRect.left()); + + float y = newY - m_localGrabPos.y(); + y = std::min(y, (float)borderRect.bottom() - (float)m_rect->height()); // c++17 cramp + y = std::max(y, (float)borderRect.top()); + + m_rect->moveTo(x, y); + rectChanged(); + updateSnap(); + emit signalEmitter.frameChanged(*m_id, *m_rect); + } else if (m_isResizable) { + if (m_hoveredParts / EdgePositionFlags::Left) { + float x = std::min(newX, (float)m_rect->right() - 0.2f); // don't go over the other side of the rect + x = std::max(x, (float)borderRect.left()); // don't go beyond the border + m_rect->setLeft(x); + } + if (m_hoveredParts / EdgePositionFlags::Right) { + float x = std::max(newX, (float)m_rect->left() + 0.2f); + x = std::min(x, (float)borderRect.right()); // don't go beyond the border + m_rect->setRight(x); + } + if (m_hoveredParts / EdgePositionFlags::Top) { + float y = std::min(newY, (float)m_rect->bottom() - 0.2f); // don't go over the other side of the rect + y = std::max(y, (float)borderRect.top()); // don't go beyond the border + m_rect->setTop(y); + } + if (m_hoveredParts / EdgePositionFlags::Bottom) { + float y = std::max(newY, (float)m_rect->top() + 0.2f); // don't go over the other side of the rect + y = std::min(y, (float)borderRect.bottom()); // don't go beyond the border + m_rect->setBottom(y); + } + rectChanged(); + m_snappedParts = EdgePositionFlags::None; + updateSnap(); + emit signalEmitter.frameChanged(*m_id, *m_rect); + } +} + +void GlMoveResizeSnap::updateSnap() { + const QRectF &m = m_coordinates->scopeRect(); + auto snapped = EdgePositionFlags::None; + if (m_rect->left() <= (float)m.left()) { + snapped |= EdgePositionFlags::Left; + } else if (m_rect->right() >= (float)m.right()) { + snapped |= EdgePositionFlags::Right; + } else if (m_rect->top() <= (float)m.top()) { + snapped |= EdgePositionFlags::Top; + } else if (m_rect->bottom() >= (float)m.bottom()) { + snapped |= EdgePositionFlags::Bottom; + } + + if (m_snappedParts == snapped) return; + m_snappedParts = snapped; + emit signalEmitter.snapChanged(*m_id); +} + +void GlMoveResizeSnap::resetState() { + if (m_hoveredParts != EdgePositionFlags::None) { + m_hoveredParts = EdgePositionFlags::None; + emit signalEmitter.requestMouseCursor(Qt::ArrowCursor); + inputStateChanged(); + } +} diff --git a/openhantek/src/scopeview/glmoveresizesnap.h b/openhantek/src/scopeview/glmoveresizesnap.h new file mode 100644 index 00000000..4e885233 --- /dev/null +++ b/openhantek/src/scopeview/glmoveresizesnap.h @@ -0,0 +1,134 @@ +#pragma once + +#include "glmousedevice.h" +#include "glscopehover.h" +#include "utils/scopecoordinates.h" + +#include +#include +#include + +/** + * Because we do not want GlMoveResizeSnap to inherit from a QObject + * but still want to send signals, we use a delegate object. + */ +class GlMoveResizeSnapSignals : public QObject { + Q_OBJECT + signals: + + /** + * @brief Emitted whenever the position or geometry of the frame changed + * @param id The frame index + * @param rect The new size and position + */ + void frameChanged(unsigned id, const QRectF &rect); + + /** + * @brief Emitted when clicked + * @param id The frame index + */ + void clicked(unsigned id); + + /** + * @brief This signal is emitted whenever the hover status changed + * @param id The frame index + * @param hoveredAreas Test for a left hover with `hoveredAreas / EdgePositionFlags::Left` for example. + */ + void hovered(unsigned id, EdgePositionFlags hoveredAreas); + + /** + * @brief This signal is emitted whenever the frame snapped to a border of the containing view + * @param id The frame index + */ + void snapChanged(unsigned id); + + /** + * @brief If the mouse hovers over the frame and it is movable or resizable, it will emit this signal + * and request a fitting mouse cursor shape. + * @param shape The cursor shape + */ + void requestMouseCursor(Qt::CursorShape shape); +}; + +/** + * Inherit from this class to gain mouse interactive abilities like resizing, moving, snapping, mouse clicks + * and hover states. + */ +class GlMoveResizeSnap { + public: + GlMoveResizeSnap(QRectF *rect, unsigned *id, GlMouseDevice *mouse, const ScopeCoordinates *coordinates, + Qt3DCore::QEntity *parent); + virtual ~GlMoveResizeSnap(); + + /** + * @brief Allows or disallows interactions (move/resize/activate). + * @param enable + */ + void setAllowInteraction(bool enable); + + /** + * @brief Allow or disallow this frame to be resizable + * @param enable + */ + inline void setResizable(bool enable) { m_isResizable = enable; } + + /** + * @brief Allow or disallow this frame to be movable + * @param enable + */ + inline void setMovable(bool enable) { m_isMovable = enable; } + + /** + * @return Returns the snapped borders. + */ + inline EdgePositionFlags snapState() const { return m_snappedParts; } + + inline GlMoveResizeSnapSignals *moveResizeSignals() { return &signalEmitter; } + + inline const ScopeCoordinates *coordinateSystem() const { return m_coordinates; } + + /** + * Call this method in your class, when the position/geometry rectangle was updated. + */ + void updateSnap(); + + protected: + /** + * You will be informed, whenever the position/geometry rectangle changes. + */ + virtual void rectChanged(); + /** + * Implement this to receive a call whenever the input state (hover state, pressed state) changed. + */ + virtual void inputStateChanged(); + + // Input state + EdgePositionFlags m_hoveredParts; + bool m_isPressed = false; + int m_inputPriority = 0; + + // Position/Geometry state + const ScopeCoordinates *m_coordinates; + + const unsigned *m_id; + + protected: + // Position/Geometry state + QRectF *m_rect; + + private: + // Input state + EdgePositionFlags m_snappedParts = EdgePositionFlags::None; + QVector2D m_localGrabPos; + bool m_isInteractive = true; + bool m_isResizable = true; + bool m_isMovable = true; + + GlMouseDevice *m_mouse; + GlMoveResizeSnapSignals signalEmitter; + + void mouseMoved(Qt3DInput::QMouseEvent *event); + void mouseClicked(Qt3DInput::QMouseEvent *event); + void handleMoveResize(float newX, float newY); + void resetState(); +}; diff --git a/openhantek/src/scopeview/glscope.cpp b/openhantek/src/scopeview/glscope.cpp new file mode 100644 index 00000000..22236dfc --- /dev/null +++ b/openhantek/src/scopeview/glscope.cpp @@ -0,0 +1,348 @@ +// SPDX-License-Identifier: GPL-2.0+ + +#include "glscope.h" + +#include "post/graphgenerator.h" +#include "post/ppresult.h" +#include "scopesettings.h" +#include "viewconstants.h" +#include "viewsettings.h" + +#include +#include + +#include +#include +#include + +#include +#include + +using namespace Qt3DRender; +using namespace Qt3DCore; + +GlScope::GlScope(Settings::ZoomViewSettings *markers, const Settings::View *view, const Settings::Colors *colors, + QSize renderSize) + : m_view(view), m_colors(colors), m_markers(markers), coords(renderSize), + m_grid(new GlScopeGrid(colors, m_zoomViewLayer, m_scene.data())) { + if (markers) + QObject::connect(markers, &Settings::ZoomViewSettings::markerChanged, m_grid, + [this](int activeMarker) { updateMarkers(activeMarker); }); +} + +void GlScope::init() { + + ///// Setup frame graph + Qt3DLogic::QLogicAspect *m_logicAspect = new Qt3DLogic::QLogicAspect; + m_logicAspect->setObjectName("logicAspect"); + m_aspectEngine->registerAspect(m_logicAspect); + + QRenderAspect *m_renderAspect = new QRenderAspect; + m_renderAspect->setObjectName("renderAspect"); + m_aspectEngine->registerAspect(m_renderAspect); + + // The pipeline chain will destroy itself, no delete required or allowed + + m_techniqueFilter = QPointer(new QTechniqueFilter); + + QFilterKey *forwardRenderingStyle = new QFilterKey; + forwardRenderingStyle->setName(QStringLiteral("renderingStyle")); + forwardRenderingStyle->setValue(QStringLiteral("forward")); + m_techniqueFilter->addMatch(forwardRenderingStyle); + + m_containerViewport->setNormalizedRect(QRectF(0, 0, 1, 1)); + m_mainViewport->setNormalizedRect(QRectF(0, 0, 1, 1)); + + m_clearbuffers = QPointer(new QClearBuffers); + m_clearbuffers->setClearColor(m_colors->background()); + m_clearbuffers->setBuffers(QClearBuffers::ColorDepthBuffer); + m_clearbuffers->setEnabled(true); + + QMatrix4x4 pmvMatrix; + pmvMatrix.ortho(-(DIVS_TIME / 2.0f), (DIVS_TIME / 2.0f), -(DIVS_VOLTAGE / 2.0f), (DIVS_VOLTAGE / 2.0f), -1.0f, + 1.0f); + + m_defaultCamera->setProjectionType(QCameraLens::PerspectiveProjection); + m_defaultCamera->setProjectionMatrix(pmvMatrix); + m_defaultCamera->setFieldOfView(500); + m_defaultCamera->setNearPlane(-1000); + m_defaultCamera->setFarPlane(1000); + m_defaultCamera->setUpVector(QVector3D(0, 1.0, 0)); + // m_defaultCamera->setViewCenter(QVector3D(0.0f, 3.5f, 0.0f)); + // m_defaultCamera->setPosition(QVector3D(0.0f, 3.5f, 25.0f)); + m_defaultCamera->lens()->setProjectionMatrix(pmvMatrix); + m_defaultCamera->setAspectRatio(float(coords.screenSize().width()) / float(coords.screenSize().height())); + + QCameraSelector *cameraSelector = new QCameraSelector; + cameraSelector->setCamera(m_defaultCamera.data()); + + m_renderSettings->setActiveFrameGraph(m_techniqueFilter.data()); + + // Qt3d will create a render-pass for each leaf of the framegraph + + // First branch: Main view + m_clearbuffers->setParent(m_containerViewport); + m_mainViewport->setParent(m_clearbuffers.data()); + cameraSelector->setParent(m_mainViewport); + m_renderCapture->setParent(cameraSelector); + + // Every zoomed view is another branch, that adds to the container view + + ///// Setup scene graph + + m_scene->addComponent(m_renderSettings.data()); + m_scene->addComponent(m_zoomViewLayer); + m_grid->addComponent(m_zoomViewLayer); + m_grid->setParent(m_scene.data()); + + // Last init steps + m_aspectEngine->setRootEntity(QEntityPtr(m_scene.data())); +} + +void GlScope::initWithWindow(QObject *eventSource) { + Qt3DInput::QInputAspect *m_inputAspect = new Qt3DInput::QInputAspect; + m_inputAspect->setObjectName("inputAspect"); + m_aspectEngine->registerAspect(m_inputAspect); + + m_mouseDevice = QPointer(new GlMouseDevice); + m_inputSettings = QPointer(new Qt3DInput::QInputSettings); + m_inputSettings->setEventSource(eventSource); + m_scene->addComponent(m_inputSettings.data()); + + QRenderSurfaceSelector *renderSurfaceSelector = new QRenderSurfaceSelector; + renderSurfaceSelector->setSurface(eventSource); + + init(); + + // First create the common path (TechniqueFilter+MouseDevice+RenderSurface+FrustumCulling+Viewport) + m_mouseDevice->setParent(m_techniqueFilter.data()); + renderSurfaceSelector->setParent(m_mouseDevice.data()); + m_containerViewport->setParent(renderSurfaceSelector); + + QObject::connect(m_colors->observer(), &Observer::changed, &m_signalEmitter, + [this]() { m_clearbuffers->setClearColor(m_colors->background()); }); + QObject::connect(&coords, &ScopeCoordinates::rectChanged, &m_signalEmitter, [this]() { + m_defaultCamera->setAspectRatio(float(coords.screenSize().width()) / float(coords.screenSize().height())); + }); + if (m_markers) updateMarkers(m_markers->activeMarker()); +} + +void GlScope::initWithoutWindow() { + + QRenderSurfaceSelector *renderSurfaceSelector = new QRenderSurfaceSelector; + QRenderTargetSelector *renderTargetSelector = new QRenderTargetSelector; + auto renderTarget = new QRenderTarget(renderTargetSelector); + renderTargetSelector->setTarget(renderTarget); + + auto texture = new QTexture2D(renderTarget); + texture->setSize(coords.screenSize().width(), coords.screenSize().height()); + texture->setFormat(QAbstractTexture::RGBA8_UNorm); + texture->setMinificationFilter(QAbstractTexture::Linear); + texture->setMagnificationFilter(QAbstractTexture::Linear); + + auto renderTargetOutput = new QRenderTargetOutput(renderTarget); + renderTargetOutput->setAttachmentPoint(QRenderTargetOutput::Color0); + renderTargetOutput->setTexture(texture); + renderTarget->addOutput(renderTargetOutput); + + auto textureDep = new QTexture2D(renderTarget); + textureDep->setSize(coords.screenSize().width(), coords.screenSize().height()); + textureDep->setFormat(QAbstractTexture::D24); + textureDep->setMinificationFilter(QAbstractTexture::Linear); + textureDep->setMagnificationFilter(QAbstractTexture::Linear); + textureDep->setComparisonFunction(QAbstractTexture::CompareLessEqual); + textureDep->setComparisonMode(QAbstractTexture::CompareRefToTexture); + + auto renderTargetOutputDep = new QRenderTargetOutput(renderTarget); + renderTargetOutputDep->setAttachmentPoint(QRenderTargetOutput::Depth); + renderTargetOutputDep->setTexture(textureDep); + renderTarget->addOutput(renderTargetOutputDep); + + m_offscreenContext = QPointer(new QOpenGLContext); + m_offscreenContext->setFormat(QSurfaceFormat::defaultFormat()); + m_offscreenContext->create(); + + m_offscreenSurface = QPointer(new QOffscreenSurface); + m_offscreenSurface->setFormat(QSurfaceFormat::defaultFormat()); + m_offscreenSurface->create(); + renderSurfaceSelector->setSurface(m_offscreenSurface.data()); + renderSurfaceSelector->setExternalRenderTargetSize(coords.screenSize()); + + m_offscreenContext->makeCurrent(m_offscreenSurface.data()); + + init(); + + // First create the common path (TechniqueFilter+RenderSurface+FrustumCulling+Viewport) + renderTargetSelector->setParent(m_techniqueFilter.data()); + renderSurfaceSelector->setParent(renderTargetSelector); + m_containerViewport->setParent(renderSurfaceSelector); + if (m_markers) updateMarkers(m_markers->activeMarker()); +} + +GlScope::~GlScope() { + // Clean up offscreen buffers + if (m_offscreenContext) { + m_offscreenContext->doneCurrent(); + delete m_offscreenContext; + delete m_offscreenSurface; + } + // Detach scene graph from frame graph + m_aspectEngine->setRootEntity(nullptr); + + // Clean up scene graph. All children including graphs, the grid, markers, zoom views will be destroyed + delete m_scene; + + // Clean up frame graph (camera, viewports, clearbuffer) + // delete m_zoomViewLayer; + // delete m_mainViewport; + // delete m_containerViewport; + + delete m_renderCapture; + delete m_defaultCamera; + + delete m_clearbuffers; + delete m_mouseDevice; + delete m_techniqueFilter; + delete m_renderSettings; + + delete m_inputSettings; + delete m_aspectEngine; +} + +void GlScope::updateMarkers(int activeMarker) { + // Delete entity entries of not existing markers + auto markerEntityIterator = m_markerEntities.begin(); + while (markerEntityIterator != m_markerEntities.end()) { + if (!m_markers->contains(markerEntityIterator->first)) { + markerEntityIterator->second->destroy(); + markerEntityIterator = m_markerEntities.erase(markerEntityIterator); + } else { + ++markerEntityIterator; + } + } + + // Create new entities + for (auto markerIterator = m_markers->begin(); markerIterator != m_markers->end(); ++markerIterator) { + markerEntityIterator = m_markerEntities.find(markerIterator->first); + if (markerEntityIterator != m_markerEntities.end()) continue; + + // If not marker+zoomview with matching unique id found: Create new entry. The entry + // contains the Qt3d entities for the marker frame and for the zoom view viewport. + const unsigned markerID = markerIterator->first; + MarkerAndZoom *localMarkerAndZoom = new MarkerAndZoom(markerID, &markerIterator->second, this); + QObject::connect(localMarkerAndZoom, &MarkerAndZoom::requestActive, m_markers, + &Settings::ZoomViewSettings::setActiveMarker); + QObject::connect(localMarkerAndZoom, &MarkerAndZoom::requestRemove, m_markers, + &Settings::ZoomViewSettings::removeMarker); + QObject::connect(localMarkerAndZoom, &MarkerAndZoom::userChangedGeometry, m_markers, [this, markerID]() { + if ((int)markerID == m_markers->activeMarker()) m_markers->notifyDataChanged(); + }); + QObject::connect(m_markers, &Settings::ZoomViewSettings::activeMarkerChanged, localMarkerAndZoom, + &MarkerAndZoom::updateActive); + m_markerEntities.put(markerID, localMarkerAndZoom); + } + + /// Used to assign zoomviews to a position, different to other default-placed zoomviews + unsigned countZoomViews = 0; + /// For every zoom view / marker: Synchronize position, geometry, shown region etc. + for (MarkerAndZoomMap::value_type &entry : m_markerEntities) { + entry.second->update(countZoomViews++, activeMarker); + } + updateZoomViewsSnap(); +} + +void GlScope::showData(std::shared_ptr data) { + // Remove too much entries + const unsigned history = m_view->digitalPhosphorDraws(); + while (history < m_GraphHistory.size()) { + delete m_GraphHistory.back(); + m_GraphHistory.pop_back(); + } + + // Add if missing + if (history > m_GraphHistory.size()) { + m_GraphHistory.push_front(new GlScopeGraph(m_scene, m_colors, m_view, m_zoomViewLayer)); + } + + // Move last entry to front + if (history > 1) { + m_GraphHistory.push_front(m_GraphHistory.back()); + m_GraphHistory.pop_back(); + } + + // Apply new data to first graph + m_GraphHistory.front()->writeData(data.get()); + + int index = 0; + for (GlScopeGraph *g : m_GraphHistory) g->setColorAlpha(1.0f - (index++) * 0.2f); +} + +Settings::ZoomViewSettings *GlScope::zoomViewSettings() { return m_markers; } + +void GlScope::updateZoomViewsSnap() { + double shrinkLeft = 0; + double shrinkTop = 0; + double shrinkRight = 0; + double shrinkBottom = 0; + + QRectF viewPortRect = coords.fixedScopeRect(); + QRectF mouseAdjustRect = viewPortRect; + + for (MarkerAndZoomMap::value_type &entry : m_markerEntities) { + const QRectF &rect = entry.second->zoomviewPosition(); + double h = rect.height(); + double w = rect.width(); + EdgePositionFlags snaps = entry.second->snapState(); + if (snaps / EdgePositionFlags::Left) { + if (shrinkLeft < w) shrinkLeft = w; + } else if (snaps / EdgePositionFlags::Right) { + if (shrinkRight < w) shrinkRight = w; + } else if (snaps / EdgePositionFlags::Top) { + if (shrinkTop < h) shrinkTop = h; + } else if (snaps / EdgePositionFlags::Bottom) { + if (shrinkBottom < h) shrinkBottom = h; + } + } + + // Marker zoom views may be snapped to one or more edges. Make the main viewport smaller now. + viewPortRect.setLeft(viewPortRect.left() + shrinkLeft); + viewPortRect.setRight(viewPortRect.right() - shrinkRight); + viewPortRect.setTop(viewPortRect.top() + shrinkTop); + viewPortRect.setBottom(viewPortRect.bottom() - shrinkBottom); + + m_mainViewport->setNormalizedRect(ScopeCoordinates::computeNormalizedRect(viewPortRect, coords.fixedScopeRect())); + + // The viewport changed its size, therefore we need to adjust the mainview coordinate system + // by virtually extending its size, so that the mouse positions within the actual smaller area + // will still be translated to correct coordinates for mouse hover/click events. + double xR = mouseAdjustRect.width() / viewPortRect.width(); + double yR = mouseAdjustRect.height() / viewPortRect.height(); + + shrinkLeft *= xR; + shrinkRight *= xR; + shrinkTop *= yR; + shrinkBottom *= yR; + + // Compute + mouseAdjustRect.setLeft(mouseAdjustRect.left() - shrinkLeft); + mouseAdjustRect.setWidth(mouseAdjustRect.width() + shrinkRight); + mouseAdjustRect.setTop(mouseAdjustRect.top() - shrinkTop); + mouseAdjustRect.setBottom(mouseAdjustRect.bottom() + shrinkBottom); + coords.setScopeRect(mouseAdjustRect); +} + +GlScopeWindow::GlScopeWindow(Settings::ZoomViewSettings *markers, const Settings::View *view, + const Settings::Colors *colors) + : GlScope(markers, view, colors, size()) { + setSurfaceType(QSurface::OpenGLSurface); + initWithWindow(this); + + connect(signalEmitter(), &GlScopeSignalEmitter::requestMouseCursor, this, + [this](Qt::CursorShape shape) { setCursor(QCursor(shape)); }); + + // Qt3DExtras::QFirstPersonCameraController *p = new Qt3DExtras::QFirstPersonCameraController(m_scene); + // p->setCamera(m_defaultCamera); + // p->setParent(m_scene); +} + +void GlScopeWindow::resizeEvent(QResizeEvent *) { coords.updateScreenSize(size()); } diff --git a/openhantek/src/scopeview/glscope.h b/openhantek/src/scopeview/glscope.h new file mode 100644 index 00000000..ac7e4eee --- /dev/null +++ b/openhantek/src/scopeview/glscope.h @@ -0,0 +1,216 @@ +// SPDX-License-Identifier: GPL-2.0+ + +#pragma once + +#include +#include +#include + +#include +#include +#include +#include + +#include +#include +#include + +#include +#include +#include +#include + +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "glmousedevice.h" +#include "glscopegraph.h" +#include "glscopegrid.h" +#include "glscopezoomviewport.h" +#include "hantekdso/enums.h" +#include "hantekprotocol/types.h" +#include "utils/scopecoordinates.h" + +namespace Settings { +class View; +struct Colors; +struct MarkerAndZoom; +class ZoomViewSettings; +} +class PPresult; + +/** + * Because we do not want GlScope to inherit from a QObject (to make it inheritable by a QObject class like QWindow) + * but still want to send signals, we use a delegate object. + */ +class GlScopeSignalEmitter : public QObject { + Q_OBJECT + signals: + /** + * @brief Request a fitting mouse cursor shape. + * @param shape The cursor shape + */ + void requestMouseCursor(Qt::CursorShape shape); + /** + * @brief Request to show a status text message + * @param text The status text + */ + void requestStatusText(const QString &text); +}; + +/** + * The GlScope class is responsible for drawing the oscilloscope screen, markers + * and zoom views and interacting with mouse events. It can also be used as an + * offscreen renderer to export an image of the scope. You need to either call + * initWithWindow() or initWithoutWindow() to finialize the initialisation. + */ +class GlScope { + friend class MarkerAndZoom; + + public: + /// \brief Initializes the scope window. + /// It is required that you call one of the init*() methods as well to create the frame graph. + /// + /// \param markers The marker settings that should be used. Can be null. + /// \param view The view settings that should be used (phosphor depth, etc) + /// \param colors The colors that will be used + /// \param renderSize The initial window/backbuffer size. You need to syncronize the window size to the + /// coords object for further render size adjustments. + GlScope(Settings::ZoomViewSettings *markers, const Settings::View *view, const Settings::Colors *colors, QSize renderSize); + virtual ~GlScope(); + GlScope(const GlScope &) = delete; + + /** + * Call this method if you are using a QSurface backed object like a QWindow to draw the scope on. + */ + void initWithWindow(QObject *eventSource); + /** + * Call this method if you want to draw to an offscreen buffer. Retrieve the image via the capture() + * method. + */ + void initWithoutWindow(); + + /** + * Show new post processed data + * @param data + */ + void showData(std::shared_ptr data); + + /** + * Captures the rendered scene. Use QRenderCaptureReply::image + * to retrieve the image. Usually the very first capture is invalid. + */ + inline QPointer capture() { + return QPointer(m_renderCapture->requestCapture()); + } + + /** + * GlScope does not inherit from QObject, so can't emit signals. Use this signal delegate object + * to connect to the signals instead. + */ + inline GlScopeSignalEmitter *signalEmitter() { return &m_signalEmitter; } + + /** + * @return Return the mouse device + */ + inline GlMouseDevice *mouseDevice() { return m_mouseDevice.data(); } + + /** + * @return Return the associated color palette + */ + inline const Settings::Colors *colors() const { return m_colors; } + + /** + * @return Return the zoom view settings + */ + Settings::ZoomViewSettings *zoomViewSettings(); + + /** + * Zoom views can snap to the edges of the scope screen. A zoom view will call this + * method as soon as it's snap property changed to update the geometry of the main scope view. + */ + void updateZoomViewsSnap(); + + protected: + // User settings + const Settings::View *m_view; + const Settings::Colors *m_colors; + Settings::ZoomViewSettings *m_markers; + ScopeCoordinates coords; ///< Screen to scope coordinate system converter + + // Signal delegate + GlScopeSignalEmitter m_signalEmitter; + + // Aspects + QPointer m_aspectEngine = QPointer(new Qt3DCore::QAspectEngine); + + // Renderer configuration + QPointer m_renderSettings = + QPointer(new Qt3DRender::QRenderSettings); + QPointer m_defaultCamera = QPointer(new Qt3DRender::QCamera); + QPointer m_renderCapture = + QPointer(new Qt3DRender::QRenderCapture); + QPointer m_techniqueFilter; + QPointer m_clearbuffers; + + // Viewports and layers + Qt3DRender::QViewport *m_containerViewport = new Qt3DRender::QViewport; + Qt3DRender::QViewport *m_mainViewport = new Qt3DRender::QViewport; + Qt3DRender::QLayer *m_zoomViewLayer = new Qt3DRender::QLayer; + + // Input + QPointer m_mouseDevice = nullptr; + QPointer m_inputSettings = nullptr; + + // Scene graph + QPointer m_scene = + QPointer(new Qt3DCore::QEntity); ///< root scene graph object + GlScopeGrid *m_grid; + + MarkerAndZoomMap m_markerEntities; + std::list m_GraphHistory; + + // Offscreen objects + QPointer m_offscreenContext; + QPointer m_offscreenSurface; + + /// Initializes the frame graph objects, camera and scene graph. The frame graph itself + /// is not tied together and no input init takes place. + void init(); + /** + * @brief A scope view can have an arbitrary number of embedded zoomed views. + * A zoom view will be created for each marker rectangle. This method synchronizes + * scope->markers with m_zoomEntities. + */ + void updateMarkers(int activeMarker); +}; + +/// \brief Qt3D accelerated window that displays the oscilloscope screen. +/// Hint for Qt4 developers: Use a QWindow in a legacy QWidget application, +/// by the static wrapper funtion: QWidget::createWindowContainer(window). +class GlScopeWindow : public QWindow, public GlScope { + Q_OBJECT + public: + GlScopeWindow(Settings::ZoomViewSettings *markers, const Settings::View *view, const Settings::Colors *colors); + + protected: + void resizeEvent(QResizeEvent *) override; +}; diff --git a/openhantek/src/scopeview/glscopegraph.cpp b/openhantek/src/scopeview/glscopegraph.cpp new file mode 100644 index 00000000..8ba46cc5 --- /dev/null +++ b/openhantek/src/scopeview/glscopegraph.cpp @@ -0,0 +1,133 @@ +#include "glscopegraph.h" +#include "viewsettings.h" + +#include + +#include + +using namespace Qt3DRender; +using namespace Qt3DExtras; +using namespace Qt3DCore; +using namespace Settings; + +GlScopeGraph::GlScopeGraph(QEntity *rootScene, const Colors *colors, const View *view, QLayer *layer) + : m_layer(layer), m_colors(colors), m_view(view) { + if (layer) addComponent(layer); + setParent(rootScene); + connect(colors->observer(), &Observer::changed, this, &GlScopeGraph::applyColors); +} + +GlScopeGraph::~GlScopeGraph() { + while (graphs.size()) graphs.pop_back(); +} + +void GlScopeGraph::writeData(PPresult *data) { + // This implemention tries to minimize allocations especially on consecutive writes of this GlSopeGraph. + // Spectrum and voltage channel graphs are generated and accessible for other writes via a linked + // list. The list is expanded and shrunk if necessary. The semantic of an entry is not fixed. + // If the user disables for instance a voltage channel that is inbetween other entries, then a spectrum + // channel may overwrite what was a voltage graph before. + auto it = graphs.begin(); + for (DataChannel &dataChannel : *data) { + if (it == graphs.end()) + it = graphs.insert(it, new ChannelDetail(dataChannel.voltage.graph, m_layer, m_view, this)); + (*it)->channelID = dataChannel.channelID; + (*it)->isSpectrum = false; + (*it)->updateGraph(dataChannel.voltage.graph); + ++it; + + if (it == graphs.end()) + it = graphs.insert(it, new ChannelDetail(dataChannel.spectrum.graph, m_layer, m_view, this)); + (*it)->channelID = dataChannel.channelID; + (*it)->isSpectrum = true; + (*it)->updateGraph(dataChannel.spectrum.graph); + ++it; + } + while (it != graphs.end()) { + delete *it; + it = graphs.erase(it); + } + + applyColors(); +} + +void GlScopeGraph::setColorAlpha(float alpha) { + m_alpha = alpha; + applyColors(); +} + +// inline QColor applyAlpha(const QColor &c, float alpha) { +// return QColor::fromRgbF(c.redF(), c.greenF(), c.blueF(), std::min((float)c.alphaF(), alpha)); +//} + +void GlScopeGraph::applyColors(const Observer *) { + for (ChannelDetail *c : graphs) { + c->material->setAmbient(c->isSpectrum ? m_colors->spectrum(c->channelID) : m_colors->voltage(c->channelID)); + c->material->setAlpha(m_alpha); + } +} + +void GlScopeGraph::ChannelDetail::updateGraph(const ChannelGraph &channelData) { + if (!channelData.size()) { + setEnabled(false); + return; + } + + if (tempBuffer.size() < channelData.size() * sizeof(QVector3D)) { + tempBuffer.resize(channelData.size() * sizeof(QVector3D)); + memcpy(tempBuffer.data(), (char *)channelData.data(), channelData.size() * sizeof(QVector3D)); + dataBuffer->setData(tempBuffer); + } else { + // QByteArray b;b.resize(channelData.size() * sizeof(QVector3D)); + memcpy(tempBuffer.data(), (char *)channelData.data(), channelData.size() * sizeof(QVector3D)); + dataBuffer->setData(tempBuffer); + } + + if (meshInit != channelData.size()) { + const unsigned s = (unsigned)channelData.size(); + meshInit = s; + mesh->setVertexCount((int)s); + attr->setCount((int)s); + } + + setEnabled(true); +} + +GlScopeGraph::ChannelDetail::ChannelDetail(const ChannelGraph &channelData, QLayer *layer, const View *view, + QEntity *parent) { + meshInit = (unsigned)channelData.size(); + + if (layer) addComponent(layer); + + // Data buffer + dataBuffer->setUsage(Qt3DRender::QBuffer::DynamicDraw); + + // Mesh + attr = new QAttribute(dataBuffer, QAttribute::defaultPositionAttributeName(), QAttribute::Float, 3, 0); + mesh = new QGeometryRenderer(this); + auto geometry = new QGeometry(mesh); + geometry->addAttribute(attr); + mesh->setVertexCount(0); + + // Set the interpolation type (line or points). React to settings changes. + auto iChangedFun = [this](const View *view) { + switch (view->interpolation()) { + case Dso::InterpolationMode::OFF: + mesh->setPrimitiveType(QGeometryRenderer::Points); + break; + case Dso::InterpolationMode::LINEAR: + mesh->setPrimitiveType(QGeometryRenderer::LineStrip); + break; + case Dso::InterpolationMode::SINC: + mesh->setPrimitiveType(QGeometryRenderer::LineStrip); + break; + } + }; + connect(view, &View::interpolationChanged, mesh, iChangedFun); + iChangedFun(view); + + mesh->setGeometry(geometry); + addComponent(mesh); + addComponent(material); + setParent(parent); +} diff --git a/openhantek/src/scopeview/glscopegraph.h b/openhantek/src/scopeview/glscopegraph.h new file mode 100644 index 00000000..837b80e9 --- /dev/null +++ b/openhantek/src/scopeview/glscopegraph.h @@ -0,0 +1,56 @@ +#pragma once + +#include "post/ppresult.h" +#include + +#include +#include +#include +#include +#include +#include +#include + +namespace Settings { +struct Colors; +class View; +} +class Observer; + +class GlScopeGraph : public Qt3DCore::QEntity { + Q_OBJECT + public: + GlScopeGraph(Qt3DCore::QEntity *rootScene, const Settings::Colors *colors, const Settings::View *view, + Qt3DRender::QLayer *layer = nullptr); + ~GlScopeGraph(); + GlScopeGraph(GlScopeGraph &&) = default; + GlScopeGraph(const GlScopeGraph &) = delete; + void writeData(PPresult *data); + void setColorAlpha(float alpha); + + private: + struct ChannelDetail : public Qt3DCore::QEntity { + ChannelDetail(const ChannelGraph &channelData, Qt3DRender::QLayer *m_layer, const Settings::View *view,Qt3DCore::QEntity*parent); + ChannelDetail(ChannelDetail &&) = default; + ChannelDetail(const ChannelDetail &) = delete; + void updateGraph(const ChannelGraph &channelData); + Qt3DRender::QBuffer *dataBuffer = new Qt3DRender::QBuffer(Qt3DRender::QBuffer::VertexBuffer, this); + Qt3DExtras::QPhongAlphaMaterial *material = new Qt3DExtras::QPhongAlphaMaterial(this); + Qt3DRender::QGeometryRenderer *mesh; + Qt3DRender::QAttribute* attr; + unsigned meshInit = 0; + ChannelID channelID; + bool isSpectrum = false; + QByteArray tempBuffer; + }; + + private: + std::list graphs; + float m_alpha = 1.0; + Qt3DRender::QLayer *m_layer; + const Settings::Colors *m_colors; + const Settings::View *m_view; + + private: + void applyColors(const Observer *target = nullptr); +}; diff --git a/openhantek/src/scopeview/glscopegrid.cpp b/openhantek/src/scopeview/glscopegrid.cpp new file mode 100644 index 00000000..38d15e32 --- /dev/null +++ b/openhantek/src/scopeview/glscopegrid.cpp @@ -0,0 +1,237 @@ +#include "glscopegrid.h" +#include "viewconstants.h" +#include "viewsettings.h" + +#include +#include +#include +#include + +using namespace Qt3DRender; +using namespace Qt3DExtras; +using namespace Qt3DCore; + +GlScopeGrid::GlScopeGrid(const Settings::Colors *colors, Qt3DRender::QLayer *layer, QEntity *parent) + : QEntity(parent), m_colors(colors), m_layer(layer) { + createSubDivDots(); + createSubDivLines(); + createAxes(); + createBorder(); +} + +void GlScopeGrid::createSubDivDots() { + auto lines = new Qt3DCore::QEntity(); + if (m_layer) lines->addComponent(m_layer); + // material + Qt3DExtras::QPhongMaterial *material = new Qt3DExtras::QPhongMaterial(lines); + material->setAmbient(m_colors->grid()); + QObject::connect(m_colors->observer(), &Observer::changed, material, + [this, material]() { material->setAmbient(m_colors->grid()); }); + lines->addComponent(material); + // buffer + auto dataBuffer = new QBuffer(QBuffer::VertexBuffer, this); + auto dataBufferNormals = new QBuffer(QBuffer::VertexBuffer, this); + + unsigned elements = 0; + constexpr int subDotSize = 4 * int(DIVS_TIME / 2 - 1) * int(DIVS_VOLTAGE / 2 * DIVS_SUB - 1) + + 4 * int(DIVS_VOLTAGE / 2 - 1) * int(DIVS_TIME / 2 * DIVS_SUB - 1); + + QByteArray vertextData; + vertextData.resize(sizeof(QVector3D) * subDotSize); + QVector3D *vertextDataF = reinterpret_cast(vertextData.data()); + + QVector3D n(0.0f, 0.0f, 1.0f); + QByteArray normalData; + normalData.resize(sizeof(QVector3D) * subDotSize); + QVector3D *normalDataF = reinterpret_cast(normalData.data()); + + // Draw vertical dot lines + for (int div = 1; div < DIVS_TIME / 2; ++div) { + for (int dot = 1; dot < DIVS_VOLTAGE / 2 * DIVS_SUB; ++dot) { + float dotPosition = (float)dot / DIVS_SUB; + elements += 4; + *(vertextDataF++) = QVector3D(-div, -dotPosition, -0.2f); + *(vertextDataF++) = QVector3D(-div, dotPosition, -0.2f); + *(vertextDataF++) = QVector3D(div, -dotPosition, -0.2f); + *(vertextDataF++) = QVector3D(div, dotPosition, -0.2f); + + *(normalDataF++) = n; + *(normalDataF++) = n; + *(normalDataF++) = n; + *(normalDataF++) = n; + } + } + // Draw horizontal dot lines + for (int div = 1; div < DIVS_VOLTAGE / 2; ++div) { + for (int dot = 1; dot < DIVS_TIME / 2 * DIVS_SUB; ++dot) { + if (dot % DIVS_SUB == 0) continue; // Already done by vertical lines + float dotPosition = (float)dot / DIVS_SUB; + elements += 4; + *(vertextDataF++) = (QVector3D(-dotPosition, -div, -0.2f)); + *(vertextDataF++) = (QVector3D(dotPosition, -div, -0.2f)); + *(vertextDataF++) = (QVector3D(-dotPosition, div, -0.2f)); + *(vertextDataF++) = (QVector3D(dotPosition, div, -0.2f)); + + *(normalDataF++) = n; + *(normalDataF++) = n; + *(normalDataF++) = n; + *(normalDataF++) = n; + } + } + vertextData.resize(int(elements * sizeof(QVector3D))); + normalData.resize(int(elements * sizeof(QVector3D))); + dataBuffer->setData(vertextData); + dataBufferNormals->setData(normalData); + auto attr = new QAttribute(dataBuffer, QAttribute::defaultPositionAttributeName(), QAttribute::Float, 3, elements); + auto attrNorm = + new QAttribute(dataBufferNormals, QAttribute::defaultNormalAttributeName(), QAttribute::Float, 3, elements); + auto mesh = new QGeometryRenderer(lines); + auto geometry = new QGeometry(mesh); + geometry->addAttribute(attr); + geometry->addAttribute(attrNorm); + mesh->setVertexCount((int)elements); + mesh->setPrimitiveType(QGeometryRenderer::Points); + mesh->setGeometry(geometry); + lines->addComponent(mesh); + lines->setParent(this); +} + +void GlScopeGrid::createSubDivLines() { + + Qt3DExtras::QPhongMaterial *materialAxes = new Qt3DExtras::QPhongMaterial(this); + materialAxes->setAmbient(m_colors->axes()); + QObject::connect(m_colors->observer(), &Observer::changed, materialAxes, + [this, materialAxes]() { materialAxes->setAmbient(m_colors->axes()); }); + + QByteArray geomArray; + const int numV = (int(DIVS_TIME * DIVS_SUB) - 1 + int(DIVS_VOLTAGE * DIVS_SUB) - 1) * 2; + geomArray.resize(numV * sizeof(QVector3D)); + QVector3D *lineVp = reinterpret_cast(geomArray.data()); + + QByteArray geomArrayN; + geomArrayN.resize(numV * sizeof(QVector3D)); + QVector3D *lineNp = reinterpret_cast(geomArrayN.data()); + + // Subdiv lines on horizontal axis + for (int lineIndex = int(-DIVS_TIME / 2 * DIVS_SUB); lineIndex < int(DIVS_TIME / 2 * DIVS_SUB); ++lineIndex) { + if (lineIndex == 0) continue; + *(lineVp++) = QVector3D((float)lineIndex / DIVS_SUB, -0.1f, 0); + *(lineVp++) = QVector3D((float)lineIndex / DIVS_SUB, +0.1f, 0); + *(lineNp++) = QVector3D(0, 0, 1); + *(lineNp++) = QVector3D(0, 0, 1); + } + // Subdiv lines on vertical axis + for (int lineIndex = int(-DIVS_VOLTAGE / 2 * DIVS_SUB); lineIndex < int(DIVS_VOLTAGE / 2 * DIVS_SUB); ++lineIndex) { + if (lineIndex == 0) continue; + *(lineVp++) = QVector3D(-0.1f, (float)lineIndex / DIVS_SUB, 0); + *(lineVp++) = QVector3D(+0.1f, (float)lineIndex / DIVS_SUB, 0); + *(lineNp++) = QVector3D(0, 0, 1); + *(lineNp++) = QVector3D(0, 0, 1); + } + + auto sublines = new Qt3DCore::QEntity(); + if (m_layer) sublines->addComponent(m_layer); + + QBuffer *dataBuffer = new QBuffer(QBuffer::VertexBuffer, sublines); + dataBuffer->setData(geomArray); + QBuffer *dataBufferN = new QBuffer(QBuffer::VertexBuffer, sublines); + dataBufferN->setData(geomArrayN); + + const auto namePosition = QAttribute::defaultPositionAttributeName(); + auto attr = new QAttribute(dataBuffer, namePosition, QAttribute::Float, 3, numV); + const auto nameNormal = QAttribute::defaultNormalAttributeName(); + auto attrN = new QAttribute(dataBufferN, nameNormal, QAttribute::Float, 3, numV); + auto mesh = new QGeometryRenderer(sublines); + auto geometry = new QGeometry(mesh); + geometry->addAttribute(attr); + geometry->addAttribute(attrN); + mesh->setVertexCount((int)numV); + mesh->setPrimitiveType(QGeometryRenderer::Lines); + mesh->setGeometry(geometry); + + sublines->addComponent(mesh); + sublines->addComponent(materialAxes); + sublines->setParent(this); +} + +void GlScopeGrid::createAxes() { + Qt3DExtras::QPhongMaterial *materialAxes = new Qt3DExtras::QPhongMaterial(this); + materialAxes->setAmbient(m_colors->axes()); + QObject::connect(m_colors->observer(), &Observer::changed, materialAxes, + [this, materialAxes]() { materialAxes->setAmbient(m_colors->axes()); }); + + const int numV = 4; + const float frameV[] = {-0.5, 0, 0, +0.5, 0, 0, // horz + 0, -0.5, 0, 0, +0.5, 0, // vert + 0, 0, 1, 0, 0, 1, 0, 0, 1, 0, 0, 1}; // normales + + auto sublines = new Qt3DCore::QEntity(); + if (m_layer) sublines->addComponent(m_layer); + + QByteArray geomArray; + geomArray.setRawData((char *)frameV, sizeof(frameV)); + QBuffer *dataBuffer = new QBuffer(QBuffer::VertexBuffer, sublines); + dataBuffer->setData(geomArray); + + const auto namePosition = QAttribute::defaultPositionAttributeName(); + auto attr = new QAttribute(dataBuffer, namePosition, QAttribute::Float, 3, numV, 0); + const auto nameNormal = QAttribute::defaultNormalAttributeName(); + auto attrN = new QAttribute(dataBuffer, nameNormal, QAttribute::Float, 3, numV, numV * sizeof(QVector3D)); + auto mesh = new QGeometryRenderer(sublines); + auto geometry = new QGeometry(mesh); + geometry->addAttribute(attr); + geometry->addAttribute(attrN); + mesh->setVertexCount((int)numV); + mesh->setPrimitiveType(QGeometryRenderer::Lines); + mesh->setGeometry(geometry); + + auto transform = new Qt3DCore::QTransform(sublines); + transform->setScale3D(QVector3D(DIVS_TIME, DIVS_VOLTAGE, 0)); + + sublines->addComponent(transform); + sublines->addComponent(mesh); + sublines->addComponent(materialAxes); + sublines->setParent(this); +} + +void GlScopeGrid::createBorder() { + auto line = new Qt3DCore::QEntity(); + if (m_layer) line->addComponent(m_layer); + + Qt3DExtras::QPhongMaterial *materialBorder = new Qt3DExtras::QPhongMaterial(line); + materialBorder->setAmbient(m_colors->border()); + QObject::connect(m_colors->observer(), &Observer::changed, materialBorder, + [this, materialBorder]() { materialBorder->setAmbient(m_colors->border()); }); + + const int numV = 8; + const float frameV[] = {0, 0, 0, 1, 0, 0, 1, 0, 0, 1, 1, 0, // lines + 1, 1, 0, 0, 1, 0, 0, 1, 0, 0, 0, 0, // lines + 0, 0, 1, 0, 0, 1, 0, 0, 1, 0, 0, 1, // normales + 0, 0, 1, 0, 0, 1, 0, 0, 1, 0, 0, 1}; // normales + + QByteArray geomArray; + geomArray.setRawData((char *)frameV, sizeof(frameV)); + QBuffer *dataBuffer = new QBuffer(QBuffer::VertexBuffer, line); + dataBuffer->setData(geomArray); + + const auto namePosition = QAttribute::defaultPositionAttributeName(); + auto attr = new QAttribute(dataBuffer, namePosition, QAttribute::Float, 3, numV, 0, 0); + const auto nameNormal = QAttribute::defaultNormalAttributeName(); + auto attrN = new QAttribute(dataBuffer, nameNormal, QAttribute::Float, 3, numV, numV * sizeof(QVector3D)); + auto mesh = new QGeometryRenderer(line); + auto geometry = new QGeometry(mesh); + geometry->addAttribute(attr); + geometry->addAttribute(attrN); + mesh->setVertexCount((int)numV); + mesh->setPrimitiveType(QGeometryRenderer::Lines); + mesh->setGeometry(geometry); + + auto transform = new Qt3DCore::QTransform(line); + transform->setTranslation(QVector3D(-DIVS_TIME / 2, -DIVS_VOLTAGE / 2, 0)); + transform->setScale3D(QVector3D(DIVS_TIME, DIVS_VOLTAGE, 0)); + + line->addComponent(mesh); + line->addComponent(materialBorder); + line->addComponent(transform); + line->setParent(this); +} diff --git a/openhantek/src/scopeview/glscopegrid.h b/openhantek/src/scopeview/glscopegrid.h new file mode 100644 index 00000000..66097eb5 --- /dev/null +++ b/openhantek/src/scopeview/glscopegrid.h @@ -0,0 +1,24 @@ +#pragma once + +#include +#include +#include +#include + +namespace Settings { +struct Colors; +} + +class GlScopeGrid : public Qt3DCore::QEntity { + Q_OBJECT + public: + GlScopeGrid(const Settings::Colors *colors, Qt3DRender::QLayer *layer = nullptr, Qt3DCore::QEntity *parent = nullptr); + + private: + const Settings::Colors *m_colors; + Qt3DRender::QLayer *m_layer; + void createSubDivDots(); + void createSubDivLines(); + void createAxes(); + void createBorder(); +}; diff --git a/openhantek/src/scopeview/glscopehover.h b/openhantek/src/scopeview/glscopehover.h new file mode 100644 index 00000000..44a1ca04 --- /dev/null +++ b/openhantek/src/scopeview/glscopehover.h @@ -0,0 +1,24 @@ +#pragma once + +#include + +enum class EdgePositionFlags : int { + None = 0, + Left = 1, + Right = 2, + Top = 4, + Bottom = 8, + Middle = Left | Right | Top | Bottom +}; + +using T = std::underlying_type::type; +inline EdgePositionFlags operator|(EdgePositionFlags lhs, EdgePositionFlags rhs) { + return (EdgePositionFlags)(static_cast(lhs) | static_cast(rhs)); +} +inline EdgePositionFlags &operator|=(EdgePositionFlags &lhs, EdgePositionFlags rhs) { + lhs = (EdgePositionFlags)(static_cast(lhs) | static_cast(rhs)); + return lhs; +} +inline bool operator/(EdgePositionFlags lhs, EdgePositionFlags rhs) { + return (EdgePositionFlags)(static_cast(lhs) & static_cast(rhs)) == rhs; +} diff --git a/openhantek/src/scopeview/glscopezoomviewport.cpp b/openhantek/src/scopeview/glscopezoomviewport.cpp new file mode 100644 index 00000000..a19375c6 --- /dev/null +++ b/openhantek/src/scopeview/glscopezoomviewport.cpp @@ -0,0 +1,127 @@ +#include "glscopezoomviewport.h" +#include "glscope.h" +#include "iconfont/QtAwesome.h" +#include "settings/colorsettings.h" +#include "settings/scopesettings.h" +#include "settings/viewsettings.h" + +#include +#include +#include + +using namespace Qt3DRender; +using namespace Qt3DExtras; +using namespace Qt3DCore; + +MarkerAndZoom::MarkerAndZoom(unsigned markerID, Settings::MarkerAndZoom *markerSetting, GlScope *scope) + : m_markerSetting(markerSetting), m_markerID(markerID), m_scope(scope), + mouseHandler(&m_zoomviewPosition, &m_markerID, scope->mouseDevice(), new ScopeCoordinates(scope->coords), + scope->m_scene.data()) { + ///// Setup camera with matching settings to the mainview ///// + zoomCamera->setFieldOfView(1000); + zoomCamera->setNearPlane(0.1f); + zoomCamera->setFarPlane(1000); + zoomCamera->setViewCenter(QVector3D(0, 0, 0)); + zoomCamera->setUpVector(QVector3D(0, 1.0, 0)); + + ///// Setup viewport with camera and layerfilter (to not show the marker frame) ///// + + QLayerFilter *layerFilter = new QLayerFilter(viewport); + layerFilter->addLayer(scope->m_zoomViewLayer); + + Qt3DRender::QCameraSelector *cameraSelector = new Qt3DRender::QCameraSelector(viewport); + cameraSelector->setCamera(zoomCamera); + + cameraSelector->setParent(layerFilter); + layerFilter->setParent(viewport); + viewport->setParent(scope->m_containerViewport); + + const Settings::Colors *colors = scope->colors(); + marker = new GlFrame(scope->mouseDevice(), &scope->coords, &colors->markers(), &colors->markerHover(), + &colors->markerSelected(), &colors->markerActive(), colors->observer(), nullptr, + scope->m_zoomViewLayer, scope->m_scene.data()); + marker->setFrameIndex(markerID); + + m_removeBtn = new GlIcon(scope->mouseDevice(), &scope->coords, &colors->zoomBackground(), &colors->zoomHover(), + &colors->zoomSelected(), colors->observer(), iconFont->icon(fa::trash), nullptr, + scope->m_scene.data()); + + GlMoveResizeSnapSignals *zvSignals = mouseHandler.moveResizeSignals(); + + connect(zvSignals, &GlMoveResizeSnapSignals::frameChanged, viewport, [this, scope](unsigned, const QRectF &rect) { + m_markerSetting->zoomRect = rect; + moveZoomview(rect); + }); + + connect(zvSignals, &GlMoveResizeSnapSignals::requestMouseCursor, &scope->m_signalEmitter, + &GlScopeSignalEmitter::requestMouseCursor); + + connect(zvSignals, &GlMoveResizeSnapSignals::snapChanged, this, [this]() { m_scope->updateZoomViewsSnap(); }); + + connect(marker->moveResizeSignals(), &GlMoveResizeSnapSignals::frameChanged, marker, + [scope, this](unsigned, const QRectF &rect) { + m_markerSetting->markerRect = rect; + m_removeBtn->updatePosition(QPointF(rect.x(), rect.y())); + updateZoomRegion(rect); + emit userChangedGeometry(); + }); + + connect(marker->moveResizeSignals(), &GlMoveResizeSnapSignals::requestMouseCursor, scope->signalEmitter(), + &GlScopeSignalEmitter::requestMouseCursor); + + connect(marker, &GlFrame::activated, this, &MarkerAndZoom::requestActive); + + connect(m_removeBtn->moveResizeSignals(), &GlMoveResizeSnapSignals::hovered, m_removeBtn, + [scope]() { emit scope->signalEmitter()->requestStatusText(QCoreApplication::tr("Remove zoom view")); }); + + connect(m_removeBtn->moveResizeSignals(), &GlMoveResizeSnapSignals::clicked, this, + [this]() { emit requestRemove(m_markerID); }); +} + +MarkerAndZoom::~MarkerAndZoom() { delete mouseHandler.coordinateSystem(); } + +void MarkerAndZoom::destroy() { + delete marker; + delete m_removeBtn; + delete viewport; + viewport = nullptr; + m_removeBtn = nullptr; + marker = nullptr; +} + +void MarkerAndZoom::updateZoomRegion(const QRectF &marker) { + QMatrix4x4 zoomViewMatrix; + zoomViewMatrix.ortho(marker.left(), marker.right(), marker.top(), marker.bottom(), -1.0f, +1.0f); + zoomCamera->setProjectionMatrix(zoomViewMatrix); +} + +bool MarkerAndZoom::moveZoomview(const QRectF &position) { + const QRectF m = mouseHandler.coordinateSystem()->scopeRect(); + + if (position.height() > m.height() || position.width() > m.width()) return false; + if (position.top() < m.top() || position.bottom() > m.bottom()) return false; + if (position.right() > m.right() || position.left() < m.left()) return false; + if (position.height() < 0.01 || position.width() < 0.01) return false; + + m_zoomviewPosition = position; + viewport->setNormalizedRect(ScopeCoordinates::computeNormalizedRect(m_zoomviewPosition, m)); + mouseHandler.updateSnap(); + return true; +} + +void MarkerAndZoom::update(unsigned positionIndexIfNoSavedPos, int activeMarker) { + // A new zoom view should default to 1/4 width of the scope screen and 1/5 height + // and be placed in the right bottom corner. + const float w = DIVS_TIME / 4; + const float h = DIVS_VOLTAGE / 5; + + // Move the viewport and update the shown region + if (m_markerSetting->zoomRect.isNull() || !moveZoomview(m_markerSetting->zoomRect)) + moveZoomview(QRectF(DIVS_TIME / 2 - w, -DIVS_VOLTAGE / 2 + h * positionIndexIfNoSavedPos, w, h)); + updateZoomRegion(m_markerSetting->markerRect); + marker->updateRectangle(m_markerSetting->markerRect); + m_removeBtn->updatePosition(QPointF(marker->rect()->x(), marker->rect()->y())); + updateActive(activeMarker); +} + +void MarkerAndZoom::updateActive(int activeMarker) { marker->setActive(activeMarker == (int)m_markerID); } diff --git a/openhantek/src/scopeview/glscopezoomviewport.h b/openhantek/src/scopeview/glscopezoomviewport.h new file mode 100644 index 00000000..7c9d8ec8 --- /dev/null +++ b/openhantek/src/scopeview/glscopezoomviewport.h @@ -0,0 +1,84 @@ +#pragma once + +#include + +#include +#include +#include + +#include "glframe.h" +#include "glicon.h" +#include "glmoveresizesnap.h" +#include "utils/scopecoordinates.h" + +namespace Settings { +class View; +struct Colors; +struct MarkerAndZoom; +class ZoomViewSettings; +} +class GlMouseDevice; +class GlScope; + +class MarkerAndZoom : public QObject { + Q_OBJECT + public: + MarkerAndZoom(unsigned markerID, Settings::MarkerAndZoom *markerSetting, GlScope *scope); + MarkerAndZoom(const MarkerAndZoom &) = delete; + virtual ~MarkerAndZoom(); + + /// Updates the positions and geometry of the zoomview and the marker frame. + /// This is necessary after creating a new MarkerAndZoom object. + void update(unsigned positionIndexIfNoSavedPos = 0, int activeMarker = -1); + void updateActive(int activeMarker); + + /// Return the coordinates and geometry of the viewport + inline const QRectF zoomviewPosition() const { return m_zoomviewPosition; } + + inline EdgePositionFlags snapState() const { return mouseHandler.snapState(); } + + /// Usually the resources are free'd due to parent/child relationship. This is not the case if + /// the zoomview/marker is deleted by the user. Call destroy in this case. + void destroy(); + + private: + /** + * Updates the shown region of this zoom view + * @param marker The marker region in scope coordinates + */ + void updateZoomRegion(const QRectF &marker); + + /** + * Move/resize this zoom view + * @param position Position and geometry in scope coordinates + * @return Return true if move was successful, false otherwise. If the geometry or coodinates + * are out of the scope coordinate boundaries, this function returns false. + */ + bool moveZoomview(const QRectF &position); + + // State: marker ID and settings marker pointer + Settings::MarkerAndZoom *m_markerSetting; + unsigned m_markerID; + + // Widgets + GlScope *m_scope; + GlFrame *marker = nullptr; + GlIcon *m_removeBtn = nullptr; + + QRectF m_zoomviewPosition; + + // 3D viewport and input + Qt3DRender::QViewport *viewport = new Qt3DRender::QViewport; // will be deleted by parent + Qt3DRender::QCamera *zoomCamera = new Qt3DRender::QCamera; // will be deleted by parent + GlMoveResizeSnap mouseHandler; + signals: + void requestRemove(unsigned markerId); + void requestActive(unsigned markerId); + void userChangedGeometry(); +}; + +struct MarkerAndZoomMap : public std::map> { + inline void put(unsigned markerID, MarkerAndZoom *mz) { + insert(MarkerAndZoomMap::value_type(markerID, std::unique_ptr(mz))); + } +}; diff --git a/openhantek/src/settings.cpp b/openhantek/src/settings.cpp deleted file mode 100644 index 0d8b4498..00000000 --- a/openhantek/src/settings.cpp +++ /dev/null @@ -1,267 +0,0 @@ -// SPDX-License-Identifier: GPL-2.0+ - -#include -#include -#include -#include - -#include "settings.h" - -#include "dsowidget.h" - -/// \brief Set the number of channels. -/// \param channels The new channel count, that will be applied to lists. -DsoSettings::DsoSettings(const Dso::ControlSpecification* deviceSpecification) { - // Add new channels to the list - while (scope.spectrum.size() < deviceSpecification->channels) { - // Spectrum - DsoSettingsScopeSpectrum newSpectrum; - newSpectrum.name = QApplication::tr("SP%1").arg(scope.spectrum.size()+1); - scope.spectrum.push_back(newSpectrum); - - // Voltage - DsoSettingsScopeVoltage newVoltage; - newVoltage.name = QApplication::tr("CH%1").arg(scope.voltage.size()+1); - scope.voltage.push_back(newVoltage); - - view.screen.voltage.push_back(QColor::fromHsv((int)(scope.spectrum.size()-1) * 60, 0xff, 0xff)); - view.screen.spectrum.push_back(view.screen.voltage.back().lighter()); - view.print.voltage.push_back(view.screen.voltage.back().darker(120)); - view.print.spectrum.push_back(view.screen.voltage.back().darker()); - } - - DsoSettingsScopeSpectrum newSpectrum; - newSpectrum.name = QApplication::tr("SPM"); - scope.spectrum.push_back(newSpectrum); - - DsoSettingsScopeVoltage newVoltage; - newVoltage.couplingOrMathIndex = (unsigned)Dso::MathMode::ADD_CH1_CH2; - newVoltage.name = QApplication::tr("MATH"); - scope.voltage.push_back(newVoltage); - - view.screen.voltage.push_back(QColor(0x7f, 0x7f, 0x7f, 0xff)); - view.screen.spectrum.push_back(view.screen.voltage.back().lighter()); - view.print.voltage.push_back(view.screen.voltage.back()); - view.print.spectrum.push_back(view.print.voltage.back().darker()); - - - load(); -} - -bool DsoSettings::setFilename(const QString &filename) { - std::unique_ptr local = std::unique_ptr(new QSettings(filename, QSettings::IniFormat)); - if (local->status() != QSettings::NoError) { - qWarning() << "Could not change the settings file to " << filename; - return false; - } - store.swap(local); - return true; -} - -void DsoSettings::load() { - // General options - store->beginGroup("options"); - if (store->contains("alwaysSave")) alwaysSave = store->value("alwaysSave").toBool(); - store->endGroup(); - - store->beginGroup("exporting"); - if (store->contains("imageSize")) exporting.imageSize = store->value("imageSize").toSize(); - store->endGroup(); - - // Oscilloscope settings - store->beginGroup("scope"); - // Horizontal axis - store->beginGroup("horizontal"); - if (store->contains("format")) scope.horizontal.format = (Dso::GraphFormat)store->value("format").toInt(); - if (store->contains("frequencybase")) - scope.horizontal.frequencybase = store->value("frequencybase").toDouble(); - for (int marker = 0; marker < 2; ++marker) { - QString name; - name = QString("marker%1").arg(marker); - if (store->contains(name)) scope.horizontal.marker[marker] = store->value(name).toDouble(); - } - if (store->contains("timebase")) scope.horizontal.timebase = store->value("timebase").toDouble(); - if (store->contains("recordLength")) scope.horizontal.recordLength = store->value("recordLength").toUInt(); - if (store->contains("samplerate")) scope.horizontal.samplerate = store->value("samplerate").toDouble(); - if (store->contains("samplerateSet")) scope.horizontal.samplerateSource = (DsoSettingsScopeHorizontal::SamplerateSource)store->value("samplerateSet").toInt(); - store->endGroup(); - // Trigger - store->beginGroup("trigger"); - if (store->contains("mode")) scope.trigger.mode = (Dso::TriggerMode)store->value("mode").toUInt(); - if (store->contains("position")) scope.trigger.position = store->value("position").toDouble(); - if (store->contains("slope")) scope.trigger.slope = (Dso::Slope)store->value("slope").toUInt(); - if (store->contains("source")) scope.trigger.source = store->value("source").toUInt(); - if (store->contains("special")) scope.trigger.special = store->value("special").toInt(); - store->endGroup(); - // Spectrum - for (ChannelID channel = 0; channel < scope.spectrum.size(); ++channel) { - store->beginGroup(QString("spectrum%1").arg(channel)); - if (store->contains("magnitude")) - scope.spectrum[channel].magnitude = store->value("magnitude").toDouble(); - if (store->contains("offset")) scope.spectrum[channel].offset = store->value("offset").toDouble(); - if (store->contains("used")) scope.spectrum[channel].used = store->value("used").toBool(); - store->endGroup(); - } - // Vertical axis - for (ChannelID channel = 0; channel < scope.voltage.size(); ++channel) { - store->beginGroup(QString("vertical%1").arg(channel)); - if (store->contains("gainStepIndex")) scope.voltage[channel].gainStepIndex = store->value("gainStepIndex").toUInt(); - if (store->contains("couplingOrMathIndex")) scope.voltage[channel].couplingOrMathIndex = store->value("couplingIndex").toUInt(); - if (store->contains("inverted")) scope.voltage[channel].inverted = store->value("inverted").toBool(); - if (store->contains("offset")) scope.voltage[channel].offset = store->value("offset").toDouble(); - if (store->contains("trigger")) scope.voltage[channel].trigger = store->value("trigger").toDouble(); - if (store->contains("used")) scope.voltage[channel].used = store->value("used").toBool(); - store->endGroup(); - } - - // Post processing - if (store->contains("spectrumLimit")) post.spectrumLimit = store->value("spectrumLimit").toDouble(); - if (store->contains("spectrumReference")) - post.spectrumReference = store->value("spectrumReference").toDouble(); - if (store->contains("spectrumWindow")) - post.spectrumWindow = (Dso::WindowFunction)store->value("spectrumWindow").toInt(); - store->endGroup(); - - // View - store->beginGroup("view"); - // Colors - store->beginGroup("color"); - DsoSettingsColorValues *colors; - for (int mode = 0; mode < 2; ++mode) { - if (mode == 0) { - colors = &view.screen; - store->beginGroup("screen"); - } else { - colors = &view.print; - store->beginGroup("print"); - } - - if (store->contains("axes")) colors->axes = store->value("axes").value(); - if (store->contains("background")) colors->background = store->value("background").value(); - if (store->contains("border")) colors->border = store->value("border").value(); - if (store->contains("grid")) colors->grid = store->value("grid").value(); - if (store->contains("markers")) colors->markers = store->value("markers").value(); - for (ChannelID channel = 0; channel < scope.spectrum.size(); ++channel) { - QString key = QString("spectrum%1").arg(channel); - if (store->contains(key)) colors->spectrum[channel] = store->value(key).value(); - } - if (store->contains("text")) colors->text = store->value("text").value(); - for (ChannelID channel = 0; channel < scope.voltage.size(); ++channel) { - QString key = QString("voltage%1").arg(channel); - if (store->contains(key)) colors->voltage[channel] = store->value(key).value(); - } - store->endGroup(); - } - store->endGroup(); - // Other view settings - if (store->contains("digitalPhosphor")) view.digitalPhosphor = store->value("digitalPhosphor").toBool(); - if (store->contains("interpolation")) - view.interpolation = (Dso::InterpolationMode)store->value("interpolation").toInt(); - if (store->contains("screenColorImages")) view.screenColorImages = store->value("screenColorImages").toBool(); - if (store->contains("zoom")) view.zoom = (Dso::InterpolationMode)store->value("zoom").toBool(); - store->endGroup(); - - store->beginGroup("window"); - mainWindowGeometry = store->value("geometry").toByteArray(); - mainWindowState = store->value("state").toByteArray(); - store->endGroup(); -} - -void DsoSettings::save() { - // Main window layout and other general options - store->beginGroup("options"); - store->setValue("alwaysSave", alwaysSave); - store->endGroup(); - - store->beginGroup("exporting"); - store->setValue("imageSize", exporting.imageSize); - store->endGroup(); - - // Oszilloskope settings - store->beginGroup("scope"); - // Horizontal axis - store->beginGroup("horizontal"); - store->setValue("format", scope.horizontal.format); - store->setValue("frequencybase", scope.horizontal.frequencybase); - for (int marker = 0; marker < 2; ++marker) - store->setValue(QString("marker%1").arg(marker), scope.horizontal.marker[marker]); - store->setValue("timebase", scope.horizontal.timebase); - store->setValue("recordLength", scope.horizontal.recordLength); - store->setValue("samplerate", scope.horizontal.samplerate); - store->setValue("samplerateSet", (int)scope.horizontal.samplerateSource); - store->endGroup(); - // Trigger - store->beginGroup("trigger"); - store->setValue("mode", (unsigned)scope.trigger.mode); - store->setValue("position", scope.trigger.position); - store->setValue("slope", (unsigned)scope.trigger.slope); - store->setValue("source", scope.trigger.source); - store->setValue("special", scope.trigger.special); - store->endGroup(); - // Spectrum - for (ChannelID channel = 0; channel < scope.spectrum.size(); ++channel) { - store->beginGroup(QString("spectrum%1").arg(channel)); - store->setValue("magnitude", scope.spectrum[channel].magnitude); - store->setValue("offset", scope.spectrum[channel].offset); - store->setValue("used", scope.spectrum[channel].used); - store->endGroup(); - } - // Vertical axis - for (ChannelID channel = 0; channel < scope.voltage.size(); ++channel) { - store->beginGroup(QString("vertical%1").arg(channel)); - store->setValue("gainStepIndex", scope.voltage[channel].gainStepIndex); - store->setValue("couplingOrMathIndex", scope.voltage[channel].couplingOrMathIndex); - store->setValue("inverted", scope.voltage[channel].inverted); - store->setValue("offset", scope.voltage[channel].offset); - store->setValue("trigger", scope.voltage[channel].trigger); - store->setValue("used", scope.voltage[channel].used); - store->endGroup(); - } - - // Post processing - store->setValue("spectrumLimit", post.spectrumLimit); - store->setValue("spectrumReference", post.spectrumReference); - store->setValue("spectrumWindow", (int)post.spectrumWindow); - store->endGroup(); - - // View - store->beginGroup("view"); - // Colors - - store->beginGroup("color"); - DsoSettingsColorValues *colors; - for (int mode = 0; mode < 2; ++mode) { - if (mode == 0) { - colors = &view.screen; - store->beginGroup("screen"); - } else { - colors = &view.print; - store->beginGroup("print"); - } - - store->setValue("axes", colors->axes.name(QColor::HexArgb)); - store->setValue("background", colors->background.name(QColor::HexArgb)); - store->setValue("border", colors->border.name(QColor::HexArgb)); - store->setValue("grid", colors->grid.name(QColor::HexArgb)); - store->setValue("markers", colors->markers.name(QColor::HexArgb)); - for (ChannelID channel = 0; channel < scope.spectrum.size(); ++channel) - store->setValue(QString("spectrum%1").arg(channel), colors->spectrum[channel].name(QColor::HexArgb)); - store->setValue("text", colors->text.name(QColor::HexArgb)); - for (ChannelID channel = 0; channel < scope.voltage.size(); ++channel) - store->setValue(QString("voltage%1").arg(channel), colors->voltage[channel].name(QColor::HexArgb)); - store->endGroup(); - } - store->endGroup(); - - // Other view settings - store->setValue("digitalPhosphor", view.digitalPhosphor); - store->setValue("interpolation", view.interpolation); - store->setValue("screenColorImages", view.screenColorImages); - store->setValue("zoom", view.zoom); - store->endGroup(); - - store->beginGroup("window"); - store->setValue("geometry", mainWindowGeometry); - store->setValue("state", mainWindowState); - store->endGroup(); -} diff --git a/openhantek/src/settings.h b/openhantek/src/settings.h deleted file mode 100644 index 17b2eff7..00000000 --- a/openhantek/src/settings.h +++ /dev/null @@ -1,38 +0,0 @@ -// SPDX-License-Identifier: GPL-2.0+ - -#pragma once - -#include -#include -#include -#include - -#include "exporting/exportsettings.h" -#include "post/postprocessingsettings.h" -#include "scopesettings.h" -#include "viewsettings.h" - -/// \brief Holds the settings of the program. -class DsoSettings { - public: - explicit DsoSettings(const Dso::ControlSpecification *deviceSpecification); - bool setFilename(const QString &filename); - - DsoSettingsExport exporting; ///< General options of the program - DsoSettingsScope scope; ///< All oscilloscope related settings - DsoSettingsView view; ///< All view related settings - DsoSettingsPostProcessing post; ///< All post processing related settings - bool alwaysSave = true; ///< Always save the settings on exit - - QByteArray mainWindowGeometry; ///< Geometry of the main window - QByteArray mainWindowState; ///< State of docking windows and toolbars - - /// \brief Read the settings from the last session or another file. - void load(); - - /// \brief Save the settings to the harddisk. - void save(); - - private: - std::unique_ptr store = std::unique_ptr(new QSettings); -}; diff --git a/openhantek/src/settings/colorsettings.h b/openhantek/src/settings/colorsettings.h new file mode 100644 index 00000000..60c71190 --- /dev/null +++ b/openhantek/src/settings/colorsettings.h @@ -0,0 +1,71 @@ +// SPDX-License-Identifier: GPL-2.0+ + +#pragma once + +#include +#include + +#include "hantekprotocol/types.h" +#include "utils/getwithdefault.h" +#include "utils/observer.h" + +class DsoConfigColorsPage; + +namespace Settings { +/// \brief Holds the color values for the oscilloscope screen. +struct Colors { + // The settings configuration page and settings-reader get full access + friend class ::DsoConfigColorsPage; + friend struct ViewIO; + + Colors() = default; + Colors(const QColor &text, const QColor &axes, const QColor &background, const QColor &border, const QColor &grid, + const QColor &markers, const QColor &markerHover, const QColor &markerSelected, + const QColor &markerActive, // marker colors + const QColor &zoomBackground, const QColor &zoomHover, const QColor &zoomSelected, const QColor &zoomActive) + : _text(text), _axes(axes), _background(background), _border(border), _grid(grid), // everything else + _markers(markers), _markerHover(markerHover), _markerSelected(markerSelected), + _markerActive(markerActive), // markers + _zoomBackground(zoomBackground), _zoomHover(zoomHover), _zoomSelected(zoomSelected), _zoomActive(zoomActive), + _observer(this) {} + + inline const QColor &text() const { return _text; } + inline const QColor &axes() const { return _axes; } + inline const QColor &background() const { return _background; } + inline const QColor &border() const { return _border; } + inline const QColor &grid() const { return _grid; } + inline const QColor &markers() const { return _markers; } + inline const QColor &markerHover() const { return _markerHover; } + inline const QColor &markerSelected() const { return _markerSelected; } + inline const QColor &markerActive() const { return _markerActive; } + inline const QColor &zoomBackground() const { return _zoomBackground; } + inline const QColor &zoomHover() const { return _zoomHover; } + inline const QColor &zoomSelected() const { return _zoomSelected; } + inline const QColor &zoomActive() const { return _zoomActive; } + inline const QColor &spectrum(ChannelID channelID) const { + return GetWithDef(_spectrum, channelID, _spectrum.at(0)); + } + inline const QColor &voltage(ChannelID channelID) const { return GetWithDef(_voltage, channelID, _voltage.at(0)); } + inline void setVoltage(ChannelID channelID, const QColor &color) { _voltage[channelID] = color; } + inline void setSpectrum(ChannelID channelID, const QColor &color) { _spectrum[channelID] = color; } + inline Observer *observer() const { return &_observer; } + + protected: + QColor _text; ///< The default text color + QColor _axes; ///< X- and Y-axis and subdiv lines on them + QColor _background; ///< The scope clear color + QColor _border; ///< The border of the scope screen + QColor _grid; ///< The color of the grid + QColor _markers; ///< The color of the markers + QColor _markerHover; ///< The color of a hovered marker + QColor _markerSelected; ///< The color of a selected marker + QColor _markerActive; ///< The color of an active marker + QColor _zoomBackground; ///< A zoomview has a seperate background layer + QColor _zoomHover; ///< The color of a hovered zoomview + QColor _zoomSelected; ///< When the mouse is pressed down to move/resize a zoomview this color is used + QColor _zoomActive; ///< There can only be one zoomview marked as active, this is the color + std::map _spectrum; ///< The colors of the spectrum graphs + std::map _voltage; ///< The colors of the voltage graphs + mutable Observer _observer; +}; +} diff --git a/openhantek/src/settings/markerandzoomsettings.cpp b/openhantek/src/settings/markerandzoomsettings.cpp new file mode 100644 index 00000000..d1b795fe --- /dev/null +++ b/openhantek/src/settings/markerandzoomsettings.cpp @@ -0,0 +1,18 @@ +#include "markerandzoomsettings.h" +namespace Settings { +void ZoomViewSettings::setActiveMarker(unsigned markerID) { + if (!size() || !contains(markerID)) { + m_activeMarker = -1; + } else + m_activeMarker = (int)markerID; + emit activeMarkerChanged(m_activeMarker); +} + +void ZoomViewSettings::removeMarker(int markerID) { + if (markerID < 0) return; + if (std::map::erase((unsigned)markerID)) { + if ((int)markerID == m_activeMarker) { m_activeMarker = -1; } + emit markerChanged(m_activeMarker); + } +} +} diff --git a/openhantek/src/settings/markerandzoomsettings.h b/openhantek/src/settings/markerandzoomsettings.h new file mode 100644 index 00000000..d69b46c9 --- /dev/null +++ b/openhantek/src/settings/markerandzoomsettings.h @@ -0,0 +1,62 @@ +// SPDX-License-Identifier: GPL-2.0+ + +#pragma once + +#include +#include +#include + +namespace Settings { +struct MarkerAndZoom { + QRectF zoomRect = QRectF(); + QRectF markerRect = QRectF(); + bool visible = true; +}; + +/** + * A zoom view needs a position and geometry for the zoom view window as well as for + * the zoomed area. This is stored together as MarkerAndZoom. ZoomViews stores those MarkerAndZooms + * and notifies if new ones are added or if one is removed. + */ +class ZoomViewSettings : public QObject, private std::map { + Q_OBJECT + public: + ZoomViewSettings() = default; + ZoomViewSettings(const ZoomViewSettings &) = delete; + ZoomViewSettings(const ZoomViewSettings &&) = delete; + + inline MarkerAndZoom &get(unsigned markerID) { return find(markerID)->second; } + inline const MarkerAndZoom &get(unsigned markerID) const { return find(markerID)->second; } + inline void insert(unsigned markerID, MarkerAndZoom &z) { + std::map::insert(std::make_pair(markerID, z)); + emit markerChanged(m_activeMarker); + } + inline void eraseNoNotify(unsigned markerID) { std::map::erase(markerID); } + inline bool contains(unsigned markerID) { return find(markerID) != end(); } + inline size_t size() const { return std::map::size(); } + inline iterator begin() { return std::map::begin(); } + inline iterator end() { return std::map::end(); } + inline const_iterator begin() const { return std::map::begin(); } + inline const_iterator end() const { return std::map::end(); } + + typedef std::pair value_type; + + /// There is always one of the zoomviews that is marked as actived. The GUI will show detailed information + /// about an active zoom view. + void setActiveMarker(unsigned markerID); + void removeMarker(int markerID); + inline int activeMarker() const { return m_activeMarker; } + /// Call this method if the geometry of the current active marker changed + inline void notifyDataChanged() { emit markerDataChanged(m_activeMarker); } + + private: + int m_activeMarker = 0; + signals: + /// This signal is emitted whenever a marker/zoomview is added or removed. The (new) active marker is given. + void markerChanged(int activeMarker); + /// Emitted as soon as the active marker changes or no marker is selected (activeMarker==-1) + void activeMarkerChanged(int activeMarker); + /// Listen to this signal to be notifed if the currently active marker/zoomview changed its geometry + void markerDataChanged(int activeMarker); +}; +} diff --git a/openhantek/src/settings/scopechannel.cpp b/openhantek/src/settings/scopechannel.cpp new file mode 100644 index 00000000..ba850cac --- /dev/null +++ b/openhantek/src/settings/scopechannel.cpp @@ -0,0 +1,100 @@ +#include "scopechannel.h" +#include "hantekdso/channelusage.h" +#include "scopemathchannel.h" + +Settings::Channel::~Channel() { + if (isMathChannel()) delete m_voltage; +} + +Settings::Channel *Settings::Channel::createReal(Dso::ChannelUsage *channelUsage, const Dso::Channel *channel, + ChannelID channelID) { + Channel *v = new Channel; + v->m_voltage = channel; + v->m_channelUsage = channelUsage; + v->m_channelid = channelID; + return v; +} + +Settings::MathChannel *Settings::MathChannel::createMath(Dso::ChannelUsage *channelUsage, ChannelID channelID) { + MathChannel *v = new MathChannel; + v->m_voltage = new Dso::Channel(); + v->m_isMathChannel = true; + v->m_channelid = channelID; + v->m_channelUsage = channelUsage; + return v; +} + +void Settings::MathChannel::setSpectrumVisible(bool visible) { + m_spectrum.m_visible = visible && m_firstChannel && m_secondChannel; + emit spectrum()->visibleChanged(m_spectrum.m_visible); + + if (m_spectrum.m_visible) { + m_channelUsage->addChannelUser(m_first, spectrum()); + m_channelUsage->addChannelUser(m_second, spectrum()); + } else { + if (Q_LIKELY(m_firstChannel)) m_channelUsage->removeChannelUser(m_first, spectrum()); + if (Q_LIKELY(m_secondChannel)) m_channelUsage->removeChannelUser(m_second, spectrum()); + } +} + +void Settings::MathChannel::setVoltageVisible(bool visible) { + m_visible = visible && m_firstChannel && m_secondChannel; + emit visibleChanged(m_visible); + + if (m_visible) { + m_channelUsage->addChannelUser(m_first, this); + m_channelUsage->addChannelUser(m_second, this); + } else { + if (Q_LIKELY(m_firstChannel)) m_channelUsage->removeChannelUser(m_first, this); + if (Q_LIKELY(m_secondChannel)) m_channelUsage->removeChannelUser(m_second, this); + } +} + +void Settings::MathChannel::setOffset(double offset) { const_cast(m_voltage)->setOffset(offset); } + +void Settings::MathChannel::setMathMode(::PostProcessing::MathMode e) { + m_mode = e; + emit mathModeChanged(this); +} + +void Settings::MathChannel::setFirstChannel(ChannelID channel, const Dso::Channel *channelPointer) { + m_first = channel; + m_firstChannel = channelPointer; + emit firstChannelChanged(channel); +} + +void Settings::MathChannel::setSecondChannel(ChannelID channel, const Dso::Channel *channelPointer) { + m_second = channel; + m_secondChannel = channelPointer; + emit secondChannelChanged(channel); +} + +void Settings::Channel::setInverted(bool e) { + m_inverted = e; + emit invertedChanged(m_inverted); +} + +void Settings::Channel::setGain(float e) { + m_gain = e; + emit gainChanged(m_gain); +} + +void Settings::Channel::setSpectrumVisible(bool visible) { + m_spectrum.m_visible = visible; + emit spectrum()->visibleChanged(m_spectrum.m_visible); + + if (visible) + m_channelUsage->addChannelUser(m_channelid, spectrum()); + else + m_channelUsage->removeChannelUser(m_channelid, spectrum()); +} + +void Settings::Channel::setVoltageVisible(bool visible) { + m_visible = visible; + emit visibleChanged(m_visible); + + if (visible) + m_channelUsage->addChannelUser(m_channelid, this); + else + m_channelUsage->removeChannelUser(m_channelid, this); +} diff --git a/openhantek/src/settings/scopechannel.h b/openhantek/src/settings/scopechannel.h new file mode 100644 index 00000000..14ac470e --- /dev/null +++ b/openhantek/src/settings/scopechannel.h @@ -0,0 +1,84 @@ +// SPDX-License-Identifier: GPL-2.0+ + +#pragma once + +#include +#include + +#include "hantekdso/devicesettings.h" +#include "hantekdso/enums.h" +#include "spectrum.h" +#include + +class QSettings; +namespace Dso { +class ChannelUsage; +} + +namespace Settings { + +/// \brief Holds the settings for a graph channel, inluding some post processing capabilities. +class Channel : public QObject { + Q_OBJECT + friend struct ScopeIO; + + public: + ~Channel(); + Channel(const Channel &) = default; + static Channel *createReal(Dso::ChannelUsage *channelUsage, const Dso::Channel *channel, ChannelID channelID); + enum { INVALID = UINT_MAX }; + + /// Return true if voltage or spectrum is in use for this channel + inline bool anyVisible() const { return visible() | m_spectrum.visible(); } + virtual void setSpectrumVisible(bool visible); + virtual void setVoltageVisible(bool visible); + + inline bool visible() const { return m_visible; } + + /// true if the channel is inverted (mirrored on cross-axis) + inline bool inverted() const { return m_inverted; } + void setInverted(bool e); + + /// Screen gain value. Defaults to 1.0. Do not confuse this with the hardware gain value + /// to be found in devicespec->gain[channel->voltage()->gainStepIndex()].gain, which can be + /// set by DsoControl->setGain(channelId, gainStepIndex). + /// + /// This is nothing more than a simple stretch factor, that is influenced by the voltage gain + /// setup as well as optional probe dividers etc. + inline float gain() const { return m_gain; } + void setGain(float e); + + /// Name of this channel + inline QString name() const { return m_name; } + void setName(const QString &name) { m_name = name; } + + /// Return true if this is a math channel. + inline bool isMathChannel() const { return m_isMathChannel; } + + /// Returns the channel ID. This is usually the position in the channel array, except for math + /// channels that can be added/removed dynamically. + inline unsigned channelID() const { return m_channelid; } + + inline Spectrum *spectrum() { return &m_spectrum; } + inline const Spectrum *spectrum() const { return &m_spectrum; } + inline const Dso::Channel *voltage() const { return m_voltage; } + + protected: + /// Spectrum channel data + Spectrum m_spectrum; + /// Pointer to device channel (or artificial math channel) for voltage + const Dso::Channel *m_voltage; + Dso::ChannelUsage *m_channelUsage; + QString m_name; ///< Name of this channel + bool m_isMathChannel = false; + bool m_inverted = false; ///< true if the channel is inverted (mirrored on cross-axis) + bool m_visible = false; + float m_gain = 1.0f; + ChannelID m_channelid; ///< The channel id. This is usually just the position in the channel array. + Channel() = default; + signals: + void visibleChanged(bool visible); + void invertedChanged(bool inverted); + void gainChanged(float gainValue); +}; +} diff --git a/openhantek/src/settings/scopemathchannel.h b/openhantek/src/settings/scopemathchannel.h new file mode 100644 index 00000000..64a91dd1 --- /dev/null +++ b/openhantek/src/settings/scopemathchannel.h @@ -0,0 +1,49 @@ +// SPDX-License-Identifier: GPL-2.0+ + +#pragma once + +#include +#include + +#include "post/enums.h" +#include "scopechannel.h" + +class QSettings; + +namespace Settings { + +class MathChannel : public Channel { + Q_OBJECT + friend struct ScopeIO; + + public: + static MathChannel *createMath(Dso::ChannelUsage *channelUsage, ChannelID channelID); + + virtual void setSpectrumVisible(bool visible) override; + virtual void setVoltageVisible(bool visible) override; + + void setOffset(double offset); + + /// mode for math-channels + inline ::PostProcessing::MathMode mathMode() const { return m_mode; } + void setMathMode(::PostProcessing::MathMode e); + + inline ChannelID firstID() const { return m_first; } + void setFirstChannel(ChannelID channel, const Dso::Channel *channelPointer); + + inline ChannelID secondID() const { return m_second; } + void setSecondChannel(ChannelID channel, const Dso::Channel *channelPointer); + + private: + ::PostProcessing::MathMode m_mode = ::PostProcessing::MathMode::ADD; ///< mode for math-channels + ChannelID m_first = Channel::INVALID; ///< For storing/restoring + ChannelID m_second = Channel::INVALID; ///< For storing/restoring + const Dso::Channel *m_firstChannel = nullptr; ///< Pointer to real channel + const Dso::Channel *m_secondChannel = nullptr; ///< Pointer to real channel + + signals: + void mathModeChanged(const Channel *); + void firstChannelChanged(ChannelID channelid); + void secondChannelChanged(ChannelID channelid); +}; +} diff --git a/openhantek/src/settings/scopesettings.cpp b/openhantek/src/settings/scopesettings.cpp new file mode 100644 index 00000000..3b263982 --- /dev/null +++ b/openhantek/src/settings/scopesettings.cpp @@ -0,0 +1,146 @@ +// SPDX-License-Identifier: GPL-2.0+ + +#include +#include +#include +#include + +#include "scopesettings.h" +#include "utils/enumhelper.h" + +void Settings::ScopeIO::read(QSettings *store, Scope &scope, const Dso::DeviceSettings *deviceSpecification, + Dso::ChannelUsage *channelUsage) { + scope.m_channels.clear(); + // Add new channels to the list + for (ChannelID i = 0; i < deviceSpecification->voltage.size(); ++i) { + Channel *newVoltage = Channel::createReal(channelUsage, deviceSpecification->voltage[i], i); + newVoltage->m_name = QApplication::tr("CH%1").arg(scope.m_channels.size() + 1); + newVoltage->spectrum()->m_name = QApplication::tr("SP%1").arg(scope.m_channels.size() + 1); + scope.m_channels[i] = std::shared_ptr(newVoltage); + } + + // Oscilloscope settings + store->beginGroup("scope"); + scope.m_format = loadForEnum(store, "format", scope.format()); + scope.m_frequencybase = store->value("frequencybase", scope.frequencybase()).toDouble(); + + // Vertical axis + unsigned channelCount = (unsigned)store->beginReadArray("channels"); + for (ChannelID channel = 0; channel < channelCount; ++channel) { + store->setArrayIndex((int)channel); + auto c = scope.m_channels[channel]; + if (!c) { + c = std::shared_ptr(MathChannel::createMath(channelUsage, channel)); + scope.m_channels[channel] = c; + } + store->beginGroup("spectrum"); + c->spectrum()->m_name = store->value("name", c->spectrum()->name()).toString(); + c->spectrum()->m_magnitude = store->value("magnitude", c->spectrum()->magnitude()).toDouble(); + c->spectrum()->m_offset = store->value("offset", c->spectrum()->offset()).toDouble(); + c->spectrum()->m_visible = store->value("used", c->spectrum()->visible()).toBool(); + store->endGroup(); + store->beginGroup("voltage"); + c->m_name = store->value("name", c->name()).toString(); + c->m_visible = store->value("used", c->m_visible).toBool(); + c->m_inverted = store->value("inverted", c->m_inverted).toBool(); + if (c->isMathChannel()) { + MathChannel *mc = ((MathChannel *)c.get()); + mc->m_mode = loadForEnum(store, "mathMode", mc->m_mode); + mc->m_first = store->value("first", mc->m_first).toUInt(); + mc->m_second = store->value("second", mc->m_second).toUInt(); + if (mc->m_first >= deviceSpecification->voltage.size()) mc->m_first = 0; + if (mc->m_second >= deviceSpecification->voltage.size()) mc->m_second = 1; + mc->m_firstChannel = deviceSpecification->voltage[mc->m_first]; + mc->m_secondChannel = deviceSpecification->voltage[mc->m_second]; + } + store->endGroup(); + } + store->endArray(); + store->endGroup(); // end "scope" +} + +void Settings::ScopeIO::write(QSettings *store, const Scope &scope) { + // Oszilloskope settings + store->beginGroup("scope"); + store->setValue("format", enumName(scope.format())); + store->setValue("frequencybase", scope.frequencybase()); + + // Vertical axis + store->beginWriteArray("channels", (int)scope.m_channels.size()); + int newChannelIndex = 0; + for (const Channel *channel : scope) { + store->setArrayIndex(newChannelIndex); + + store->beginGroup("spectrum"); + store->setValue("magnitude", channel->spectrum()->magnitude()); + store->setValue("offset", channel->spectrum()->offset()); + store->setValue("used", channel->spectrum()->visible()); + store->setValue("name", channel->spectrum()->name()); + store->endGroup(); + + store->beginGroup("voltage"); + store->setValue("name", channel->name()); + store->setValue("used", channel->visible()); + store->setValue("inverted", channel->m_inverted); + if (channel->m_isMathChannel) { + MathChannel *mchannel = ((MathChannel *)channel); + store->setValue("mathMode", enumName(mchannel->m_mode)); + store->setValue("first", mchannel->m_first); + store->setValue("second", mchannel->m_second); + } + store->endGroup(); + + ++newChannelIndex; + } + store->endArray(); + + store->endGroup(); +} + +Settings::MathChannel *Settings::Scope::addMathChannel(Dso::ChannelUsage *channelUsage, + const Dso::DeviceSettings *deviceSettings) { + ChannelID highest = m_channels.size() ? m_channels.rbegin()->first : 0; + MathChannel *m = MathChannel::createMath(channelUsage, highest + 1); + m->setName(tr("Math %1").arg(highest)); + m->setFirstChannel(0, deviceSettings->voltage[0]); + m->setSecondChannel(1, deviceSettings->voltage[1]); + m_channels[m->channelID()] = std::shared_ptr(m); + emit mathChannelAdded(m); + return m; +} + +void Settings::Scope::removeMathChannel(ChannelID channelID) { + for (auto it = m_channels.begin(); it != m_channels.end(); ++it) { + if (it->first == channelID) { + it->second->setVoltageVisible(false); + it->second->setSpectrumVisible(false); + m_channels.erase(it); + break; + } + } +} + +void Settings::Scope::setFormat(Dso::GraphFormat v) { + m_format = v; + emit formatChanged(this); +} + +void Settings::Scope::setFrequencybase(double v) { + m_frequencybase = v; + emit frequencybaseChanged(this); +} + +void Settings::Scope::setUseHardwareGainSteps(bool v) { + m_useHardwareGain = v; + emit useHardwareGainChanged(v); +} + +void Settings::Spectrum::setMagnitude(double v) { + m_magnitude = v; + emit magnitudeChanged(this); +} + +void Settings::Spectrum::setOffset(double v) { + m_offset = v; + emit offsetChanged(this); +} diff --git a/openhantek/src/settings/scopesettings.h b/openhantek/src/settings/scopesettings.h new file mode 100644 index 00000000..e2f897bb --- /dev/null +++ b/openhantek/src/settings/scopesettings.h @@ -0,0 +1,87 @@ +// SPDX-License-Identifier: GPL-2.0+ + +#pragma once + +#include +#include + +#include "hantekdso/enums.h" +#include "hantekprotocol/definitions.h" +#include "scopechannel.h" +#include "scopemathchannel.h" +#include "utils/getwithdefault.h" +#include +#include +#include + +class QSettings; + +namespace Settings { + +/** + * @brief Holds the settings for the oscilloscope. + * Access this class object only from the main gui thread! + */ +class Scope : public QObject { + Q_OBJECT + friend struct ScopeIO; + Scope(const Scope &) = delete; + Scope(const Scope &&) = delete; + + public: + Scope() = default; + using ChannelMap = std::map>; + using iterator = map_iterator; + using const_iterator = map_iterator; + using const_reverse_iterator = map_iterator; + + inline const std::shared_ptr channel(ChannelID channel) const { return m_channels.at(channel); } + inline std::shared_ptr channel(ChannelID channel) { return m_channels[channel]; } + + inline iterator begin() { return make_map_iterator(m_channels.begin()); } + inline iterator end() { return make_map_iterator(m_channels.end()); } + + inline const_iterator begin() const { return make_map_const_iterator(m_channels.begin()); } + inline const_iterator end() const { return make_map_const_iterator(m_channels.end()); } + + inline const_reverse_iterator rbegin() const { return make_map_const_iterator(m_channels.rbegin()); } + inline const_reverse_iterator rend() const { return make_map_const_iterator(m_channels.rend()); } + + inline const ChannelMap &channels() const { return m_channels; } + + MathChannel *addMathChannel(Dso::ChannelUsage *channelUsage, const Dso::DeviceSettings *deviceSettings); + void removeMathChannel(ChannelID channelID); + + /// Graph drawing mode of the scope + inline Dso::GraphFormat format() const { return m_format; } + void setFormat(Dso::GraphFormat v); + + /// Frequencybase in Hz/div + inline double frequencybase() const { return m_frequencybase; } + void setFrequencybase(double v); + + /// If enabled, use only the natively supported hardware gain steps. + /// Use predefined 200mV...5V non-native gain steps otherwise (which are mapped to the hardware gain steps) + inline bool useHardwareGainSteps() { return m_useHardwareGain; } + void setUseHardwareGainSteps(bool v); + + private: + /// Settings for the channels of the graphs + ChannelMap m_channels; + + Dso::GraphFormat m_format = Dso::GraphFormat::TY; ///< Graph drawing mode of the scope + double m_frequencybase = 1e3; ///< Frequencybase in Hz/div + bool m_useHardwareGain = false; + signals: + void formatChanged(const Scope *); + void frequencybaseChanged(const Scope *); + void mathChannelAdded(Settings::MathChannel *channel); + void useHardwareGainChanged(bool useHardwareGainSteps); +}; + +struct ScopeIO { + static void read(QSettings *io, Scope &scope, const Dso::DeviceSettings *deviceSpecification, + Dso::ChannelUsage *channelUsage); + static void write(QSettings *io, const Scope &scope); +}; +} diff --git a/openhantek/src/settings/settings.cpp b/openhantek/src/settings/settings.cpp new file mode 100644 index 00000000..04347ed0 --- /dev/null +++ b/openhantek/src/settings/settings.cpp @@ -0,0 +1,68 @@ +// SPDX-License-Identifier: GPL-2.0+ + +#include +#include +#include +#include + +#include "settings.h" +#include "viewconstants.h" + +bool Settings::DsoSettings::setFilename(const QString &filename) { + std::unique_ptr local = std::unique_ptr(new QSettings(filename, QSettings::IniFormat)); + if (local->status() != QSettings::NoError) { + qWarning() << "Could not change the settings file to " << filename; + return false; + } + store.swap(local); + return true; +} + +/// \brief Set the number of channels. +/// \param channels The new channel count, that will be applied to lists. +Settings::DsoSettings::DsoSettings(const Dso::ModelSpec *deviceSpecification) { + deviceSettings = std::make_shared(deviceSpecification); +} + +void Settings::DsoSettings::save() { + // Main window layout and other general options + store->beginGroup("options"); + store->setValue("alwaysSave", alwaysSave); + store->endGroup(); + + store->beginGroup("window"); + store->setValue("geometry", mainWindowGeometry); + store->setValue("state", mainWindowState); + store->endGroup(); + + DeviceSettingsIO::write(store.get(), *deviceSettings.get()); + ScopeIO::write(store.get(), scope); + ViewIO::write(store.get(), view); + DsoExportIO::write(store.get(), exporting); + PostProcessing::SettingsIO::write(store.get(), post); +} + +void Settings::DsoSettings::load(Dso::ChannelUsage *channelUsage) { + // General options + store->beginGroup("options"); + if (store->contains("alwaysSave")) alwaysSave = store->value("alwaysSave").toBool(); + store->endGroup(); + + store->beginGroup("window"); + mainWindowGeometry = store->value("geometry").toByteArray(); + mainWindowState = store->value("state").toByteArray(); + store->endGroup(); + + DeviceSettingsIO::read(store.get(), *deviceSettings.get()); + ScopeIO::read(store.get(), scope, deviceSettings.get(), channelUsage); + ViewIO::read(store.get(), view, &scope); + DsoExportIO::read(store.get(), exporting); + PostProcessing::SettingsIO::read(store.get(), post); + /// Initally, after loading, the stored channel settings contain information about + /// enabled/disabled (visible/hidden) physical and math channels. Those are not + /// in sync with the deviceSettings usage information. We force a manual sync here. + for (Channel *c : scope) { + c->setVoltageVisible(c->visible()); + c->setSpectrumVisible(c->spectrum()->visible()); + } +} diff --git a/openhantek/src/settings/settings.h b/openhantek/src/settings/settings.h new file mode 100644 index 00000000..e3c0443a --- /dev/null +++ b/openhantek/src/settings/settings.h @@ -0,0 +1,45 @@ +// SPDX-License-Identifier: GPL-2.0+ + +#pragma once + +#include +#include +#include +#include + +#include "exporting/exportsettings.h" +#include "post/postprocessingsettings.h" +#include "scopesettings.h" +#include "viewsettings.h" + +namespace Settings { +/// \brief Holds the settings of the program. +class DsoSettings { + DsoSettings(const DsoSettings &) = delete; + DsoSettings(const DsoSettings &&) = delete; + + public: + explicit DsoSettings(const Dso::ModelSpec *deviceSpecification); + bool setFilename(const QString &filename); + + /// All device related settings. This is shared with DsoControl and manipulated through DsoControl. + /// You should not modify anything directly, as soon as DsoControl is running, except the channel usages. + std::shared_ptr deviceSettings; + Scope scope; ///< All oscilloscope related settings + View view; ///< All view related settings + DsoExport exporting; ///< General options of the program + PostProcessing::Settings post; ///< All post processing related settings + + bool alwaysSave = true; ///< Always save the settings on exit + QByteArray mainWindowGeometry; ///< Geometry of the main window + QByteArray mainWindowState; ///< State of docking windows and toolbars + + /// \brief Save settings to the underlying QSettings file. + void save(); + /// \brief Load settings from the underlying QSettings file. + void load(Dso::ChannelUsage *channelUsage); + + private: + std::unique_ptr store = std::unique_ptr(new QSettings); +}; +} diff --git a/openhantek/src/settings/spectrum.h b/openhantek/src/settings/spectrum.h new file mode 100644 index 00000000..dee216b8 --- /dev/null +++ b/openhantek/src/settings/spectrum.h @@ -0,0 +1,39 @@ +// SPDX-License-Identifier: GPL-2.0+ + +#pragma once + +#include +#include +#include + +namespace Settings { +/// \brief Holds the settings for the spectrum analysis post processing. +class Spectrum : public QObject { + Q_OBJECT + friend struct ScopeIO; + friend class Channel; + friend class MathChannel; + + public: + void setName(const QString &name) { m_name = name; } + inline const QString &name() const { return m_name; } + + void setMagnitude(double v); + inline double magnitude() const { return m_magnitude; } + + void setOffset(double v); + inline double offset() const { return m_offset; } + + inline bool visible() const { return m_visible; } + + protected: + QString m_name; ///< Name of this channel + double m_magnitude = 20.0; ///< The vertical resolution in dB/div + double m_offset = 0.0; ///< Vertical offset in divs + bool m_visible = false; ///< true if the spectrum is turned on + signals: + void magnitudeChanged(const Spectrum *); + void offsetChanged(const Spectrum *); + void visibleChanged(bool visible); +}; +} diff --git a/openhantek/src/settings/viewsettings.cpp b/openhantek/src/settings/viewsettings.cpp new file mode 100644 index 00000000..74bce763 --- /dev/null +++ b/openhantek/src/settings/viewsettings.cpp @@ -0,0 +1,165 @@ +// SPDX-License-Identifier: GPL-2.0+ + +#include +#include +#include +#include + +#include "settings.h" +#include "viewconstants.h" + +void Settings::ViewIO::readColor(QSettings *store, Settings::Colors *colors, const Settings::Scope *scope) { + colors->_axes = store->value("axes", colors->_axes).value(); + colors->_background = store->value("background", colors->_background).value(); + colors->_border = store->value("border", colors->_border).value(); + colors->_grid = store->value("grid", colors->_grid).value(); + colors->_markers = store->value("markers", colors->_markers).value(); + colors->_markerActive = store->value("markerActive", colors->_markerActive).value(); + colors->_markerHover = store->value("markerHover", colors->_markerHover).value(); + colors->_markerSelected = store->value("markerSelected", colors->_markerSelected).value(); + colors->_zoomBackground = store->value("zoomBackground", colors->_zoomBackground).value(); + colors->_zoomActive = store->value("zoomActive", colors->_zoomActive).value(); + colors->_zoomHover = store->value("zoomHover", colors->_zoomHover).value(); + colors->_zoomSelected = store->value("zoomSelected", colors->_zoomSelected).value(); + colors->_text = store->value("text", colors->_text).value(); + + QString key; + for (auto *c : *scope) { + key = QString("spectrum%1").arg(c->channelID()); + colors->_spectrum[c->channelID()] = store->value(key, colors->spectrum(c->channelID())).value(); + key = QString("voltage%1").arg(c->channelID()); + colors->_voltage[c->channelID()] = store->value(key, colors->voltage(c->channelID())).value(); + } +} + +void Settings::ViewIO::read(QSettings *store, View &view, const Settings::Scope *scope) { + syncChannels(view, scope); + QObject::connect(scope, &Settings::Scope::mathChannelAdded, &view, [&view, scope]() { syncChannels(view, scope); }); + // View + store->beginGroup("view"); + // Colors + store->beginGroup("color"); + + store->beginGroup("screen"); + readColor(store, &view.screen, scope); + store->endGroup(); + + store->beginGroup("print"); + readColor(store, &view.print, scope); + store->endGroup(); + + store->endGroup(); // color + + unsigned markerSize = (unsigned)store->beginReadArray("markers"); + for (unsigned i = 0; i < markerSize; ++i) { + store->setArrayIndex((int)i); + MarkerAndZoom z; + z.zoomRect = store->value("zoomview", z.zoomRect).toRectF(); + z.visible = store->value("visible", z.visible).toBool(); + z.markerRect = store->value("pos", z.markerRect).toRectF(); + view.zoomviews.insert(i, z); + } + store->endArray(); + + // Other view settings + view.m_digitalPhosphor = store->value("digitalPhosphor", view.m_digitalPhosphor).toBool(); + view.m_digitalPhosphorDepth = + std::max((unsigned)2, store->value("digitalPhosphorDepth", view.m_digitalPhosphorDepth).toUInt()); + view.m_interpolation = (Dso::InterpolationMode)store->value("interpolation", (int)view.m_interpolation).toInt(); + view.screenColorImages = store->value("screenColorImages", view.screenColorImages).toBool(); + store->endGroup(); +} + +void Settings::ViewIO::write(QSettings *store, const View &view) { + // View + store->beginGroup("view"); + // Colors + + store->beginGroup("color"); + const Colors *colors; + for (int mode = 0; mode < 2; ++mode) { + if (mode == 0) { + colors = &view.screen; + store->beginGroup("screen"); + } else { + colors = &view.print; + store->beginGroup("print"); + } + + store->setValue("axes", colors->axes()); + store->setValue("background", colors->background()); + store->setValue("border", colors->border()); + store->setValue("grid", colors->grid()); + + store->setValue("markers", colors->markers()); + store->setValue("markerActive", colors->markerActive()); + store->setValue("markerHover", colors->markerHover()); + store->setValue("markerSelected", colors->markerSelected()); + + store->setValue("zoomBackground", colors->zoomBackground()); + store->setValue("zoomActive", colors->zoomActive()); + store->setValue("zoomHover", colors->zoomHover()); + store->setValue("zoomSelected", colors->zoomSelected()); + + store->setValue("text", colors->text()); + + // As soon as we store channels and channel colors, we renumber the indices to [0,n] + int newI = 0; + for (auto &c : colors->_spectrum) store->setValue(QString("spectrum%1").arg(newI++), c.second); + newI = 0; + for (auto &c : colors->_voltage) store->setValue(QString("voltage%1").arg(newI++), c.second); + store->endGroup(); + } + store->endGroup(); + + store->beginWriteArray("markers", (int)view.zoomviews.size()); + auto it = view.zoomviews.begin(); + for (unsigned i = 0; i < view.zoomviews.size(); ++i) { + const MarkerAndZoom &v = it->second; + store->setArrayIndex((int)i); + store->setValue("zoomview", QVariant::fromValue(v.zoomRect)); + store->setValue("pos", QVariant::fromValue(v.markerRect)); + store->setValue("visible", v.visible); + ++it; + } + store->endArray(); + + // Other view settings + store->setValue("digitalPhosphor", view.m_digitalPhosphor); + store->setValue("interpolation", (unsigned)view.m_interpolation); + store->setValue("screenColorImages", view.screenColorImages); + store->endGroup(); +} + +void Settings::ViewIO::syncChannels(Settings::Colors &c, const Settings::Scope *scope) { + // Create copy + auto voltageCopy = c._voltage; + auto spectrumCopy = c._spectrum; + // Clear original + c._voltage.clear(); + c._spectrum.clear(); + // Fill original with existing colors if available or a default color + for (const Settings::Channel *channel : *scope) { + const QColor voltageDefault = QColor::fromHsv((int)(c._voltage.size() - 1) * 60, 0xff, 0xff); + c._voltage.insert( + std::make_pair(channel->channelID(), GetWithDef(voltageCopy, channel->channelID(), voltageDefault))); + c._spectrum.insert(std::make_pair(channel->channelID(), + GetWithDef(spectrumCopy, channel->channelID(), voltageDefault.lighter()))); + } +} + +void Settings::ViewIO::syncChannels(Settings::View &view, const Settings::Scope *scope) { + syncChannels(view.screen, scope); + syncChannels(view.print, scope); +} + +void Settings::View::setInterpolation(Dso::InterpolationMode mode) { + m_interpolation = mode; + emit interpolationChanged(this); +} + +void Settings::View::setDigitalPhosphor(bool enable, unsigned historyDepth) { + m_digitalPhosphor = enable; + m_digitalPhosphorDepth = std::max((unsigned)2, historyDepth); + emit digitalPhosphorChanged(this); +} diff --git a/openhantek/src/settings/viewsettings.h b/openhantek/src/settings/viewsettings.h new file mode 100644 index 00000000..bddaee7d --- /dev/null +++ b/openhantek/src/settings/viewsettings.h @@ -0,0 +1,68 @@ +// SPDX-License-Identifier: GPL-2.0+ + +#pragma once + +#include "markerandzoomsettings.h" +#include "colorsettings.h" +#include "hantekdso/enums.h" +#include +#include + +class QSettings; +namespace Settings { + +/// \brief Holds all view settings. +class View : public QObject { + Q_OBJECT + friend struct ViewIO; + View(const View &) = delete; + View(const View &&) = delete; + + public: + View() = default; + Colors screen = {QColor(0xff, 0xff, 0xff, 0xff), QColor(0xff, 0xff, 0xff, 0x7f), // text, axes + QColor(0x00, 0x00, 0x00, 0xff), QColor(0xff, 0xff, 0xff, 0xff), // bg, border + QColor(0xff, 0xff, 0xff, 0x3f), // grid + QColor(0xff, 0xff, 0xff, 0x0f), QColor(0xff, 0xff, 0xff, 0xff), // markers, markers hover + QColor(0xff, 0x00, 0x00, 0xff), QColor(0xff, 0x00, 0x00, 0xff), // markers sel+active + QColor::fromRgbF(0.3, 0.3, 0.3, 0.1), QColor::fromRgbF(0.1, 0.1, 0.1, 1), // zoom bg, zoom hover + QColor::fromRgbF(0.5, 0.1, 0.1, 1), QColor::fromRgbF(0.1, 0.1, 0.1, 1)}; // zoom sel, zoom active + Colors print = {QColor(0x00, 0x00, 0x00, 0xff), QColor(0x00, 0x00, 0x00, 0xbf), // text, axes + QColor(0xff, 0xff, 0xff, 0xff), QColor(0x00, 0x00, 0x00, 0xff), // bg, border + QColor(0x00, 0x00, 0x00, 0x7f), // grid + QColor(0x00, 0x00, 0x00, 0xef), QColor(0xff, 0x00, 0x00, 0x00), // markers, markers hover + QColor(0xff, 0x00, 0x00, 0xff), QColor(0xff, 0x00, 0x00, 0xff), // markers sel+active + QColor::fromRgbF(0.7, 0.7, 0.7, 0.1), QColor::fromRgbF(0.9, 0.9, 0.9, 1), // zoom bg, zoom hover + QColor::fromRgbF(0.5, 0.1, 0.1, 1), QColor::fromRgbF(0.9, 0.9, 0.9, 1)}; // zoom sel, zoom active + + bool screenColorImages = false; ///< true exports images with screen colors + ZoomViewSettings zoomviews; ///< Settings for the zoomviews + + inline unsigned digitalPhosphorDraws() const { return m_digitalPhosphor ? m_digitalPhosphorDepth : 1; } + inline bool digitalPhosphor() { return m_digitalPhosphor; } + inline unsigned digitalPhosphorDepth() { return m_digitalPhosphorDepth; } + void setDigitalPhosphor(bool enable, unsigned historyDepth); + + inline Dso::InterpolationMode interpolation() const { return m_interpolation; } + void setInterpolation(Dso::InterpolationMode mode); + + private: + Dso::InterpolationMode m_interpolation = Dso::InterpolationMode::LINEAR; ///< Interpolation mode for the graph + bool m_digitalPhosphor = false; ///< true slowly fades out the previous graphs + unsigned m_digitalPhosphorDepth = 8; ///< Number of channels shown at one time + signals: + void interpolationChanged(View *view); + void digitalPhosphorChanged(View *view); +}; + +class Scope; +struct ViewIO { + static void read(QSettings *io, View &view, const Settings::Scope *scope); + static void write(QSettings *io, const View &view); + static void syncChannels(View &view, const Scope *scope); + + private: + static void readColor(QSettings *store, Settings::Colors *colors, const Settings::Scope *scope); + static void syncChannels(Settings::Colors &c, const Settings::Scope *scope); +}; +} diff --git a/openhantek/src/usb/usbdevice.cpp b/openhantek/src/usb/usbdevice.cpp index 953e6b40..20d2a9da 100644 --- a/openhantek/src/usb/usbdevice.cpp +++ b/openhantek/src/usb/usbdevice.cpp @@ -99,7 +99,10 @@ bool USBDevice::connectDevice(QString &errorMessage) { return true; } -USBDevice::~USBDevice() { disconnectFromDevice(); } +USBDevice::~USBDevice() { + disconnectFromDevice(); + libusb_unref_device(device); +} int USBDevice::claimInterface(const libusb_interface_descriptor *interfaceDescriptor, int endpointOut, int endPointIn) { int errorCode = libusb_claim_interface(this->handle, interfaceDescriptor->bInterfaceNumber); @@ -125,17 +128,15 @@ int USBDevice::claimInterface(const libusb_interface_descriptor *interfaceDescri void USBDevice::disconnectFromDevice() { if (!device) return; - if (this->handle) { + if (handle) { // Release claimed interface - if (this->interface != -1) libusb_release_interface(this->handle, this->interface); - this->interface = -1; + if (interface != -1) libusb_release_interface(handle, interface); + interface = -1; // Close device handle - libusb_close(this->handle); + libusb_close(handle); } - this->handle = nullptr; - - libusb_unref_device(device); + handle = nullptr; emit deviceDisconnected(); } @@ -180,19 +181,3 @@ int USBDevice::bulkReadMulti(unsigned char *data, unsigned length, int attempts) else return errorCode; } - -int USBDevice::controlTransfer(unsigned char type, unsigned char request, unsigned char *data, unsigned int length, - int value, int index, int attempts) { - if (!this->handle) return LIBUSB_ERROR_NO_DEVICE; - - int errorCode = LIBUSB_ERROR_TIMEOUT; - for (int attempt = 0; (attempt < attempts || attempts == -1) && errorCode == LIBUSB_ERROR_TIMEOUT; ++attempt) - errorCode = libusb_control_transfer(this->handle, type, request, value, index, data, length, HANTEK_TIMEOUT); - - if (errorCode == LIBUSB_ERROR_NO_DEVICE) disconnectFromDevice(); - return errorCode; -} - - - - diff --git a/openhantek/src/usb/usbdevice.h b/openhantek/src/usb/usbdevice.h index 23126a1f..067920b7 100644 --- a/openhantek/src/usb/usbdevice.h +++ b/openhantek/src/usb/usbdevice.h @@ -13,7 +13,6 @@ class DSOModel; typedef unsigned long UniqueUSBid; - /// \brief Returns string representation for libusb errors. /// \param error The error code. /// \return String explaining the error. @@ -25,8 +24,8 @@ class USBDevice : public QObject { Q_OBJECT public: - explicit USBDevice(DSOModel* model, libusb_device *device, unsigned findIteration = 0); - USBDevice(const USBDevice&) = delete; + explicit USBDevice(DSOModel *model, libusb_device *device, unsigned findIteration = 0); + USBDevice(const USBDevice &) = delete; ~USBDevice(); bool connectDevice(QString &errorMessage); void disconnectFromDevice(); @@ -55,8 +54,8 @@ class USBDevice : public QObject { /// \param timeout The timeout in ms. /// \return Number of transferred bytes on success, libusb error code on /// error. - int bulkTransfer(unsigned char endpoint, const unsigned char *data, unsigned int length, int attempts = HANTEK_ATTEMPTS, - unsigned int timeout = HANTEK_TIMEOUT); + int bulkTransfer(unsigned char endpoint, const unsigned char *data, unsigned int length, + int attempts = HANTEK_ATTEMPTS, unsigned int timeout = HANTEK_TIMEOUT); /// \brief Bulk write to the oscilloscope. /// \param data Buffer for the sent/recieved data. @@ -64,7 +63,7 @@ class USBDevice : public QObject { /// \param attempts The number of attempts, that are done on timeouts. /// \return Number of sent bytes on success, libusb error code on error. inline int bulkWrite(const unsigned char *data, unsigned int length, int attempts = HANTEK_ATTEMPTS) { - return bulkTransfer(HANTEK_EP_OUT, data, length, attempts); + return bulkTransfer(HANTEK_EP_OUT, data, length, attempts); } /// \brief Bulk read from the oscilloscope. @@ -72,8 +71,7 @@ class USBDevice : public QObject { /// \param length The length of the packet. /// \param attempts The number of attempts, that are done on timeouts. /// \return Number of received bytes on success, libusb error code on error. - template - inline int bulkRead(const T *command, int attempts = HANTEK_ATTEMPTS) { + template inline int bulkRead(const T *command, int attempts = HANTEK_ATTEMPTS) { return bulkTransfer(HANTEK_EP_IN, command->data(), command->size(), attempts); } @@ -84,36 +82,38 @@ class USBDevice : public QObject { /// \return Number of received bytes on success, libusb error code on error. int bulkReadMulti(unsigned char *data, unsigned length, int attempts = HANTEK_ATTEMPTS_MULTI); - /// \brief Control transfer to the oscilloscope. - /// \param type The request type, also sets the direction of the transfer. - /// \param request The request field of the packet. - /// \param data Buffer for the sent/recieved data. - /// \param length The length field of the packet. - /// \param value The value field of the packet. - /// \param index The index field of the packet. - /// \param attempts The number of attempts, that are done on timeouts. - /// \return Number of transferred bytes on success, libusb error code on error. - int controlTransfer(unsigned char type, unsigned char request, unsigned char *data, unsigned int length, int value, - int index, int attempts = HANTEK_ATTEMPTS); - /// \brief Control write to the oscilloscope. /// \param command Buffer for the sent/recieved data. /// \return Number of sent bytes on success, libusb error code on error. - template - inline int controlWrite(const T *command) { - return controlTransfer(LIBUSB_REQUEST_TYPE_VENDOR | LIBUSB_ENDPOINT_OUT, (uint8_t)command->code, - (unsigned char *)command->data(), (unsigned)command->size(), command->value, 0, - HANTEK_ATTEMPTS); + template inline int controlWrite(const T *command, int attempts = HANTEK_ATTEMPTS) { + if (!this->handle) return LIBUSB_ERROR_NO_DEVICE; + + int errorCode = LIBUSB_ERROR_TIMEOUT; + for (int attempt = 0; (attempt < attempts || attempts == -1) && errorCode == LIBUSB_ERROR_TIMEOUT; ++attempt) + errorCode = + libusb_control_transfer(this->handle, LIBUSB_REQUEST_TYPE_VENDOR | LIBUSB_ENDPOINT_OUT, + (uint8_t)command->code, (uint16_t)command->value, 0, + (unsigned char *)command->data(), (uint16_t)command->size(), HANTEK_TIMEOUT); + + if (errorCode == LIBUSB_ERROR_NO_DEVICE) disconnectFromDevice(); + return errorCode; } /// \brief Control read to the oscilloscope. /// \param command Buffer for the sent/recieved data. /// \return Number of received bytes on success, libusb error code on error. - template - inline int controlRead(const T *command) { - return controlTransfer(LIBUSB_REQUEST_TYPE_VENDOR | LIBUSB_ENDPOINT_IN, (uint8_t)command->code, - (unsigned char *)command->data(), (unsigned)command->size(), command->value, 0, - HANTEK_ATTEMPTS); + template inline int controlRead(T *command, int attempts = HANTEK_ATTEMPTS) { + if (!this->handle) return LIBUSB_ERROR_NO_DEVICE; + + int errorCode = LIBUSB_ERROR_TIMEOUT; + for (int attempt = 0; (attempt < attempts || attempts == -1) && errorCode == LIBUSB_ERROR_TIMEOUT; ++attempt) + errorCode = + libusb_control_transfer(this->handle, LIBUSB_REQUEST_TYPE_VENDOR | LIBUSB_ENDPOINT_IN, + (uint8_t)command->code, (uint16_t)command->value, 0, + (unsigned char *)command->data(), (uint16_t)command->size(), HANTEK_TIMEOUT); + + if (errorCode == LIBUSB_ERROR_NO_DEVICE) disconnectFromDevice(); + return errorCode; } /** @@ -144,11 +144,12 @@ class USBDevice : public QObject { * that much data though and need an artification restriction. */ inline void overwriteInPacketLength(int len) { inPacketLength = len; } + protected: int claimInterface(const libusb_interface_descriptor *interfaceDescriptor, int endpointOut, int endPointIn); // Device model data - DSOModel* model; + DSOModel *model; // Libusb specific variables struct libusb_device_descriptor descriptor; diff --git a/openhantek/src/usb/usbdevicedefinitions.h b/openhantek/src/usb/usbdevicedefinitions.h index 92580c36..b7066b4b 100644 --- a/openhantek/src/usb/usbdevicedefinitions.h +++ b/openhantek/src/usb/usbdevicedefinitions.h @@ -2,6 +2,8 @@ #pragma once +#include + #define HANTEK_TIMEOUT 500 ///< Timeout for USB transfers in ms #define HANTEK_TIMEOUT_MULTI 100 ///< Timeout for multi packet USB transfers in ms #define HANTEK_ATTEMPTS 3 ///< The number of transfer attempts @@ -11,7 +13,8 @@ #define HANTEK_EP_IN 0x86 ///< IN Endpoint for bulk transfers /// \brief The speed level of the USB connection. -enum ConnectionSpeed { - CONNECTION_FULLSPEED = 0, ///< FullSpeed USB, 64 byte bulk transfers - CONNECTION_HIGHSPEED = 1 ///< HighSpeed USB, 512 byte bulk transfers +enum class ConnectionSpeed : uint8_t { + FULLSPEED = 0, ///< FullSpeed USB, 64 byte bulk transfers + HIGHSPEED = 1, ///< HighSpeed USB, 512 byte bulk transfers + UNKNOWNSPEED = UINT8_MAX }; diff --git a/openhantek/src/utils/debugnotify.cpp b/openhantek/src/utils/debugnotify.cpp new file mode 100644 index 00000000..9bdf987b --- /dev/null +++ b/openhantek/src/utils/debugnotify.cpp @@ -0,0 +1,66 @@ +#include "debugnotify.h" + +namespace Debug { + +LogModel::LogModel(QObject *parent) : QAbstractListModel(parent) { + qRegisterMetaType("Debug::NotificationType"); +} + +void LogModel::addEntry(const QString &msg, Debug::NotificationType typeEnum) { + if (m_filter & (int)typeEnum) return; + + QString typeStr; + switch (typeEnum) { + case Debug::NotificationType::DeviceCommandSend: + typeStr = "Send"; + break; + case Debug::NotificationType::DeviceDataReceived: + typeStr = "Received"; + break; + case Debug::NotificationType::ChannelUsageChange: + typeStr = "ChannelUsage"; + break; + case Debug::NotificationType::DSOLoop: + typeStr = "Loop"; + break; + case Debug::NotificationType::ManualCommand: + typeStr = "Manual"; + break; + case Debug::NotificationType::DSOControl: + typeStr = "Control"; + break; + } + beginInsertRows(QModelIndex(), 0, 1); + entries.push_back(Entry(msg, QTime::currentTime().toString(), typeStr)); + endInsertRows(); + // Remove last 10 entries if we have more than 250 debug messages already + if (entries.size() > 250) { + beginRemoveRows(QModelIndex(), entries.size() - 11, entries.size() - 1); + entries.erase(entries.begin(), entries.begin() + 10); + endRemoveRows(); + } +} + +void LogModel::removeAll() { + beginResetModel(); + entries.clear(); + endResetModel(); +} + +QVariant LogModel::data(const QModelIndex &index, int role) const { + if (role == Qt::DisplayRole || role == Qt::ToolTipRole) { + unsigned row = (unsigned)entries.size() - (unsigned)index.row() - 1; + switch (index.column()) { + case 0: + return entries[row].timestamp; + case 1: + return entries[row].type; + case 2: + return entries[row].msg; + default: + return ""; + } + } + return QVariant(); +} +} diff --git a/openhantek/src/utils/debugnotify.h b/openhantek/src/utils/debugnotify.h new file mode 100644 index 00000000..2b475789 --- /dev/null +++ b/openhantek/src/utils/debugnotify.h @@ -0,0 +1,52 @@ +#pragma once + +#include +#include +#include +#include +#include + +namespace Debug { + +Q_NAMESPACE + +enum class NotificationType : int { + DeviceCommandSend = 0, + DeviceDataReceived = 1, + ChannelUsageChange = 2, + ManualCommand = 4, + DSOControl = 8, + DSOLoop = 16, +}; +Q_ENUM_NS(NotificationType); + +/// A simple Qt Model that just allows to add another debug message entry. +/// Limited to 250 entries, before auto purging of the oldest messages. +class LogModel : public QAbstractListModel { + Q_OBJECT + public: + LogModel(QObject *parent = nullptr); + + void addEntry(const QString &msg, Debug::NotificationType typeEnum); + void removeAll(); + inline void clearFilter() { m_filter = 0; } + inline void addToFilter(NotificationType v) { m_filter |= (int)v; } + + private: + struct Entry { + QString msg; + QString timestamp; + QString type; + Entry(const QString &msg, const QString ×tamp, const QString &type) + : msg(msg), timestamp(timestamp), type(type) {} + }; + std::deque entries; + int m_filter = 0; + // QAbstractItemModel interface + public: + virtual int rowCount(const QModelIndex &) const override { return (int)entries.size(); } + virtual int columnCount(const QModelIndex &) const override { return 3; } + virtual QVariant data(const QModelIndex &index, int role) const override; +}; +} +Q_DECLARE_METATYPE(Debug::NotificationType) diff --git a/openhantek/src/utils/enumclass.h b/openhantek/src/utils/enumclass.h deleted file mode 100644 index 7ba33a58..00000000 --- a/openhantek/src/utils/enumclass.h +++ /dev/null @@ -1,47 +0,0 @@ -// SPDX-License-Identifier: GPL-2.0+ - -#pragma once - -template< typename T, T first, T last > -class Enum -{ -public: - class Iterator - { - public: - Iterator( int value ) : - m_value( value ) - { } - - T operator*( void ) const - { - return (T)m_value; - } - - void operator++( void ) - { - ++m_value; - } - - bool operator!=( Iterator rhs ) - { - return m_value != rhs.m_value; - } - - private: - int m_value; - }; - -}; - -template< typename T, T first, T last > -typename Enum::Iterator begin( Enum ) -{ - return typename Enum::Iterator( (int)first ); -} - -template< typename T, T first, T last > -typename Enum::Iterator end( Enum ) -{ - return typename Enum::Iterator( ((int)last) + 1 ); -} diff --git a/openhantek/src/utils/enumhelper.h b/openhantek/src/utils/enumhelper.h new file mode 100644 index 00000000..f0570fab --- /dev/null +++ b/openhantek/src/utils/enumhelper.h @@ -0,0 +1,44 @@ +#pragma once + +#include +#include + +/// Returns the c-string key of the given enum class value. Can be used for storing enum values to QSettings. +template const char *enumName(T value) { + return QMetaEnum::fromType().valueToKey(static_cast(value)); +} + +template T loadForEnum(QSettings *settings, const char *key, T defaultValue) { + bool ok = false; + T v = static_cast(QMetaEnum::fromType().keyToValue(settings->value(key).toString().toUtf8().data(), &ok)); + return ok ? v : defaultValue; +} + +/// Make enums declared with Q_ENUM iterable. +/// Usage: for(auto v: Enum()) {...} +template class Enum { + public: + using underlying_type_t = typename std::underlying_type::type; + class Iterator { + public: + Iterator(underlying_type_t value) : m_value(value) {} + T operator*(void)const { return (T)m_value; } + void operator++(void) { ++m_value; } + bool operator!=(Iterator rhs) { return m_value != rhs.m_value; } + + private: + underlying_type_t m_value; + }; +}; + +template typename Enum::Iterator begin(Enum) { + const QMetaEnum m = QMetaEnum::fromType(); + const typename Enum::underlying_type_t v = m.value(0); + return typename Enum::Iterator(v); +} + +template typename Enum::Iterator end(Enum) { + const QMetaEnum m = QMetaEnum::fromType(); + const typename Enum::underlying_type_t v = m.value(m.keyCount() - 1); + return typename Enum::Iterator(v + 1); +} diff --git a/openhantek/src/utils/getwithdefault.h b/openhantek/src/utils/getwithdefault.h new file mode 100644 index 00000000..8af23973 --- /dev/null +++ b/openhantek/src/utils/getwithdefault.h @@ -0,0 +1,125 @@ +#pragma once +#include +#include +#include + +template