diff --git a/src/ngscopeclient/CMakeLists.txt b/src/ngscopeclient/CMakeLists.txt index 2cdccd07b..a73d4cc79 100644 --- a/src/ngscopeclient/CMakeLists.txt +++ b/src/ngscopeclient/CMakeLists.txt @@ -73,7 +73,6 @@ add_executable(ngscopeclient FilterGraphWorkspace.cpp FilterPropertiesDialog.cpp FontManager.cpp - FunctionGeneratorDialog.cpp GuiLogSink.cpp HistoryDialog.cpp HistoryManager.cpp @@ -89,7 +88,6 @@ add_executable(ngscopeclient MeasurementsDialog.cpp MemoryLeakerDialog.cpp MetricsDialog.cpp - MultimeterDialog.cpp NFDFileBrowser.cpp NotesDialog.cpp PacketManager.cpp diff --git a/src/ngscopeclient/ChannelPropertiesDialog.cpp b/src/ngscopeclient/ChannelPropertiesDialog.cpp index a0d937abb..7e6add57b 100644 --- a/src/ngscopeclient/ChannelPropertiesDialog.cpp +++ b/src/ngscopeclient/ChannelPropertiesDialog.cpp @@ -2,7 +2,7 @@ * * * ngscopeclient * * * -* Copyright (c) 2012-2025 Andrew D. Zonenberg and contributors * +* Copyright (c) 2012-2026 Andrew D. Zonenberg and contributors * * All rights reserved. * * * * Redistribution and use in source and binary forms, with or without modification, are permitted provided that the * @@ -43,9 +43,20 @@ using namespace std; //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// // Construction / destruction -ChannelPropertiesDialog::ChannelPropertiesDialog(InstrumentChannel* chan, MainWindow* parent, bool graphEditorMode) +ChannelPropertiesDialog::ChannelPropertiesDialog(InstrumentChannel* chan, MainWindow* parent, bool graphEditorMode) : BaseChannelPropertiesDialog(chan, parent, graphEditorMode) { + // Get oscilloscope state, for that we need to make a shared_ptr out of the base pointer returned by chan->GetInstrument() + m_session = &m_parent->GetSession(); + auto instrument = chan->GetInstrument(); + if(instrument) + { + std::shared_ptr scopeSharedPointer = instrument->shared_from_this(); + shared_ptr sharedScope = dynamic_pointer_cast(scopeSharedPointer); + if(sharedScope) + m_state = m_session->GetOscilloscopeState(sharedScope); + } + auto ochan = dynamic_cast(chan); if(!ochan) LogFatal("ChannelPropertiesDialog expects an OscilloscopeChannel\n"); @@ -307,12 +318,12 @@ bool ChannelPropertiesDialog::DoRender() //Input settings only make sense if we have an attached scope auto nstreams = m_channel->GetStreamCount(); + auto index = m_channel->GetIndex(); if(scope) { if(ImGui::CollapsingHeader("Input", defaultOpenFlags)) { //Type of probe connected - auto index = m_channel->GetIndex(); string ptype = m_probe; if(ptype == "") ptype = "(not detected)"; @@ -340,6 +351,9 @@ bool ChannelPropertiesDialog::DoRender() //refresh in case scope driver changed the value m_committedThreshold = scope->GetDigitalThreshold(index); m_threshold = yunit.PrettyPrint(m_committedThreshold); + + // Tell intrument thread that the scope state has to be updated + if(m_state) m_state->m_needsUpdate[index] = true; } HelpMarker("Switching threshold for the digital input buffer"); } @@ -354,6 +368,9 @@ bool ChannelPropertiesDialog::DoRender() //refresh in case scope driver changed the value m_committedHysteresis = scope->GetDigitalHysteresis(index); m_hysteresis = yunit.PrettyPrint(m_committedHysteresis); + + // Tell intrument thread that the scope state has to be updated + if(m_state) m_state->m_needsUpdate[index] = true; } HelpMarker("Hysteresis for the digital input buffer"); } @@ -398,6 +415,9 @@ bool ChannelPropertiesDialog::DoRender() m_committedRange[i] = ochan->GetVoltageRange(i); m_range[i] = unit.PrettyPrint(m_committedRange[i]); } + + // Tell intrument thread that the scope state has to be updated + if(m_state) m_state->m_needsUpdate[index] = true; } if(m_probe != "") ImGui::EndDisabled(); @@ -408,7 +428,12 @@ bool ChannelPropertiesDialog::DoRender() { ImGui::SetNextItemWidth(width); if(Combo("Coupling", m_couplingNames, m_coupling)) + { ochan->SetCoupling(m_couplings[m_coupling]); + + // Tell intrument thread that the scope state has to be updated + if(m_state) m_state->m_needsUpdate[index] = true; + } HelpMarker("Coupling configuration for the input"); } @@ -417,7 +442,12 @@ bool ChannelPropertiesDialog::DoRender() { ImGui::SetNextItemWidth(width); if(Combo("Bandwidth", m_bwlNames, m_bwl)) + { ochan->SetBandwidthLimit(m_bwlValues[m_bwl]); + + // Tell intrument thread that the scope state has to be updated + if(m_state) m_state->m_needsUpdate[index] = true; + } HelpMarker("Hardware bandwidth limiter setting"); } } @@ -465,8 +495,13 @@ bool ChannelPropertiesDialog::DoRender() if(scope->CanInvert(index)) { if(ImGui::Checkbox("Invert", &m_inverted)) + { ochan->Invert(m_inverted); + // Tell intrument thread that the scope state has to be updated + if(m_state) m_state->m_needsUpdate[index] = true; + } + HelpMarker( "When checked, input value is multiplied by -1.\n\n" "For a differential probe, this is equivalent to swapping the positive and negative inputs." @@ -558,8 +593,13 @@ bool ChannelPropertiesDialog::DoRender() } ImGui::SetNextItemWidth(width); if(UnitInputWithExplicitApply("Offset", m_offset[i], m_committedOffset[i], unit)) + { ochan->SetOffset(m_committedOffset[i], i); + // Tell intrument thread that the scope state has to be updated + if(m_state) m_state->m_needsUpdate[index] = true; + } + //Same for range auto range = ochan->GetVoltageRange(i); auto srange = unit.PrettyPrint(m_committedRange[i]); @@ -570,8 +610,13 @@ bool ChannelPropertiesDialog::DoRender() } ImGui::SetNextItemWidth(width); if(UnitInputWithExplicitApply("Range", m_range[i], m_committedRange[i], unit)) + { ochan->SetVoltageRange(m_committedRange[i], i); + // Tell intrument thread that the scope state has to be updated + if(m_state) m_state->m_needsUpdate[index] = true; + } + ImGui::PopID(); } } diff --git a/src/ngscopeclient/ChannelPropertiesDialog.h b/src/ngscopeclient/ChannelPropertiesDialog.h index 460ab6203..048e84d3e 100644 --- a/src/ngscopeclient/ChannelPropertiesDialog.h +++ b/src/ngscopeclient/ChannelPropertiesDialog.h @@ -2,7 +2,7 @@ * * * ngscopeclient * * * -* Copyright (c) 2012-2025 Andrew D. Zonenberg * +* Copyright (c) 2012-2026 Andrew D. Zonenberg * * All rights reserved. * * * * Redistribution and use in source and binary forms, with or without modification, are permitted provided that the * @@ -49,6 +49,9 @@ class ChannelPropertiesDialog : public BaseChannelPropertiesDialog void RefreshInputSettings(Oscilloscope* scope, size_t nchan); + ///@brief Current channel stats, live updated + std::shared_ptr m_state; + std::string m_displayName; std::string m_committedDisplayName; diff --git a/src/ngscopeclient/Dialog.cpp b/src/ngscopeclient/Dialog.cpp index f5b95da21..4de5aba43 100644 --- a/src/ngscopeclient/Dialog.cpp +++ b/src/ngscopeclient/Dialog.cpp @@ -35,17 +35,23 @@ #include "ngscopeclient.h" #include "Dialog.h" #include "MainWindow.h" +#include "PreferenceTypes.h" using namespace std; +#define CARRIAGE_RETURN_CHAR "⏎" +#define DEFAULT_APPLY_BUTTON_COLOR "#4CCC4C" + //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// // Construction / destruction -Dialog::Dialog(const string& title, const string& id, ImVec2 defaultSize) +Dialog::Dialog(const string& title, const string& id, ImVec2 defaultSize, Session* session, MainWindow* parent) : m_open(true) , m_id(id) , m_title(title) , m_defaultSize(defaultSize) + , m_session(session) + , m_parent(parent) { } @@ -217,55 +223,6 @@ void Dialog::HelpMarker(const string& header, const vector& bullets) } } -/** - @brief Helper for displaying a floating-point input box with an "apply" button - */ -bool Dialog::FloatInputWithApplyButton(const string& label, float& currentValue, float& committedValue) -{ - ImGui::BeginGroup(); - - bool dirty = currentValue != committedValue; - ImGui::InputFloat(label.c_str(), ¤tValue); - ImGui::SameLine(); - if(!dirty) - ImGui::BeginDisabled(); - auto applyLabel = string("Apply###Apply") + label; - bool changed = false; - if(ImGui::Button(applyLabel.c_str())) - { - changed = true; - committedValue = currentValue; - } - if(!dirty) - ImGui::EndDisabled(); - - ImGui::EndGroup(); - return changed; -} - -bool Dialog::TextInputWithApplyButton(const string& label, string& currentValue, string& committedValue) -{ - ImGui::BeginGroup(); - - bool dirty = currentValue != committedValue; - ImGui::InputText(label.c_str(), ¤tValue); - ImGui::SameLine(); - if(!dirty) - ImGui::BeginDisabled(); - auto applyLabel = string("Apply###Apply") + label; - bool changed = false; - if(ImGui::Button(applyLabel.c_str())) - { - changed = true; - committedValue = currentValue; - } - if(!dirty) - ImGui::EndDisabled(); - - ImGui::EndGroup(); - return changed; -} - bool Dialog::TextInputWithImplicitApply(const string& label, string& currentValue, string& committedValue) { bool dirty = currentValue != committedValue; @@ -370,6 +327,7 @@ bool Dialog::UnitInputWithImplicitApply( int64_t& committedValue, Unit unit) { + // return renderEditableProperty(-1,label,currentValue,committedValue,unit,); bool dirty = unit.PrettyPrintInt64(committedValue) != currentValue; ImGui::InputText(label.c_str(), ¤tValue); @@ -407,25 +365,660 @@ bool Dialog::UnitInputWithExplicitApply( float& committedValue, Unit unit) { - bool dirty = unit.PrettyPrint(committedValue) != currentValue; + return renderEditablePropertyWithExplicitApply(-1,label,currentValue,committedValue,unit); +} - ImGui::BeginGroup(); +/** + @brief Render a numeric value + @param value the string representation of the value to display (may include the unit) + @param clicked output value for clicked state + @param hovered output value for hovered state + @param optcolor the optional color to use (defaults to text color) + @param allow7SegmentDisplay (defaults to false) true if the value can be displayed in 7 segment format + @param digitHeight the height of a digit (if 0 (defualt), will use ImGui::GetFontSize()) + @param clickable true (default) if the displayed value should be clickable + */ +void Dialog::renderNumericValue(const std::string& value, bool &clicked, bool &hovered, std::optional optcolor, bool allow7SegmentDisplay, float digitHeight, bool clickable) +{ + bool use7Segment = false; + bool changeFont = false; + int64_t displayType = NumericValueDisplay::NUMERIC_DISPLAY_DEFAULT_FONT; + ImVec4 color = optcolor ? optcolor.value() : ImGui::GetStyleColorVec4(ImGuiCol_Text); + if(m_session) + { + auto& prefs = m_session->GetPreferences(); + displayType = prefs.GetEnumRaw("Appearance.Stream Browser.numeric_value_display"); + } + FontWithSize font; + if(allow7SegmentDisplay) + { + use7Segment = (displayType == NumericValueDisplay::NUMERIC_DISPLAY_7SEGMENT); + if(!use7Segment && m_parent) + { + font = m_parent->GetFontPref(displayType == NumericValueDisplay::NUMERIC_DISPLAY_DEFAULT_FONT ? "Appearance.General.default_font" : "Appearance.General.console_font"); + changeFont = true; + } + } + if(use7Segment) + { + if(digitHeight <= 0) digitHeight = ImGui::GetFontSize(); + + Render7SegmentValue(value,color,digitHeight,clicked,hovered,clickable); + } + else + { + if(clickable) + { + ImVec2 pos = ImGui::GetCursorPos(); + ImGui::PushStyleColor(ImGuiCol_Text, color); + if(changeFont) ImGui::PushFont(font.first, font.second); + ImGui::TextUnformatted(value.c_str()); + if(changeFont) ImGui::PopFont(); + ImGui::PopStyleColor(); + + clicked |= ImGui::IsItemClicked(); + if(ImGui::IsItemHovered()) + { // Hand cursor + ImGui::SetMouseCursor(ImGuiMouseCursor_Hand); + // Lighter if hovered + color.x = color.x * 1.2f; + color.y = color.y * 1.2f; + color.z = color.z * 1.2f; + ImGui::SetCursorPos(pos); + ImGui::PushStyleColor(ImGuiCol_Text, color); + ImGui::TextUnformatted(value.c_str()); + ImGui::PopStyleColor(); + hovered = true; + } + } + else + { + ImGui::PushStyleColor(ImGuiCol_Text, color); + if(changeFont) ImGui::PushFont(font.first, font.second); + ImGui::TextUnformatted(value.c_str()); + if(changeFont) ImGui::PopFont(); + ImGui::PopStyleColor(); + } + } +} - ImGui::InputText(label.c_str(), ¤tValue); +/** + @brief Render a read-only instrument property value + @param label the value label (used as a label for the property) + @param currentValue the string representation of the current value + @param tooltip if not null, will add the provided text as an help marker (defaults to nullptr) +*/ +void Dialog::renderReadOnlyProperty(float width, const string& label, const string& value, const char* tooltip) +{ + ImGui::PushID(label.c_str()); // Prevent collision if several sibling links have the same linktext + float fontSize = ImGui::GetFontSize(); + if(width <= 0) width = 6*fontSize; + ImGuiStyle& style = ImGui::GetStyle(); + ImVec4 bg = style.Colors[ImGuiCol_FrameBg]; + ImGui::PushStyleColor(ImGuiCol_ChildBg, bg); + ImGui::BeginChild("##readOnlyValue", ImVec2(width, ImGui::GetFontSize()),false,ImGuiWindowFlags_None); + ImGui::TextUnformatted(value.c_str()); + ImGui::EndChild(); + ImGui::PopStyleColor(); ImGui::SameLine(); - if(!dirty) - ImGui::BeginDisabled(); - auto applyLabel = string("Apply###Apply") + label; + ImGui::TextUnformatted(label.c_str()); + ImGui::PopID(); + if(tooltip) + { + HelpMarker(tooltip); + } +} + + +template +/** + @brief Render an editable numeric value + @param width the width of the input value (if <0 will be ignored, if =0 will default to 6*ImGui::GetStyle()) + @param label the value label (used as a label for the TextInput) + @param currentValue the string representation of the current value + @param comittedValue the last comitted typed (float, double or int64_t) value + @param unit the Unit of the value + @param tooltip if not null, will add the provided text as an help marker (defaults to nullptr) + @param optcolor the optional color to use (defaults to text color) + @param clicked output value for clicked state + @param hovered output value for hovered state + @param allow7SegmentDisplay (defaults to false) true if the value can be displayed in 7 segment format + @param explicitApply (defaults to false) true if the input value needs to explicitly be applied (by clicking the apply button) + @return true if the value has changed + */ +bool Dialog::renderEditableProperty(float width, const std::string& label, std::string& currentValue, T& committedValue, Unit unit, const char* tooltip, std::optional optcolor, bool allow7SegmentDisplay, bool explicitApply) +{ + static_assert(std::is_same_v || std::is_same_v || std::is_same_v,"renderEditableProperty only supports int64_t, float or double"); + bool use7Segment = false; + bool changeFont = false; + int64_t displayType = NumericValueDisplay::NUMERIC_DISPLAY_DEFAULT_FONT; + ImVec4 buttonColor; + ImVec4 color = optcolor ? optcolor.value() : ImGui::GetStyleColorVec4(ImGuiCol_Text); + if(m_session) + { + auto& prefs = m_session->GetPreferences(); + displayType = prefs.GetEnumRaw("Appearance.Stream Browser.numeric_value_display"); + buttonColor = ImGui::ColorConvertU32ToFloat4(prefs.GetColor("Appearance.General.apply_button_color")); + } + else + { + buttonColor = ImGui::ColorConvertU32ToFloat4(ColorFromString(DEFAULT_APPLY_BUTTON_COLOR)); + } + FontWithSize font; + if(allow7SegmentDisplay) + { + use7Segment = (displayType == NumericValueDisplay::NUMERIC_DISPLAY_7SEGMENT); + if(!use7Segment && m_parent) + { + font = m_parent->GetFontPref(displayType == NumericValueDisplay::NUMERIC_DISPLAY_DEFAULT_FONT ? "Appearance.General.default_font" : "Appearance.General.console_font"); + changeFont = true; + } + } + bool changed = false; - if(ImGui::Button(applyLabel.c_str())) + bool validateChange = false; + bool cancelEdit = false; + bool keepEditing = false; + bool dirty; + float fontSize = ImGui::GetFontSize(); + if(width >= 0) { - changed = true; - committedValue = unit.ParseString(currentValue); - currentValue = unit.PrettyPrint(committedValue); + if(width == 0) width = 6*fontSize; + ImGui::SetNextItemWidth(width); + } + if constexpr (std::is_same_v) + dirty = unit.PrettyPrintInt64(committedValue) != currentValue; + else + dirty = unit.PrettyPrint(committedValue) != currentValue; + string editLabel = label+"##Edit"; + ImGuiID editId = ImGui::GetID(editLabel.c_str()); + ImGuiID labelId = ImGui::GetID(label.c_str()); + if(m_editedItemId == editId) + { // Item currently beeing edited + ImGui::BeginGroup(); + float inputXPos = ImGui::GetCursorPosX(); + ImGuiContext& g = *GImGui; + float inputWidth = g.NextItemData.Width; + // Allow overlap for apply button + ImGui::PushItemFlag(ImGuiItemFlags_AllowOverlap, true); + ImGui::PushStyleColor(ImGuiCol_Text, color); + if(changeFont) ImGui::PushFont(font.first, font.second); + if(ImGui::InputText(editLabel.c_str(), ¤tValue, ImGuiInputTextFlags_EnterReturnsTrue)) + { // Input validated (but no apply button) + if(!explicitApply) + { // Implcit apply => validate change + validateChange = true; + } + else + { // Explicit apply needed => keep editing + keepEditing = true; + } + } + if(changeFont) ImGui::PopFont(); + ImGui::PopStyleColor(); + ImGui::PopItemFlag(); + if(explicitApply) + { // Add Apply button + float buttonWidth = ImGui::GetFontSize() * 2; + // Position the button just before the right side of the text input + ImGui::SameLine(inputXPos+inputWidth-ImGui::GetCursorPosX()-buttonWidth+2*ImGui::GetStyle().ItemInnerSpacing.x); + ImVec4 buttonColorHovered = buttonColor; + float bgmul = 0.8f; + ImVec4 buttonColorDefault = ImVec4(buttonColor.x*bgmul, buttonColor.y*bgmul, buttonColor.z*bgmul, buttonColor.w); + bgmul = 0.7f; + ImVec4 buttonColorActive = ImVec4(buttonColor.x*bgmul, buttonColor.y*bgmul, buttonColor.z*bgmul, buttonColor.w); + ImGui::PushStyleColor(ImGuiCol_Button, buttonColorDefault); + ImGui::PushStyleColor(ImGuiCol_ButtonHovered, buttonColorHovered); + ImGui::PushStyleColor(ImGuiCol_ButtonActive, buttonColorActive); + ImGui::BeginDisabled(!dirty); + if(ImGui::Button(CARRIAGE_RETURN_CHAR)) // Carriage return symbol + { // Apply button click + validateChange = true; + } + ImGui::EndDisabled(); + if(dirty && ImGui::IsItemHovered() && m_parent) + { // Help to explain apply button + m_parent->AddStatusHelp("mouse_lmb", "Apply value changes and send them to the instrument"); + } + ImGui::PopStyleColor(3); + } + if(!validateChange) + { + if(keepEditing) + { // Give back focus to test input + ImGui::ActivateItemByID(editId); + } + else if(ImGui::IsKeyPressed(ImGuiKey_Escape)) + { // Detect escape => stop editing + cancelEdit = true; + //Prevent focus from going to parent node + ImGui::ActivateItemByID(0); + } + else if((ImGui::GetActiveID() != editId) && (!explicitApply || !ImGui::IsItemActive() /* This is here to prevent detecting focus lost when apply button is clicked */)) + { // Detect focus lost => stop editing too + if(explicitApply) + { // Cancel on focus lost + cancelEdit = true; + } + else + { // Validate on focus list + validateChange = true; + } + } + } + ImGui::EndGroup(); } - if(!dirty) - ImGui::EndDisabled(); + else + { + if(m_lastEditedItemId == editId) + { // Focus lost + if(explicitApply) + { // Cancel edit + cancelEdit = true; + } + else + { // Validate change + validateChange = true; + } + m_lastEditedItemId = 0; + } + bool clicked = false; + bool hovered = false; + if(use7Segment) + { + ImGui::PushID(labelId); + Render7SegmentValue(currentValue,color,ImGui::GetFontSize(),clicked,hovered); + ImGui::PopID(); + } + else + { + ImGui::PushStyleColor(ImGuiCol_Text, color); + if(changeFont) ImGui::PushFont(font.first, font.second); + ImGui::InputText(label.c_str(),¤tValue,ImGuiInputTextFlags_ReadOnly); + if(changeFont) ImGui::PopFont(); + ImGui::PopStyleColor(); + clicked |= ImGui::IsItemClicked(); + if(ImGui::IsItemHovered()) + { // Keep hand cursor while read-only + ImGui::SetMouseCursor(ImGuiMouseCursor_Hand); + hovered = true; + } + } - ImGui::EndGroup(); + if (clicked) + { + m_lastEditedItemId = m_editedItemId; + m_editedItemId = editId; + ImGui::ActivateItemByID(editId); + } + if (hovered && m_parent) + m_parent->AddStatusHelp("mouse_lmb", "Edit value"); + } + if(validateChange) + { + if(m_editedItemId == editId) + { + m_lastEditedItemId = 0; + m_editedItemId = 0; + } + if(dirty) + { // Content actually changed + if constexpr (std::is_same_v) + { + //Float path if the user input a decimal value like "3.5G" + if(currentValue.find(".") != string::npos) + committedValue = unit.ParseString(currentValue); + //Integer path otherwise for full precision + else + committedValue = unit.ParseStringInt64(currentValue); + + currentValue = unit.PrettyPrintInt64(committedValue); + } + else + { + committedValue = static_cast(unit.ParseString(currentValue)); + if constexpr (std::is_same_v) + currentValue = unit.PrettyPrintInt64(committedValue); + else + currentValue = unit.PrettyPrint(committedValue); + } + changed = true; + } + } + else if(cancelEdit) + { // Restore value + if constexpr (std::is_same_v) + currentValue = unit.PrettyPrintInt64(committedValue); + else + currentValue = unit.PrettyPrint(committedValue); + if(m_editedItemId == editId) + { + m_lastEditedItemId = 0; + m_editedItemId = 0; + } + } + if(tooltip) + { + HelpMarker(tooltip); + } return changed; } + +template bool Dialog::renderEditableProperty(float width, const std::string& label, std::string& currentValue, float& committedValue, Unit unit, const char* tooltip, std::optional optcolor, bool allow7SegmentDisplay, bool explicitApply); +template bool Dialog::renderEditableProperty(float width, const std::string& label, std::string& currentValue, double& committedValue, Unit unit, const char* tooltip, std::optional optcolor, bool allow7SegmentDisplay, bool explicitApply); +template bool Dialog::renderEditableProperty(float width, const std::string& label, std::string& currentValue, int64_t& committedValue, Unit unit, const char* tooltip, std::optional optcolor, bool allow7SegmentDisplay, bool explicitApply); + +template +/** + @brief Render an editable numeric value with explicit apply (if the input value needs to explicitly be applied by clicking the apply button) + @param width the width of the input value (if <0 will be ignored, if =0 will default to 6*ImGui::GetStyle()) + @param label the value label (used as a label for the TextInput) + @param currentValue the string representation of the current value + @param comittedValue the last comitted typed (float, double or int64_t) value + @param unit the Unit of the value + @param tooltip if not null, will add the provided text as an help marker (defaults to nullptr) + @param optcolor the optional color to use (defaults to text color) + @param clicked output value for clicked state + @param hovered output value for hovered state + @param allow7SegmentDisplay (defaults to false) true if the value can be displayed in 7 segment format + @return true if the value has changed + */ +bool Dialog::renderEditablePropertyWithExplicitApply(float width, const std::string& label, std::string& currentValue, T& committedValue, Unit unit, const char* tooltip, std::optional optcolor, bool allow7SegmentDisplay) +{ + return renderEditableProperty(width,label,currentValue,committedValue,unit,tooltip,optcolor,allow7SegmentDisplay,true); +} + +template bool Dialog::renderEditablePropertyWithExplicitApply(float width, const std::string& label, std::string& currentValue, float& committedValue, Unit unit, const char* tooltip, std::optional optcolor, bool allow7SegmentDisplay); +template bool Dialog::renderEditablePropertyWithExplicitApply(float width, const std::string& label, std::string& currentValue, double& committedValue, Unit unit, const char* tooltip, std::optional optcolor, bool allow7SegmentDisplay); +template bool Dialog::renderEditablePropertyWithExplicitApply(float width, const std::string& label, std::string& currentValue, int64_t& committedValue, Unit unit, const char* tooltip, std::optional optcolor, bool allow7SegmentDisplay); + + +/** + @brief Segment on/off state for each of the 10 digits + "L" (needed for OL / Overload) + 0b01000000 : Top h segment + 0b00100000 : Top right v seglent + 0b00010000 : Bottom right v segment + 0b00001000 : Bottom h segment + 0b00000100 : Bottom left v segment + 0b00000010 : Top left v segment + 0b00000001 : Center h segment + */ +static char SEGMENTS[] = +{ + 0x7E, // 0 + 0x30, // 1 + 0x6D, // 2 + 0x79, // 3 + 0x33, // 4 + 0x5B, // 5 + 0x5F, // 6 + 0x70, // 7 + 0x7F, // 8 + 0x7B, // 9 + 0x0E, // L + 0x01, // - +}; + +/** + @brief Render a single digit in 7 segment display style + @param drawList the drawList used for rendering + @param digit the digit to render + @param size the size of the digit + @param position the position of the digit + @param thickness the thickness of a segment + @param colorOn the color for an "on" segment + @param colorOff the color for an "off" segment + */ +void Dialog::Render7SegmentDigit(ImDrawList* drawList, uint8_t digit, ImVec2 size, ImVec2 position, float thickness, ImU32 colorOn, ImU32 colorOff) +{ + // Inspired by https://github.com/ocornut/imgui/issues/3606#issuecomment-736855952 + if(digit == '-') + digit = 11; // Minus sign + else if(digit > 10) + digit = 10; // 10 is for L of OL (Overload) + size.y += thickness; + ImVec2 halfSize(size.x/2,size.y/2); + ImVec2 centerPosition(position.x+halfSize.x,position.y+halfSize.y); + float w = thickness; + float h = thickness/2; + float segmentSpec[7][4] = + { + {-1, -1, h, h}, // Top h segment + { 1, -1, -h, h}, // Top right v seglent + { 1, 0, -h, -h}, // Bottom right v segment + {-1, 1, h, -w * 1.5f},// Bottom h segment + {-1, 0, h, -h}, // Bottom left v segment + {-1, -1, h, h}, // Top left v segment + {-1, 0, h, -h}, // Center h segment + }; + for(int i = 0; i < 7; i++) + { + ImVec2 topLeft, bottomRight; + if(i % 3 == 0) + { + // Horizontal segment + topLeft = ImVec2(centerPosition.x + segmentSpec[i][0] * halfSize.x + segmentSpec[i][2], centerPosition.y + segmentSpec[i][1] * halfSize.y + segmentSpec[i][3] - h); + bottomRight = ImVec2(topLeft.x + size.x - w, topLeft.y + w); + } + else + { + // Vertical segment + topLeft = ImVec2(centerPosition.x + segmentSpec[i][0] * halfSize.x + segmentSpec[i][2] - h, centerPosition.y + segmentSpec[i][1] * halfSize.y + segmentSpec[i][3]); + bottomRight = ImVec2(topLeft.x + w, topLeft.y + halfSize.y - w); + } + ImVec2 segmentSize = bottomRight - topLeft; + float space = w * 0.6; + float u = space - h; + if(segmentSize.x > segmentSize.y) + { + // Horizontal segment + ImVec2 points[] = + { + {topLeft.x + u, topLeft.y + segmentSize.y * .5f}, + {topLeft.x + space, topLeft.y}, + {bottomRight.x - space, topLeft.y}, + {bottomRight.x - u, topLeft.y + segmentSize.y * .5f}, + {bottomRight.x - space, bottomRight.y}, + {topLeft.x + space, bottomRight.y} + }; + drawList->AddConvexPolyFilled(points, 6, (SEGMENTS[digit] >> (6 - i)) & 1 ? colorOn : colorOff); + } + else + { + // Vertical segment + ImVec2 points[] = { + {topLeft.x + segmentSize.x * .5f, topLeft.y + u}, + {bottomRight.x, topLeft.y + space}, + {bottomRight.x, bottomRight.y - space}, + {bottomRight.x - segmentSize.x * .5f, bottomRight.y - u}, + {topLeft.x, bottomRight.y - space}, + {topLeft.x, topLeft.y + space}}; + drawList->AddConvexPolyFilled(points, 6, (SEGMENTS[digit] >> (6 - i)) & 1 ? colorOn : colorOff); + } + } +} + +// @brief ratio between unit font size and digit size +#define UNIT_SCALE 0.80f + +// @brief ratio between digit width and height +#define DIGIT_WIDTH_RATIO 0.50f + +/** + @brief Render a numeric value with a 7 segment display style + @param value the string representation of the value to display (may include the unit) + @param color the color to use + @param digitHeight the height of a digit + */ +void Dialog::Render7SegmentValue(const std::string& value, ImVec4 color, float digitHeight) +{ + bool ignoredClicked, ignoredHovered; + Render7SegmentValue(value,color,digitHeight,ignoredClicked,ignoredHovered,false); +} + +/** + @brief Render a numeric value with a 7 segment display style + @param value the string representation of the value to display (may include the unit) + @param color the color to use + @param digitHeight the height of a digit + @param clicked output value for clicked state + @param hovered output value for hovered state + @param clickable true (default) if the displayed value should be clickable + */ +void Dialog::Render7SegmentValue(const std::string& value, ImVec4 color, float digitHeight, bool &clicked, bool &hovered, bool clickable) +{ + ImDrawList* draw_list = ImGui::GetWindowDrawList(); + + // Compute digit width according th height + float digitWidth = digitHeight*DIGIT_WIDTH_RATIO; + + // Compute front and back color + float bgmul = 0.15; + auto bcolor = ImGui::ColorConvertFloat4ToU32(ImVec4(color.x*bgmul, color.y*bgmul, color.z*bgmul, color.w)); + auto fcolor = ImGui::ColorConvertFloat4ToU32(color); + + + // Parse value string to get integer and fractional part + unit + bool inIntPart = true; + bool inFractPart = false; + vector intPart; + vector fractPart; + string unit; + + // TODO : move this to Unit.h + #define UNIT_OVERLOAD_LABEL "Overload" + + if(value == UNIT_OVERLOAD_LABEL) + { + // Overload + intPart.push_back(0); + intPart.push_back(10); // 10 is for L + unit = "Inf."; + } + else + { + // Iterate on each char of the value string + for(const char c : value) + { + if(c >= '0' && c <='9') + { + // This is a numeric digit + if(inIntPart) + intPart.push_back((uint8_t)(c-'0')); + else if(inFractPart) + fractPart.push_back((uint8_t)(c-'0')); + else + unit += c; + } + else if(c == '-') + { + // This is the decimal separator + if(inIntPart) + { + intPart.push_back(c); + } + else + LogWarning("Unexpected sign '%c' in value '%s'.\n",c,value.c_str()); + } + else if(c == '.' || c == std::use_facet >(std::locale()).decimal_point() || c == ',') + { + // This is the decimal separator + if(inIntPart) + { + inFractPart = true; + inIntPart = false; + } + else + LogWarning("Unexpected decimal separator '%c' in value '%s'.\n",c,value.c_str()); + } + else if(isspace(c) || c == std::use_facet< std::numpunct >(std::locale()).thousands_sep()) + { + // We ingore spaces (except in unit part) + if(inIntPart || inFractPart) {} // Ignore + else + unit += c; + } + else // Anything else + { + // This is the unit + inFractPart = false; + inIntPart = false; + unit += c; + } + } + // Trim the unit string + unit = Trim(unit); + + // Fill fractional part with 2 zeros if it's empty + if(fractPart.empty()) + { + fractPart.push_back(0); + fractPart.push_back(0); + } + } + + // Segment thickness + float thickness = digitHeight/10; + + // Space between digits + float spacing = 0.08 * digitWidth; + + // Size of decimal separator + float dotSize = 2*thickness; + + // Size of unit font and unit text + float unitSize = digitHeight*UNIT_SCALE; + float unitTextWidth = ImGui::GetFont()->CalcTextSizeA(unitSize,FLT_MAX, 0.0f,unit.c_str()).x; + + ImVec2 size(digitWidth*(intPart.size()+fractPart.size())+dotSize+2*spacing+unitTextWidth+thickness, digitHeight); + + if(clickable) + { + bgmul = 0.0f; + ImVec4 buttonColor = ImVec4(color.x*bgmul, color.y*bgmul, color.z*bgmul, 0); + bgmul = 0.2f; + ImVec4 buttonColorHovered = ImVec4(color.x*bgmul, color.y*bgmul, color.z*bgmul, color.w); + bgmul = 0.3f; + ImVec4 buttonColorActive = ImVec4(color.x*bgmul, color.y*bgmul, color.z*bgmul, color.w); + ImGui::PushStyleColor(ImGuiCol_Button, buttonColor); + ImGui::PushStyleColor(ImGuiCol_ButtonHovered, buttonColorHovered); + ImGui::PushStyleColor(ImGuiCol_ButtonActive, buttonColorActive); + clicked |= ImGui::Button(" ",size); + hovered |= ImGui::IsItemHovered(); + ImGui::PopStyleColor(3); + if(hovered) + ImGui::SetMouseCursor(ImGuiMouseCursor_Hand); + } + else + ImGui::InvisibleButton("seven", size, ImGuiButtonFlags_EnableNav); + + ImVec2 position = ImGui::GetItemRectMin(); + + // Actual digit width (without space) + float digitActualWidth = digitWidth - spacing; + // Current x position + float x = 0; + + // Integer part + for(size_t i = 0; i < intPart.size(); i++) + { + Render7SegmentDigit(draw_list, intPart[i], ImVec2(digitActualWidth, digitHeight), ImVec2(position.x + x, position.y),thickness,fcolor,bcolor); + x += digitWidth; + } + // Decimal separator + x+= spacing; + draw_list->AddCircleFilled(ImVec2(position.x+x+dotSize/2-spacing/2,position.y+digitHeight-dotSize/2),dotSize/2,fcolor); + x+= dotSize; + x+= spacing; + // Factional part + for(size_t i = 0; i < fractPart.size(); i++) + { + Render7SegmentDigit(draw_list, fractPart[i], ImVec2(digitActualWidth, digitHeight), ImVec2(position.x + x, position.y),thickness,fcolor,bcolor); + x += digitWidth; + } + // Unit + draw_list->AddText(NULL,unitSize, + ImVec2(position.x + x + thickness, position.y), + fcolor, + unit.c_str()); +} \ No newline at end of file diff --git a/src/ngscopeclient/Dialog.h b/src/ngscopeclient/Dialog.h index 43bd49573..84b1008b1 100644 --- a/src/ngscopeclient/Dialog.h +++ b/src/ngscopeclient/Dialog.h @@ -2,7 +2,7 @@ * * * ngscopeclient * * * -* Copyright (c) 2012-2024 Andrew D. Zonenberg and contributors * +* Copyright (c) 2012-2026 Andrew D. Zonenberg and contributors * * All rights reserved. * * * * Redistribution and use in source and binary forms, with or without modification, are permitted provided that the * @@ -37,13 +37,15 @@ #include "imgui_stdlib.h" +class MainWindow; + /** @brief Generic dialog box or other popup window */ class Dialog { public: - Dialog(const std::string& title, const std::string& id, ImVec2 defaultSize = ImVec2(300, 100) ); + Dialog(const std::string& title, const std::string& id, ImVec2 defaultSize = ImVec2(300, 100),Session* session = nullptr, MainWindow* parent = nullptr); virtual ~Dialog(); virtual bool Render(); @@ -79,14 +81,19 @@ class Dialog std::string& committedValue); protected: - bool FloatInputWithApplyButton(const std::string& label, float& currentValue, float& committedValue); - bool TextInputWithApplyButton(const std::string& label, std::string& currentValue, std::string& committedValue); bool IntInputWithImplicitApply(const std::string& label, int& currentValue, int& committedValue); bool UnitInputWithExplicitApply( const std::string& label, std::string& currentValue, float& committedValue, Unit unit); + void renderNumericValue(const std::string& value, bool &clicked, bool &hovered, std::optional optcolor = std::nullopt, bool allow7SegmentDisplay = false, float digitHeight = 0, bool clickable = true); + void renderReadOnlyProperty(float width, const std::string& label, const std::string& value, const char* tooltip = nullptr); + template + bool renderEditableProperty(float width, const std::string& label, std::string& currentValue, T& committedValue, Unit unit, const char* tooltip = nullptr, std::optional optcolor = std::nullopt, bool allow7SegmentDisplay = false, bool explicitApply = false); + template + bool renderEditablePropertyWithExplicitApply(float width, const std::string& label, std::string& currentValue, T& committedValue, Unit unit, const char* tooltip = nullptr, std::optional optcolor = std::nullopt, bool allow7SegmentDisplay = false); + public: static void Tooltip(const std::string& str, bool allowDisabled = false); static void HelpMarker(const std::string& str); @@ -96,6 +103,10 @@ class Dialog void RenderErrorPopup(); void ShowErrorPopup(const std::string& title, const std::string& msg); + void Render7SegmentDigit(ImDrawList* drawList, uint8_t digit, ImVec2 size, ImVec2 position, float thikness, ImU32 colorOn, ImU32 colorOff); + void Render7SegmentValue(const std::string& value, ImVec4 color, float digitHeight); + void Render7SegmentValue(const std::string& value, ImVec4 color, float digitHeight, bool &clicked, bool &hovered, bool clickable = true); + bool m_open; std::string m_id; std::string m_title; @@ -103,6 +114,17 @@ class Dialog std::string m_errorPopupTitle; std::string m_errorPopupMessage; + + ///@brief optional reference to session + Session* m_session; + ///@brief optional reference to parent MainWindow + MainWindow* m_parent; + + + ///@brief Id of the item currently beeing edited + ImGuiID m_editedItemId = 0; + ///@brief Id of the last edited item + ImGuiID m_lastEditedItemId = 0; }; #endif diff --git a/src/ngscopeclient/DigitalIOChannelDialog.cpp b/src/ngscopeclient/DigitalIOChannelDialog.cpp index ce23d5f4e..d5cee3046 100644 --- a/src/ngscopeclient/DigitalIOChannelDialog.cpp +++ b/src/ngscopeclient/DigitalIOChannelDialog.cpp @@ -53,6 +53,7 @@ DigitalIOChannelDialog::DigitalIOChannelDialog(DigitalIOChannel* chan, MainWindo , m_drive("") , m_committedDrive(0) { + m_session = &parent->GetSession(); m_committedDisplayName = m_channel->GetDisplayName(); m_displayName = m_committedDisplayName; diff --git a/src/ngscopeclient/DigitalOutputChannelDialog.cpp b/src/ngscopeclient/DigitalOutputChannelDialog.cpp index 5e2a0377e..c96621147 100644 --- a/src/ngscopeclient/DigitalOutputChannelDialog.cpp +++ b/src/ngscopeclient/DigitalOutputChannelDialog.cpp @@ -51,6 +51,7 @@ DigitalOutputChannelDialog::DigitalOutputChannelDialog(DigitalOutputChannel* cha , m_drive("") , m_committedDrive(0) { + m_session = &parent->GetSession(); m_committedDisplayName = m_channel->GetDisplayName(); m_displayName = m_committedDisplayName; diff --git a/src/ngscopeclient/FunctionGeneratorDialog.cpp b/src/ngscopeclient/FunctionGeneratorDialog.cpp deleted file mode 100644 index 0d0cd70e4..000000000 --- a/src/ngscopeclient/FunctionGeneratorDialog.cpp +++ /dev/null @@ -1,299 +0,0 @@ -/*********************************************************************************************************************** -* * -* ngscopeclient * -* * -* Copyright (c) 2012-2024 Andrew D. Zonenberg and contributors * -* All rights reserved. * -* * -* Redistribution and use in source and binary forms, with or without modification, are permitted provided that the * -* following conditions are met: * -* * -* * Redistributions of source code must retain the above copyright notice, this list of conditions, and the * -* following disclaimer. * -* * -* * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the * -* following disclaimer in the documentation and/or other materials provided with the distribution. * -* * -* * Neither the name of the author nor the names of any contributors may be used to endorse or promote products * -* derived from this software without specific prior written permission. * -* * -* THIS SOFTWARE IS PROVIDED BY THE AUTHORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED * -* TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL * -* THE AUTHORS BE HELD LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES * -* (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR * -* BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT * -* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE * -* POSSIBILITY OF SUCH DAMAGE. * -* * -***********************************************************************************************************************/ - -/** - @file - @author Andrew D. Zonenberg - @brief Implementation of FunctionGeneratorDialog - */ - -#include "ngscopeclient.h" -#include "FunctionGeneratorDialog.h" - -using namespace std; - -//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// -// Construction / destruction - -FunctionGeneratorDialog::FunctionGeneratorDialog(shared_ptr generator, std::shared_ptr sessionState, Session* session) - : Dialog( - string("Function Generator: ") + generator->m_nickname, - string("Function Generator: ") + generator->m_nickname, - ImVec2(400, 350)) - , m_session(session) - , m_generator(generator) - , m_state(sessionState) -{ - Unit hz(Unit::UNIT_HZ); - Unit percent(Unit::UNIT_PERCENT); - Unit volts(Unit::UNIT_VOLTS); - Unit fs(Unit::UNIT_FS); - - size_t n = m_generator->GetChannelCount(); - for(size_t i=0; iGetInstrumentTypesForChannel(i) & Instrument::INST_FUNCTION)) - { - //Add dummy placeholder (never used) - m_uiState.push_back(state); - continue; - } - - state.m_outputEnabled = m_generator->GetFunctionChannelActive(i); - - state.m_committedAmplitude = m_generator->GetFunctionChannelAmplitude(i); - state.m_amplitude = volts.PrettyPrint(state.m_committedAmplitude); - - state.m_committedOffset = m_generator->GetFunctionChannelOffset(i); - state.m_offset = volts.PrettyPrint(state.m_committedOffset); - - state.m_committedDutyCycle = m_generator->GetFunctionChannelDutyCycle(i); - state.m_dutyCycle = percent.PrettyPrint(state.m_committedDutyCycle); - - state.m_committedFrequency = m_generator->GetFunctionChannelFrequency(i); - state.m_frequency = hz.PrettyPrint(state.m_committedFrequency); - - state.m_committedRiseTime = m_generator->GetFunctionChannelRiseTime(i); - state.m_riseTime = fs.PrettyPrint(state.m_committedRiseTime); - - state.m_committedFallTime = m_generator->GetFunctionChannelFallTime(i); - state.m_fallTime = fs.PrettyPrint(state.m_committedFallTime); - - //Convert waveform shape to list box index - state.m_waveShapes = m_generator->GetAvailableWaveformShapes(i); - state.m_shapeIndex = 0; - auto shape = m_generator->GetFunctionChannelShape(i); - for(size_t j=0; jGetNameOfShape(state.m_waveShapes[j])); - } - - if(m_generator->GetFunctionChannelOutputImpedance(i) == FunctionGenerator::IMPEDANCE_50_OHM) - state.m_impedanceIndex = 1; - else - state.m_impedanceIndex = 0; - - m_uiState.push_back(state); - } - - m_impedances.push_back(FunctionGenerator::IMPEDANCE_HIGH_Z); - m_impedances.push_back(FunctionGenerator::IMPEDANCE_50_OHM); - m_impedanceNames.push_back("High-Z"); - m_impedanceNames.push_back("50Ω"); -} - -FunctionGeneratorDialog::~FunctionGeneratorDialog() -{ -} - -//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// -// Rendering - -bool FunctionGeneratorDialog::DoRender() -{ - //Device information - if(ImGui::CollapsingHeader("Info")) - { - ImGui::BeginDisabled(); - - auto name = m_generator->GetName(); - auto vendor = m_generator->GetVendor(); - auto serial = m_generator->GetSerial(); - auto driver = m_generator->GetDriverName(); - auto transport = m_generator->GetTransport(); - auto tname = transport->GetName(); - auto tstring = transport->GetConnectionString(); - - ImGui::InputText("Make", &vendor[0], vendor.size()); - ImGui::InputText("Model", &name[0], name.size()); - ImGui::InputText("Serial", &serial[0], serial.size()); - ImGui::InputText("Driver", &driver[0], driver.size()); - ImGui::InputText("Transport", &tname[0], tname.size()); - ImGui::InputText("Path", &tstring[0], tstring.size()); - - ImGui::EndDisabled(); - } - - size_t n = m_generator->GetChannelCount(); - for(size_t i=0; iGetInstrumentTypesForChannel(i) & Instrument::INST_FUNCTION)) - continue; - - DoChannel(i); - } - - return true; -} - -/** - @brief Run the UI for a single channel - */ -void FunctionGeneratorDialog::DoChannel(size_t i) -{ - auto chname = m_generator->GetChannel(i)->GetDisplayName(); - - float valueWidth = 200; - - Unit pct(Unit::UNIT_PERCENT); - Unit hz(Unit::UNIT_HZ); - Unit volts(Unit::UNIT_VOLTS); - Unit fs(Unit::UNIT_FS); - - if(ImGui::CollapsingHeader(chname.c_str(), ImGuiTreeNodeFlags_DefaultOpen)) - { - //Check for updates (value changed instrument side since last commit) - //TODO: move to background thread? or just rely on clientside caching to make it fast? - auto freq = m_generator->GetFunctionChannelFrequency(i); - if(freq != m_uiState[i].m_committedFrequency) - { - m_uiState[i].m_committedFrequency = freq; - m_uiState[i].m_frequency = hz.PrettyPrint(freq); - } - - ImGui::PushID(chname.c_str()); - - if(ImGui::Checkbox("Output Enable", &m_uiState[i].m_outputEnabled)) - { - m_generator->SetFunctionChannelActive(i, m_uiState[i].m_outputEnabled); - - // Tell intrument thread that the FunctionGenerator state has to be updated - m_state->m_needsUpdate[i] = true; - } - HelpMarker("Turns the output signal from this channel on or off"); - - if(m_generator->HasFunctionImpedanceControls(i)) - { - ImGui::SetNextItemWidth(valueWidth); - if(Combo("Output Impedance", m_impedanceNames, m_uiState[i].m_impedanceIndex)) - { - auto& state = m_uiState[i]; - m_generator->SetFunctionChannelOutputImpedance(i, m_impedances[state.m_impedanceIndex]); - - //Refresh amplitude and offset when changing impedance - state.m_committedAmplitude = m_generator->GetFunctionChannelAmplitude(i); - state.m_amplitude = volts.PrettyPrint(state.m_committedAmplitude); - state.m_committedOffset = m_generator->GetFunctionChannelOffset(i); - state.m_offset = volts.PrettyPrint(state.m_committedOffset); - - // Tell intrument thread that the FunctionGenerator state has to be updated - m_state->m_needsUpdate[i] = true; - } - HelpMarker( - "Select the expected load impedance.\n\n" - "If set incorrectly, amplitude and offset will be inaccurate due to reflections."); - } - - //Amplitude and offset are potentially damaging operations - //Require the user to explicitly commit changes before they take effect - ImGui::SetNextItemWidth(valueWidth); - if(UnitInputWithExplicitApply("Amplitude", m_uiState[i].m_amplitude, m_uiState[i].m_committedAmplitude, volts)) - { - m_generator->SetFunctionChannelAmplitude(i, m_uiState[i].m_committedAmplitude); - // Tell intrument thread that the FunctionGenerator state has to be updated - m_state->m_needsUpdate[i] = true; - } - HelpMarker("Peak-to-peak amplitude of the generated waveform"); - - ImGui::SetNextItemWidth(valueWidth); - if(UnitInputWithExplicitApply("Offset", m_uiState[i].m_offset, m_uiState[i].m_committedOffset, volts)) - { - m_generator->SetFunctionChannelOffset(i, m_uiState[i].m_committedOffset); - // Tell intrument thread that the FunctionGenerator state has to be updated - m_state->m_needsUpdate[i] = true; - } - HelpMarker("DC offset for the waveform above (positive) or below (negative) ground"); - - //All other settings apply when user presses enter or focus is lost - ImGui::SetNextItemWidth(valueWidth); - if(Combo("Waveform", m_uiState[i].m_waveShapeNames, m_uiState[i].m_shapeIndex)) - { - m_generator->SetFunctionChannelShape(i, m_uiState[i].m_waveShapes[m_uiState[i].m_shapeIndex]); - // Tell intrument thread that the FunctionGenerator state has to be updated - m_state->m_needsUpdate[i] = true; - } - HelpMarker("Select the type of waveform to generate"); - - ImGui::SetNextItemWidth(valueWidth); - if(UnitInputWithImplicitApply("Frequency", m_uiState[i].m_frequency, m_uiState[i].m_committedFrequency, hz)) - { - m_generator->SetFunctionChannelFrequency(i, m_uiState[i].m_committedFrequency); - // Tell intrument thread that the FunctionGenerator state has to be updated - m_state->m_needsUpdate[i] = true; - } - - //Duty cycle controls are not available in all generators - if(m_generator->HasFunctionDutyCycleControls(i)) - { - auto waveformType = m_uiState[i].m_waveShapes[m_uiState[i].m_shapeIndex]; - bool hasDutyCycle = false; - switch(waveformType) - { - case FunctionGenerator::SHAPE_PULSE: - case FunctionGenerator::SHAPE_SQUARE: - case FunctionGenerator::SHAPE_PRBS_NONSTANDARD: - hasDutyCycle = true; - break; - - default: - hasDutyCycle = false; - } - ImGui::SetNextItemWidth(valueWidth); - if(!hasDutyCycle) - ImGui::BeginDisabled(); - if(UnitInputWithImplicitApply("Duty Cycle", m_uiState[i].m_dutyCycle, m_uiState[i].m_committedDutyCycle, pct)) - m_generator->SetFunctionChannelDutyCycle(i, m_uiState[i].m_committedDutyCycle); - if(!hasDutyCycle) - ImGui::EndDisabled(); - HelpMarker("Duty cycle of the waveform, in percent. Not applicable to all waveform types."); - } - - //Rise and fall time controls are not present in all generators - //TODO: not all waveforms make sense to have rise/fall times etiher - if(m_generator->HasFunctionRiseFallTimeControls(i)) - { - ImGui::SetNextItemWidth(valueWidth); - if(UnitInputWithImplicitApply("Rise Time", m_uiState[i].m_riseTime, m_uiState[i].m_committedRiseTime, fs)) - m_generator->SetFunctionChannelRiseTime(i, m_uiState[i].m_committedRiseTime); - - ImGui::SetNextItemWidth(valueWidth); - if(UnitInputWithImplicitApply("Fall Time", m_uiState[i].m_fallTime, m_uiState[i].m_committedFallTime, fs)) - m_generator->SetFunctionChannelFallTime(i, m_uiState[i].m_committedFallTime); - } - - ImGui::PopID(); - } - - //Push config for dedicated generators - if(dynamic_pointer_cast(m_generator) == nullptr) - m_generator->GetTransport()->FlushCommandQueue(); -} diff --git a/src/ngscopeclient/FunctionGeneratorDialog.h b/src/ngscopeclient/FunctionGeneratorDialog.h deleted file mode 100644 index ef07be81a..000000000 --- a/src/ngscopeclient/FunctionGeneratorDialog.h +++ /dev/null @@ -1,108 +0,0 @@ -/*********************************************************************************************************************** -* * -* ngscopeclient * -* * -* Copyright (c) 2012-2024 Andrew D. Zonenberg and contributors * -* All rights reserved. * -* * -* Redistribution and use in source and binary forms, with or without modification, are permitted provided that the * -* following conditions are met: * -* * -* * Redistributions of source code must retain the above copyright notice, this list of conditions, and the * -* following disclaimer. * -* * -* * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the * -* following disclaimer in the documentation and/or other materials provided with the distribution. * -* * -* * Neither the name of the author nor the names of any contributors may be used to endorse or promote products * -* derived from this software without specific prior written permission. * -* * -* THIS SOFTWARE IS PROVIDED BY THE AUTHORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED * -* TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL * -* THE AUTHORS BE HELD LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES * -* (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR * -* BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT * -* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE * -* POSSIBILITY OF SUCH DAMAGE. * -* * -***********************************************************************************************************************/ - -/** - @file - @author Andrew D. Zonenberg - @brief Declaration of FunctionGeneratorDialog - */ -#ifndef FunctionGeneratorDialog_h -#define FunctionGeneratorDialog_h - -#include "Dialog.h" -#include "RollingBuffer.h" -#include "Session.h" - -class FunctionGeneratorChannelUIState -{ -public: - bool m_outputEnabled; - - std::string m_amplitude; - float m_committedAmplitude; - - std::string m_offset; - float m_committedOffset; - - std::string m_dutyCycle; - float m_committedDutyCycle; - - std::string m_frequency; - float m_committedFrequency; - - std::string m_riseTime; - float m_committedRiseTime; - - std::string m_fallTime; - float m_committedFallTime; - - int m_impedanceIndex; - - int m_shapeIndex; - std::vector m_waveShapes; - std::vector m_waveShapeNames; -}; - -class FunctionGeneratorDialog : public Dialog -{ -public: - FunctionGeneratorDialog(std::shared_ptr gen, std::shared_ptr sessionState, Session* session); - virtual ~FunctionGeneratorDialog(); - - virtual bool DoRender(); - - std::shared_ptr GetGenerator() - { return m_generator; } - -protected: - void DoChannel(size_t i); - - ///@brief Session handle so we can remove the PSU when closed - Session* m_session; - - ///@brief The generator we're controlling - std::shared_ptr m_generator; - - ///@brief Current channel stats, live updated - std::shared_ptr m_state; - - ///@brief UI state for each channel - std::vector m_uiState; - - ///@brief Output impedances - std::vector m_impedances; - - ///@brief Human readable description of each element in m_impedances - std::vector m_impedanceNames; - -}; - - - -#endif diff --git a/src/ngscopeclient/FunctionGeneratorState.h b/src/ngscopeclient/FunctionGeneratorState.h index 58a8bda3e..b2cf995d5 100644 --- a/src/ngscopeclient/FunctionGeneratorState.h +++ b/src/ngscopeclient/FunctionGeneratorState.h @@ -2,7 +2,7 @@ * * * ngscopeclient * * * -* Copyright (c) 2012-2024 Andrew D. Zonenberg and contributors * +* Copyright (c) 2012-2026 Andrew D. Zonenberg and contributors * * All rights reserved. * * * * Redistribution and use in source and binary forms, with or without modification, are permitted provided that the * @@ -45,10 +45,14 @@ class FunctionGeneratorState FunctionGeneratorState(std::shared_ptr generator) { size_t n = generator->GetChannelCount(); - m_channelActive = std::make_unique[] >(n); + m_channelNumber = n; + m_channelActive = std::make_unique(n); m_channelAmplitude = std::make_unique[] >(n); m_channelOffset= std::make_unique[] >(n); m_channelFrequency = std::make_unique[] >(n); + m_channelDutyCycle = std::make_unique[] >(n); + m_channelRiseTime = std::make_unique[] >(n); + m_channelFallTime = std::make_unique[] >(n); m_channelShape = std::make_unique[] >(n); m_channelOutputImpedance = std::make_unique[] >(n); m_channelShapes = std::make_unique[] >(n); @@ -63,6 +67,12 @@ class FunctionGeneratorState m_committedAmplitude = std::make_unique(n); m_strFrequency = std::make_unique(n); m_committedFrequency = std::make_unique(n); + m_strDutyCycle = std::make_unique(n); + m_committedDutyCycle = std::make_unique(n); + m_strRiseTime = std::make_unique(n); + m_committedRiseTime = std::make_unique(n); + m_strFallTime = std::make_unique(n); + m_committedFallTime = std::make_unique(n); Unit volts(Unit::UNIT_VOLTS); @@ -72,6 +82,9 @@ class FunctionGeneratorState m_channelAmplitude[i] = 0; m_channelOffset[i] = 0; m_channelFrequency[i] = 0; + m_channelDutyCycle[i] = 0; + m_channelRiseTime[i] = 0; + m_channelFallTime[i] = 0; m_channelShape[i] = FunctionGenerator::WaveShape::SHAPE_SINE; m_channelOutputImpedance[i] = FunctionGenerator::OutputImpedance::IMPEDANCE_HIGH_Z; // Init shape list and names @@ -86,13 +99,25 @@ class FunctionGeneratorState m_committedAmplitude[i] = FLT_MIN; m_committedOffset[i] = FLT_MIN; m_committedFrequency[i] = FLT_MIN; + m_committedDutyCycle[i] = FLT_MIN; + m_committedRiseTime[i] = FLT_MIN; + m_committedFallTime[i] = FLT_MIN; } } - std::unique_ptr[]> m_channelActive; + void FlushConfigCache() + { + for(size_t i = 0 ; i < m_channelNumber.load() ; i++) + m_needsUpdate[i] = true; + } + + std::unique_ptr m_channelActive; std::unique_ptr[]> m_channelAmplitude; std::unique_ptr[]> m_channelOffset; std::unique_ptr[]> m_channelFrequency; + std::unique_ptr[]> m_channelDutyCycle; + std::unique_ptr[]> m_channelRiseTime; + std::unique_ptr[]> m_channelFallTime; std::unique_ptr[]> m_channelShape; std::unique_ptr[]> m_channelOutputImpedance; std::unique_ptr[]> m_channelShapes; @@ -101,6 +126,8 @@ class FunctionGeneratorState std::unique_ptr[]> m_needsUpdate; + std::atomic m_channelNumber; + //UI state for dialogs etc std::unique_ptr m_committedOffset; std::unique_ptr m_strOffset; @@ -110,6 +137,15 @@ class FunctionGeneratorState std::unique_ptr m_committedFrequency; std::unique_ptr m_strFrequency; + + std::unique_ptr m_committedDutyCycle; + std::unique_ptr m_strDutyCycle; + + std::unique_ptr m_committedRiseTime; + std::unique_ptr m_strRiseTime; + + std::unique_ptr m_committedFallTime; + std::unique_ptr m_strFallTime; }; #endif diff --git a/src/ngscopeclient/InstrumentThread.cpp b/src/ngscopeclient/InstrumentThread.cpp index 9346b7799..a1fad710a 100644 --- a/src/ngscopeclient/InstrumentThread.cpp +++ b/src/ngscopeclient/InstrumentThread.cpp @@ -71,6 +71,7 @@ void InstrumentThread(InstrumentThreadArgs args) auto misc = dynamic_pointer_cast(inst); auto psu = dynamic_pointer_cast(inst); auto awg = dynamic_pointer_cast(inst); + auto scopestate = args.oscilloscopestate; auto loadstate = args.loadstate; auto meterstate = args.meterstate; auto bertstate = args.bertstate; @@ -127,6 +128,118 @@ void InstrumentThread(InstrumentThreadArgs args) } triggerUpToDate = false; } + + if(scopestate) + { // Update state + //Read status for channels that need it + for(size_t i=0; iGetChannelCount(); i++) + { + if(scopestate->m_needsUpdate[i]) + { + //Skip non-scope channels + auto scopechan = dynamic_cast(scope->GetChannel(i)); + if(!scopechan) + continue; + + scopestate->m_channelInverted[i] = scope->IsInverted(i); + + bool isDigital = (scopechan->GetType(0) == Stream::STREAM_TYPE_DIGITAL); + if(isDigital) + { + Unit unit = scopechan->GetYAxisUnits(0); + scopestate->m_channelDigitalTrehshold[i] = scope->GetDigitalThreshold(i); + scopestate->m_committedDigitalThreshold[i] = scopestate->m_channelDigitalTrehshold[i]; + scopestate->m_strDigitalThreshold[i] = unit.PrettyPrint(scopestate->m_channelDigitalTrehshold[i]); + } + else + { + scopestate->m_channelAttenuation[i] = scope->GetChannelAttenuation(i); + scopestate->m_channelBandwidthLimit[i] = scope->GetChannelBandwidthLimit(i); + scopestate->m_committedAttenuation[i] = scopestate->m_channelAttenuation[i]; + Unit counts(Unit::UNIT_COUNTS); + scopestate->m_strAttenuation[i] = counts.PrettyPrint(scopestate->m_committedAttenuation[i]); + + size_t nstreams = scopechan->GetStreamCount(); + for(size_t j=0; jGetOffset(j); + float range = scopechan->GetVoltageRange(j); + Unit unit = scopechan->GetYAxisUnits(j); + scopestate->m_channelOffset[i][j] = scopechan->GetOffset(j); + scopestate->m_channelRange[i][j] = scopechan->GetVoltageRange(j); + scopestate->m_committedOffset[i][j] = offset; + scopestate->m_committedRange[i][j] = range; + scopestate->m_strOffset[i][j] = unit.PrettyPrint(offset); + scopestate->m_strRange[i][j] = unit.PrettyPrint(range); + } + // Get probe name + scopestate->m_probeName[i] = scope->GetProbeName(i); + // Populate bandwidth limit values + auto limit = scope->GetChannelBandwidthLimit(i); + scopestate->m_bandwidthLimits[i].clear(); + scopestate->m_bandwidthLimitNames[i].clear(); + scopestate->m_bandwidthLimits[i] = scope->GetChannelBandwidthLimiters(i); + Unit hz(Unit::UNIT_HZ); + for(size_t j=0; jm_bandwidthLimits[i].size(); j++) + { + auto b = scopestate->m_bandwidthLimits[i][j]; + if(b == 0) + scopestate->m_bandwidthLimitNames[i].push_back("Full"); + else + scopestate->m_bandwidthLimitNames[i].push_back(hz.PrettyPrint(b*1e6)); + + if(b == limit) + scopestate->m_channelBandwidthLimit[i] = j; + } + + + // Populate coupling values + auto coupling = scope->GetChannelCoupling(i); + scopestate->m_couplings[i].clear(); + scopestate->m_couplingNames[i].clear(); + scopestate->m_couplings[i] = scope->GetAvailableCouplings(i); + for(size_t j=0; jm_couplings[i].size(); j++) + { + auto c = scopestate->m_couplings[i][j]; + + switch(c) + { + case OscilloscopeChannel::COUPLE_DC_50: + scopestate->m_couplingNames[i].push_back("DC 50Ω"); + break; + + case OscilloscopeChannel::COUPLE_AC_50: + scopestate->m_couplingNames[i].push_back("AC 50Ω"); + break; + + case OscilloscopeChannel::COUPLE_DC_1M: + scopestate->m_couplingNames[i].push_back("DC 1MΩ"); + break; + + case OscilloscopeChannel::COUPLE_AC_1M: + scopestate->m_couplingNames[i].push_back("AC 1MΩ"); + break; + + case OscilloscopeChannel::COUPLE_GND: + scopestate->m_couplingNames[i].push_back("Ground"); + break; + + default: + scopestate->m_couplingNames[i].push_back("Invalid"); + break; + } + if(c == coupling) + scopestate->m_channelCoupling[i] = j; + } + } + + session->MarkChannelDirty(scopechan); + + scopestate->m_needsUpdate[i] = false; + } + + } + } } //Always acquire data from non-scope instruments @@ -150,6 +263,24 @@ void InstrumentThread(InstrumentThreadArgs args) psustate->m_channelFuseTripped[i] = psu->GetPowerOvercurrentShutdownTripped(i); psustate->m_channelOn[i] = psu->GetPowerChannelActive(i); + if(psustate->m_needsUpdate[i]) + { + psustate->m_overcurrentShutdownEnabled[i] = psu->GetPowerOvercurrentShutdownEnabled(i); + psustate->m_softStartEnabled[i] = psu->IsSoftStartEnabled(i); + psustate->m_committedSetVoltage[i] = psu->GetPowerVoltageNominal(i); + psustate->m_committedSetCurrent[i] = psu->GetPowerCurrentNominal(i); + psustate->m_committedSSRamp[i] = psu->GetSoftStartRampTime(i); + Unit volts(Unit::UNIT_VOLTS); + Unit amps(Unit::UNIT_AMPS); + Unit fs(Unit::UNIT_FS); + psustate->m_setVoltage[i] = volts.PrettyPrint(psustate->m_committedSetVoltage[i]); + psustate->m_setCurrent[i] = amps.PrettyPrint(psustate->m_committedSetCurrent[i]); + psustate->m_setSSRamp[i] = fs.PrettyPrint(psustate->m_committedSSRamp[i]); + + psustate->m_needsUpdate[i] = false; + + } + session->MarkChannelDirty(pchan); } @@ -180,6 +311,13 @@ void InstrumentThread(InstrumentThreadArgs args) meterstate->m_secondaryMeasurement = chan->GetSecondaryValue(); meterstate->m_firstUpdateDone = true; + if(meterstate->m_needsUpdate.load()) + { // We need to update dmm state + meterstate->m_selectedChannel = meter->GetCurrentMeterChannel(); + meterstate->m_autoRange = meter->GetMeterAutoRange(); + meterstate->m_needsUpdate = false; + } + session->MarkChannelDirty(chan); } } @@ -234,8 +372,6 @@ void InstrumentThread(InstrumentThreadArgs args) { if(awgstate->m_needsUpdate[i]) { - Unit volts(Unit::UNIT_VOLTS); - //Skip non-awg channels auto awgchan = dynamic_cast(awg->GetChannel(i)); if(!awgchan) @@ -244,6 +380,9 @@ void InstrumentThread(InstrumentThreadArgs args) awgstate->m_channelAmplitude[i] = awg->GetFunctionChannelAmplitude(i); awgstate->m_channelOffset[i] = awg->GetFunctionChannelOffset(i); awgstate->m_channelFrequency[i] = awg->GetFunctionChannelFrequency(i); + awgstate->m_channelDutyCycle[i] = awg->GetFunctionChannelDutyCycle(i); + awgstate->m_channelRiseTime[i] = awg->GetFunctionChannelRiseTime(i); + awgstate->m_channelFallTime[i] = awg->GetFunctionChannelFallTime(i); awgstate->m_channelShape[i] = awg->GetFunctionChannelShape(i); awgstate->m_channelOutputImpedance[i] = awg->GetFunctionChannelOutputImpedance(i); session->MarkChannelDirty(awgchan); diff --git a/src/ngscopeclient/LoadDialog.cpp b/src/ngscopeclient/LoadDialog.cpp index af74463ca..93dedc340 100644 --- a/src/ngscopeclient/LoadDialog.cpp +++ b/src/ngscopeclient/LoadDialog.cpp @@ -45,8 +45,7 @@ LoadDialog::LoadDialog(shared_ptr load, shared_ptr state, S : Dialog( string("Load: ") + load->m_nickname, string("Load: ") + load->m_nickname, - ImVec2(500, 400)) - , m_session(session) + ImVec2(500, 400), session) , m_tstart(GetTime()) , m_load(load) , m_state(state) diff --git a/src/ngscopeclient/LoadDialog.h b/src/ngscopeclient/LoadDialog.h index a1ee8227d..03d461e43 100644 --- a/src/ngscopeclient/LoadDialog.h +++ b/src/ngscopeclient/LoadDialog.h @@ -155,9 +155,6 @@ class LoadDialog : public Dialog protected: void ChannelSettings(size_t channel); - ///@brief Session handle so we can remove the load when closed - Session* m_session; - ///@brief Timestamp of when we opened the dialog double m_tstart; diff --git a/src/ngscopeclient/MainWindow.cpp b/src/ngscopeclient/MainWindow.cpp index d43e77915..c13ded6e9 100644 --- a/src/ngscopeclient/MainWindow.cpp +++ b/src/ngscopeclient/MainWindow.cpp @@ -55,14 +55,12 @@ #include "FileBrowser.h" #include "FilterGraphWorkspace.h" #include "FilterPropertiesDialog.h" -#include "FunctionGeneratorDialog.h" #include "HistoryDialog.h" #include "LoadDialog.h" #include "LogViewerDialog.h" #include "ManageInstrumentsDialog.h" #include "MeasurementsDialog.h" #include "MetricsDialog.h" -#include "MultimeterDialog.h" #include "NotesDialog.h" #include "PersistenceSettingsDialog.h" #include "PowerSupplyDialog.h" @@ -1121,18 +1119,10 @@ void MainWindow::ToolbarButtons() void MainWindow::OnDialogClosed(const std::shared_ptr& dlg) { //Handle multi-instance dialogs - auto meterDlg = dynamic_pointer_cast(dlg); - if(meterDlg) - m_meterDialogs.erase(meterDlg->GetMeter()); - auto psuDlg = dynamic_pointer_cast(dlg); if(psuDlg) m_psuDialogs.erase(psuDlg->GetPSU()); - auto genDlg = dynamic_pointer_cast(dlg); - if(genDlg) - m_generatorDialogs.erase(genDlg->GetGenerator()); - auto rgenDlg = dynamic_pointer_cast(dlg); if(rgenDlg) m_rfgeneratorDialogs.erase(rgenDlg->GetGenerator()); @@ -1499,30 +1489,6 @@ void MainWindow::ShowInstrumentProperties(std::shared_ptr instrument AddDialog(make_shared(psu, m_session.GetPSUState(psu), &m_session)); return; } - // Meter - auto dmm = dynamic_pointer_cast(instrument); - if(dmm) - { - if(m_meterDialogs.find(dmm) != m_meterDialogs.end()) - { - LogTrace("Multimeter properties dialog is already open, no action required\n"); - return; - } - m_session.AddMultimeterDialog(dmm); - return; - } - // AWG - auto awg = dynamic_pointer_cast(instrument); - if(awg) - { - if(m_generatorDialogs.find(awg) != m_generatorDialogs.end()) - { - LogTrace("Generator properties dialog is already open, no action required\n"); - return; - } - AddDialog(make_shared(awg, m_session.GetFunctionGeneratorState(awg), &m_session)); - return; - } // Bert auto bert = dynamic_pointer_cast(instrument); if(bert) @@ -2758,45 +2724,6 @@ bool MainWindow::LoadDialogs(const YAML::Node& node) } } - auto meters = node["meters"]; - if(meters) - { - for(auto it : meters) - { - auto meter = dynamic_cast(m_session.m_idtable.Lookup(it.second.as())); - if(meter) - { - auto smeter = dynamic_pointer_cast(meter->shared_from_this()); - m_session.AddMultimeterDialog(smeter); - } - else - { - ShowErrorPopup("Invalid meter", "Multimeter dialog references nonexistent instrument"); - continue; - } - } - } - - auto generators = node["generators"]; - if(generators) - { - for(auto it : generators) - { - auto gen = dynamic_cast(m_session.m_idtable.Lookup(it.second.as())); - - if(gen) - { - auto sgen = dynamic_pointer_cast(gen->shared_from_this()); - AddDialog(make_shared(sgen, m_session.GetFunctionGeneratorState(sgen), &m_session)); - } - else - { - ShowErrorPopup("Invalid function generator", "Function generator dialog references nonexistent instrument"); - continue; - } - } - } - auto psus = node["psus"]; if(psus) { diff --git a/src/ngscopeclient/MainWindow.h b/src/ngscopeclient/MainWindow.h index aa5725a72..c06bea169 100644 --- a/src/ngscopeclient/MainWindow.h +++ b/src/ngscopeclient/MainWindow.h @@ -53,7 +53,6 @@ #include "../scopehal/PacketDecoder.h" class MeasurementsDialog; -class MultimeterDialog; class HistoryDialog; class FileBrowser; class CreateFilterBrowser; @@ -259,9 +258,7 @@ class MainWindow : public VulkanWindow void SetupMenu(); void WindowMenu(); void WindowAnalyzerMenu(); - void WindowGeneratorMenu(); void WindowPSUMenu(); - void WindowMultimeterMenu(); void DebugMenu(); void DebugSCPIConsoleMenu(); void HelpMenu(); diff --git a/src/ngscopeclient/MainWindow_Menus.cpp b/src/ngscopeclient/MainWindow_Menus.cpp index 4ea2199c1..1521e1552 100644 --- a/src/ngscopeclient/MainWindow_Menus.cpp +++ b/src/ngscopeclient/MainWindow_Menus.cpp @@ -46,14 +46,12 @@ #include "BERTDialog.h" #include "CreateFilterBrowser.h" #include "FilterGraphEditor.h" -#include "FunctionGeneratorDialog.h" #include "HistoryDialog.h" #include "LoadDialog.h" #include "LogViewerDialog.h" #include "MeasurementsDialog.h" #include "MemoryLeakerDialog.h" #include "MetricsDialog.h" -#include "MultimeterDialog.h" #include "NotesDialog.h" #include "PersistenceSettingsDialog.h" #include "PowerSupplyDialog.h" @@ -72,10 +70,6 @@ void MainWindow::AddDialog(shared_ptr dlg) { m_dialogs.emplace(dlg); - auto mdlg = dynamic_cast(dlg.get()); - if(mdlg != nullptr) - m_meterDialogs[mdlg->GetMeter()] = dlg; - auto pdlg = dynamic_cast(dlg.get()); if(pdlg != nullptr) m_psuDialogs[pdlg->GetPSU()] = dlg; @@ -84,10 +78,6 @@ void MainWindow::AddDialog(shared_ptr dlg) if(bdlg != nullptr) m_bertDialogs[bdlg->GetBERT()] = dlg; - auto fdlg = dynamic_cast(dlg.get()); - if(fdlg != nullptr) - m_generatorDialogs[fdlg->GetGenerator()] = dlg; - auto rdlg = dynamic_cast(dlg.get()); if(rdlg != nullptr) m_rfgeneratorDialogs[rdlg->GetGenerator()] = dlg; @@ -535,8 +525,6 @@ void MainWindow::WindowMenu() if(ImGui::BeginMenu("Window")) { WindowAnalyzerMenu(); - WindowGeneratorMenu(); - WindowMultimeterMenu(); WindowPSUMenu(); bool hasLabNotes = m_notesDialog != nullptr; @@ -689,50 +677,6 @@ void MainWindow::WindowAnalyzerMenu() ImGui::EndDisabled(); } -/** - @brief Run the Window | Generator menu - - This menu is used for connecting to a function generator that is part of an oscilloscope or other instrument. - */ -void MainWindow::WindowGeneratorMenu() -{ - //Make a list of generators - vector< shared_ptr > gens; - auto insts = m_session.GetSCPIInstruments(); - for(auto inst : insts) - { - //Skip anything that's not a function generator - if( (inst->GetInstrumentTypes() & Instrument::INST_FUNCTION) == 0) - continue; - - //Do we already have a dialog open for it? If so, don't make another - auto generator = dynamic_pointer_cast(inst); - if(m_generatorDialogs.find(generator) != m_generatorDialogs.end()) - continue; - - gens.push_back(generator); - } - - ImGui::BeginDisabled(gens.empty()); - if(ImGui::BeginMenu("Generator")) - { - for(auto generator : gens) - { - //Add it to the menu - if(ImGui::MenuItem(generator->m_nickname.c_str())) - { - AddDialog(make_shared( - generator, - m_session.GetFunctionGeneratorState(generator), - &m_session)); - } - } - - ImGui::EndMenu(); - } - ImGui::EndDisabled(); -} - /** @brief Run the Window | Power Supply menu @@ -772,43 +716,6 @@ void MainWindow::WindowPSUMenu() ImGui::EndDisabled(); } -/** - @brief Run the Window | Multimeter menu - */ -void MainWindow::WindowMultimeterMenu() -{ - //This is a bit of a hack but all of the dialogs are gonna get redone eventually so - vector< shared_ptr > meters; - auto insts = m_session.GetScopes(); - for(auto inst : insts) - { - //Skip anything that's not a multimeter - if( (inst->GetInstrumentTypes() & Instrument::INST_DMM) == 0) - continue; - - //Do we already have a dialog open for it? If so, don't make another - auto meter = dynamic_pointer_cast(inst); - if(m_meterDialogs.find(meter) != m_meterDialogs.end()) - continue; - - meters.push_back(meter); - } - - ImGui::BeginDisabled(meters.empty()); - if(ImGui::BeginMenu("Multimeter")) - { - for(auto meter : meters) - { - //Add it to the menu - if(ImGui::MenuItem(meter->m_nickname.c_str())) - m_session.AddInstrument(meter); - } - - ImGui::EndMenu(); - } - ImGui::EndDisabled(); -} - /** @brief Runs the Debug | SCPI Console menu */ diff --git a/src/ngscopeclient/MultimeterDialog.cpp b/src/ngscopeclient/MultimeterDialog.cpp deleted file mode 100644 index ec13e3823..000000000 --- a/src/ngscopeclient/MultimeterDialog.cpp +++ /dev/null @@ -1,233 +0,0 @@ -/*********************************************************************************************************************** -* * -* ngscopeclient * -* * -* Copyright (c) 2012-2024 Andrew D. Zonenberg and contributors * -* All rights reserved. * -* * -* Redistribution and use in source and binary forms, with or without modification, are permitted provided that the * -* following conditions are met: * -* * -* * Redistributions of source code must retain the above copyright notice, this list of conditions, and the * -* following disclaimer. * -* * -* * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the * -* following disclaimer in the documentation and/or other materials provided with the distribution. * -* * -* * Neither the name of the author nor the names of any contributors may be used to endorse or promote products * -* derived from this software without specific prior written permission. * -* * -* THIS SOFTWARE IS PROVIDED BY THE AUTHORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED * -* TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL * -* THE AUTHORS BE HELD LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES * -* (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR * -* BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT * -* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE * -* POSSIBILITY OF SUCH DAMAGE. * -* * -***********************************************************************************************************************/ - -/** - @file - @author Andrew D. Zonenberg - @brief Implementation of MultimeterDialog - */ - -#include "ngscopeclient.h" -#include "MultimeterDialog.h" - -using namespace std; - -//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// -// Construction / destruction - -MultimeterDialog::MultimeterDialog(shared_ptr meter, shared_ptr state, Session* session) - : Dialog( - string("Multimeter: ") + meter->m_nickname, - string("Multimeter: ") + meter->m_nickname, - ImVec2(500, 400)) - , m_session(session) - , m_tstart(GetTime()) - , m_meter(meter) - , m_state(state) - , m_selectedChannel(m_meter->GetCurrentMeterChannel()) - , m_autorange(m_meter->GetMeterAutoRange()) -{ - m_meter->StartMeter(); - - //Inputs - for(size_t i=0; iGetChannelCount(); i++) - m_channelNames.push_back(m_meter->GetChannel(i)->GetDisplayName()); - - //Primary operating modes - auto modemask = m_meter->GetMeasurementTypes(); - auto primode = m_meter->GetMeterMode(); - m_primaryModeSelector = 0; - for(unsigned int i=0; i<32; i++) - { - auto mode = static_cast(1 << i); - if(modemask & mode) - { - m_primaryModes.push_back(mode); - m_primaryModeNames.push_back(m_meter->ModeToText(mode)); - if(primode == mode) - m_primaryModeSelector = m_primaryModes.size() - 1; - } - } - - //Secondary operating modes - RefreshSecondaryModeList(); -} - -MultimeterDialog::~MultimeterDialog() -{ - m_meter->StopMeter(); -} - -//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// -// Rendering - -bool MultimeterDialog::DoRender() -{ - float valueWidth = 10 * ImGui::GetFontSize(); - - //Device information - if(ImGui::CollapsingHeader("Info")) - { - ImGui::BeginDisabled(); - - auto name = m_meter->GetName(); - auto vendor = m_meter->GetVendor(); - auto serial = m_meter->GetSerial(); - auto driver = m_meter->GetDriverName(); - auto transport = m_meter->GetTransport(); - auto tname = transport->GetName(); - auto tstring = transport->GetConnectionString(); - - ImGui::SetNextItemWidth(valueWidth); - ImGui::InputText("Make", &vendor[0], vendor.size()); - - ImGui::SetNextItemWidth(valueWidth); - ImGui::InputText("Model", &name[0], name.size()); - - ImGui::SetNextItemWidth(valueWidth); - ImGui::InputText("Serial", &serial[0], serial.size()); - - ImGui::SetNextItemWidth(valueWidth); - ImGui::InputText("Driver", &driver[0], driver.size()); - - ImGui::SetNextItemWidth(valueWidth); - ImGui::InputText("Transport", &tname[0], tname.size()); - - ImGui::SetNextItemWidth(valueWidth); - ImGui::InputText("Path", &tstring[0], tstring.size()); - - ImGui::EndDisabled(); - } - - //Save history - auto pri = m_state->m_primaryMeasurement.load(); - auto sec = m_state->m_secondaryMeasurement.load(); - bool firstUpdateDone = m_state->m_firstUpdateDone.load(); - bool hasSecondary = m_meter->GetSecondaryMeterMode() != Multimeter::NONE; - - auto primaryMode = m_meter->ModeToText(m_meter->GetMeterMode()); - auto secondaryMode = m_meter->ModeToText(m_meter->GetSecondaryMeterMode()); - - if(ImGui::CollapsingHeader("Configuration", ImGuiTreeNodeFlags_DefaultOpen)) - { - if(ImGui::Checkbox("Autorange", &m_autorange)) - m_meter->SetMeterAutoRange(m_autorange); - HelpMarker("Enables automatic selection of meter scale ranges."); - - //Channel selector (hide if we have only one channel) - if(m_meter->GetChannelCount() > 1) - { - ImGui::SetNextItemWidth(valueWidth); - if(Combo("Channel", m_channelNames, m_selectedChannel)) - m_meter->SetCurrentMeterChannel(m_selectedChannel); - - HelpMarker("Select which input channel is being monitored."); - } - - //Primary operating mode selector - ImGui::SetNextItemWidth(valueWidth); - if(Combo("Mode", m_primaryModeNames, m_primaryModeSelector)) - OnPrimaryModeChanged(); - HelpMarker("Select the type of measurement to make."); - - //Secondary operating mode selector - if(m_secondaryModeNames.empty()) - ImGui::BeginDisabled(); - ImGui::SetNextItemWidth(valueWidth); - if(Combo("Secondary Mode", m_secondaryModeNames, m_secondaryModeSelector)) - m_meter->SetSecondaryMeterMode(m_secondaryModes[m_secondaryModeSelector]); - if(m_secondaryModeNames.empty()) - ImGui::EndDisabled(); - - HelpMarker( - "Select auxiliary measurement mode, if supported.\n\n" - "The set of available auxiliary measurements depends on the current primary measurement mode."); - } - - if(ImGui::CollapsingHeader("Measurements", ImGuiTreeNodeFlags_DefaultOpen)) - { - string spri; - string ssec; - - //Hide values until we get first readings back from the meter - if(firstUpdateDone) - { - spri = m_meter->GetMeterUnit().PrettyPrint(pri, m_meter->GetMeterDigits()); - if(hasSecondary) - ssec = m_meter->GetSecondaryMeterUnit().PrettyPrint(sec, m_meter->GetMeterDigits()); - } - - ImGui::BeginDisabled(); - ImGui::SetNextItemWidth(valueWidth); - ImGui::InputText(primaryMode.c_str(), &spri[0], spri.size()); - ImGui::EndDisabled(); - HelpMarker("Most recent value for the primary measurement"); - - if(hasSecondary) - { - ImGui::BeginDisabled(); - ImGui::SetNextItemWidth(valueWidth); - ImGui::InputText(secondaryMode.c_str(), &ssec[0], ssec.size()); - ImGui::EndDisabled(); - HelpMarker("Most recent value for the secondary measurement"); - } - } - - return true; -} - -void MultimeterDialog::OnPrimaryModeChanged() -{ - //Push the new mode to the meter - m_meter->SetMeterMode(m_primaryModes[m_primaryModeSelector]); - - //Redo the list of available secondary meter modes - RefreshSecondaryModeList(); -} - -void MultimeterDialog::RefreshSecondaryModeList() -{ - m_secondaryModes.clear(); - m_secondaryModeNames.clear(); - m_secondaryModeSelector = -1; - - auto modemask = m_meter->GetSecondaryMeasurementTypes(); - auto secmode = m_meter->GetSecondaryMeterMode(); - for(unsigned int i=0; i<32; i++) - { - auto mode = static_cast(1 << i); - if(modemask & mode) - { - m_secondaryModes.push_back(mode); - m_secondaryModeNames.push_back(m_meter->ModeToText(mode)); - if(secmode == mode) - m_secondaryModeSelector = m_secondaryModes.size() - 1; - } - } -} diff --git a/src/ngscopeclient/MultimeterDialog.h b/src/ngscopeclient/MultimeterDialog.h deleted file mode 100644 index 85a69f464..000000000 --- a/src/ngscopeclient/MultimeterDialog.h +++ /dev/null @@ -1,98 +0,0 @@ -/*********************************************************************************************************************** -* * -* ngscopeclient * -* * -* Copyright (c) 2012-2024 Andrew D. Zonenberg and contributors * -* All rights reserved. * -* * -* Redistribution and use in source and binary forms, with or without modification, are permitted provided that the * -* following conditions are met: * -* * -* * Redistributions of source code must retain the above copyright notice, this list of conditions, and the * -* following disclaimer. * -* * -* * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the * -* following disclaimer in the documentation and/or other materials provided with the distribution. * -* * -* * Neither the name of the author nor the names of any contributors may be used to endorse or promote products * -* derived from this software without specific prior written permission. * -* * -* THIS SOFTWARE IS PROVIDED BY THE AUTHORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED * -* TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL * -* THE AUTHORS BE HELD LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES * -* (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR * -* BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT * -* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE * -* POSSIBILITY OF SUCH DAMAGE. * -* * -***********************************************************************************************************************/ - -/** - @file - @author Andrew D. Zonenberg - @brief Declaration of MultimeterDialog - */ -#ifndef MultimeterDialog_h -#define MultimeterDialog_h - -#include "Dialog.h" -#include "Session.h" - -class MultimeterDialog : public Dialog -{ -public: - MultimeterDialog(std::shared_ptr meter, std::shared_ptr state, Session* session); - virtual ~MultimeterDialog(); - - virtual bool DoRender(); - - std::shared_ptr GetMeter() - { return m_meter; } - -protected: - void OnPrimaryModeChanged(); - void RefreshSecondaryModeList(); - - ///@brief Session handle so we can remove the PSU when closed - Session* m_session; - - ///@brief Timestamp of when we opened the dialog - double m_tstart; - - ///@brief The meter we're controlling - std::shared_ptr m_meter; - - ///@brief Current channel stats, live updated - std::shared_ptr m_state; - - ///@brief Set of channel names - std::vector m_channelNames; - - ///@brief The currently selected input channel - int m_selectedChannel; - - ///@brief Names of primary channel operating modes - std::vector m_primaryModeNames; - - ///@brief List of primary channel operating modes - std::vector m_primaryModes; - - ///@brief Index of primary mode - int m_primaryModeSelector; - - ///@brief Names of secondary channel operating modes - std::vector m_secondaryModeNames; - - ///@brief List of secondary channel operating modes - std::vector m_secondaryModes; - - ///@brief Index of secondary mode - int m_secondaryModeSelector; - - ///@brief Autorange enable flag - bool m_autorange; -}; - - - -#endif diff --git a/src/ngscopeclient/MultimeterState.h b/src/ngscopeclient/MultimeterState.h index 2ae95c959..f4787edee 100644 --- a/src/ngscopeclient/MultimeterState.h +++ b/src/ngscopeclient/MultimeterState.h @@ -44,14 +44,27 @@ class MultimeterState MultimeterState() { + m_started = false; + m_selectedChannel = 0; m_primaryMeasurement = 0; m_secondaryMeasurement = 0; m_firstUpdateDone = false; + m_autoRange = true; + m_needsUpdate = true; } + void FlushConfigCache() + { + m_needsUpdate = true; + } + + bool m_started; + int m_selectedChannel; std::atomic m_primaryMeasurement; std::atomic m_secondaryMeasurement; std::atomic m_firstUpdateDone; + std::atomic m_autoRange; + std::atomic m_needsUpdate; }; #endif diff --git a/src/ngscopeclient/OscilloscopeState.h b/src/ngscopeclient/OscilloscopeState.h new file mode 100644 index 000000000..48cdeb06a --- /dev/null +++ b/src/ngscopeclient/OscilloscopeState.h @@ -0,0 +1,155 @@ +/*********************************************************************************************************************** +* * +* ngscopeclient * +* * +* Copyright (c) 2012-2026 Andrew D. Zonenberg and contributors * +* All rights reserved. * +* * +* Redistribution and use in source and binary forms, with or without modification, are permitted provided that the * +* following conditions are met: * +* * +* * Redistributions of source code must retain the above copyright notice, this list of conditions, and the * +* following disclaimer. * +* * +* * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the * +* following disclaimer in the documentation and/or other materials provided with the distribution. * +* * +* * Neither the name of the author nor the names of any contributors may be used to endorse or promote products * +* derived from this software without specific prior written permission. * +* * +* THIS SOFTWARE IS PROVIDED BY THE AUTHORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED * +* TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL * +* THE AUTHORS BE HELD LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES * +* (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR * +* BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT * +* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE * +* POSSIBILITY OF SUCH DAMAGE. * +* * +***********************************************************************************************************************/ + +/** + @file + @author Frederic BORRY + @brief Declaration of OscilloscopeState + */ +#ifndef OscilloscopeState_h +#define OscilloscopeState_h + +/** + @brief Current status of an Oscilloscope + */ +class OscilloscopeState +{ +public: + + OscilloscopeState(std::shared_ptr scope) + { + size_t n = scope->GetChannelCount(); + m_channelNumber = n; + m_channelInverted = std::make_unique(n); + m_channelOffset = std::make_unique[] >(n); + m_channelRange = std::make_unique[] >(n); + + m_channelDigitalTrehshold = std::make_unique[] >(n); + m_channelAttenuation = std::make_unique[] >(n); + + m_needsUpdate = std::make_unique[] >(n); + + m_probeName = std::make_unique(n); + + m_channelCoupling = std::make_unique(n); + m_couplings = std::make_unique[]>(n); + m_couplingNames = std::make_unique[]>(n); + + m_channelBandwidthLimit = std::make_unique(n); + m_bandwidthLimits = std::make_unique[]>(n); + m_bandwidthLimitNames = std::make_unique[]>(n); + + m_committedOffset = std::make_unique[]>(n); + m_strOffset = std::make_unique[]>(n); + + m_committedRange = std::make_unique[]>(n); + m_strRange = std::make_unique[]>(n); + + m_committedDigitalThreshold = std::make_unique(n); + m_strDigitalThreshold = std::make_unique(n); + + m_committedAttenuation = std::make_unique(n); + m_strAttenuation = std::make_unique(n); + + Unit volts(Unit::UNIT_VOLTS); + + for(size_t i=0; i(scope->GetChannel(i)); + if(chan) + { + size_t nstreams = chan->GetStreamCount(); + for(size_t j=0; j m_channelInverted; + std::unique_ptr[]> m_channelOffset; + std::unique_ptr[]> m_channelRange; + std::unique_ptr[]> m_channelDigitalTrehshold; + std::unique_ptr[]> m_channelAttenuation; + + std::unique_ptr[]> m_needsUpdate; + + std::atomic m_channelNumber; + + //UI state for dialogs etc + std::unique_ptr m_probeName; + + std::unique_ptr m_channelBandwidthLimit; + std::unique_ptr[]> m_bandwidthLimits; + std::unique_ptr[]> m_bandwidthLimitNames; + + std::unique_ptr m_channelCoupling; + std::unique_ptr[]> m_couplings; + std::unique_ptr[]> m_couplingNames; + + std::unique_ptr[]> m_committedOffset; + std::unique_ptr[]> m_strOffset; + + std::unique_ptr[]> m_committedRange; + std::unique_ptr[]> m_strRange; + + std::unique_ptr m_committedDigitalThreshold; + std::unique_ptr m_strDigitalThreshold; + + std::unique_ptr m_committedAttenuation; + std::unique_ptr m_strAttenuation; +}; + +#endif diff --git a/src/ngscopeclient/PowerSupplyDialog.cpp b/src/ngscopeclient/PowerSupplyDialog.cpp index 85e6602f1..118d8ea97 100644 --- a/src/ngscopeclient/PowerSupplyDialog.cpp +++ b/src/ngscopeclient/PowerSupplyDialog.cpp @@ -2,7 +2,7 @@ * * * ngscopeclient * * * -* Copyright (c) 2012-2024 Andrew D. Zonenberg and contributors * +* Copyright (c) 2012-2026 Andrew D. Zonenberg and contributors * * All rights reserved. * * * * Redistribution and use in source and binary forms, with or without modification, are permitted provided that the * @@ -49,8 +49,7 @@ PowerSupplyDialog::PowerSupplyDialog( : Dialog( string("Power Supply: ") + psu->m_nickname, string("Power Supply: ") + psu->m_nickname, - ImVec2(500, 400)) - , m_session(session) + ImVec2(500, 400), session) , m_masterEnable(psu->GetMasterPowerEnable()) , m_tstart(GetTime()) , m_psu(psu) @@ -202,7 +201,11 @@ void PowerSupplyDialog::ChannelSettings(int i, float v, float a, float etime) if(m_psu->SupportsIndividualOutputSwitching()) { if(ImGui::Checkbox("Output Enable", &m_channelUIState[i].m_outputEnabled)) + { m_psu->SetPowerChannelActive(i, m_channelUIState[i].m_outputEnabled); + // Tell intrument thread that the PSU state has to be updated + m_state->m_needsUpdate[i] = true; + } if(shdn) { //TODO: preference for configuring this? @@ -230,7 +233,11 @@ void PowerSupplyDialog::ChannelSettings(int i, float v, float a, float etime) if(ocp) { if(ImGui::Checkbox("Overcurrent Shutdown", &m_channelUIState[i].m_overcurrentShutdownEnabled)) + { m_psu->SetPowerOvercurrentShutdownEnabled(i, m_channelUIState[i].m_overcurrentShutdownEnabled); + // Tell intrument thread that the PSU state has to be updated + m_state->m_needsUpdate[i] = true; + } HelpMarker( "When enabled, the channel will shut down on overcurrent rather than switching to constant current mode.\n" "\n" @@ -241,7 +248,11 @@ void PowerSupplyDialog::ChannelSettings(int i, float v, float a, float etime) if(ss) { if(ImGui::Checkbox("Soft Start", &m_channelUIState[i].m_softStartEnabled)) + { m_psu->SetSoftStartEnabled(i, m_channelUIState[i].m_softStartEnabled); + // Tell intrument thread that the PSU state has to be updated + m_state->m_needsUpdate[i] = true; + } HelpMarker( "Deliberately limit the rise time of the output in order to reduce inrush current when driving " @@ -252,6 +263,8 @@ void PowerSupplyDialog::ChannelSettings(int i, float v, float a, float etime) "Ramp time", m_channelUIState[i].m_setSSRamp, m_channelUIState[i].m_committedSSRamp, fs)) { m_psu->SetSoftStartRampTime(i, m_channelUIState[i].m_committedSSRamp); + // Tell intrument thread that the PSU state has to be updated + m_state->m_needsUpdate[i] = true; } HelpMarker( "Transition time between off and on state when using soft start\n\n" @@ -276,6 +289,8 @@ void PowerSupplyDialog::ChannelSettings(int i, float v, float a, float etime) "Voltage", m_channelUIState[i].m_setVoltage, m_channelUIState[i].m_committedSetVoltage, volts)) { m_psu->SetPowerVoltage(i, m_channelUIState[i].m_committedSetVoltage); + // Tell intrument thread that the PSU state has to be updated + m_state->m_needsUpdate[i] = true; } HelpMarker("Target voltage to be supplied to the load.\n\nChanges are not pushed to hardware until you click Apply."); @@ -284,6 +299,8 @@ void PowerSupplyDialog::ChannelSettings(int i, float v, float a, float etime) "Current", m_channelUIState[i].m_setCurrent, m_channelUIState[i].m_committedSetCurrent, amps)) { m_psu->SetPowerCurrent(i, m_channelUIState[i].m_committedSetCurrent); + // Tell intrument thread that the PSU state has to be updated + m_state->m_needsUpdate[i] = true; } HelpMarker("Maximum current to be supplied to the load.\n\nChanges are not pushed to hardware until you click Apply."); diff --git a/src/ngscopeclient/PowerSupplyDialog.h b/src/ngscopeclient/PowerSupplyDialog.h index 3a4629bcf..ea0566572 100644 --- a/src/ngscopeclient/PowerSupplyDialog.h +++ b/src/ngscopeclient/PowerSupplyDialog.h @@ -104,9 +104,6 @@ class PowerSupplyDialog : public Dialog void ChannelSettings(int i, float v, float a, float etime); void AsyncLoadState(); - ///@brief Session handle so we can remove the PSU when closed - Session* m_session; - //@brief Global power enable (if we have one) bool m_masterEnable; diff --git a/src/ngscopeclient/PowerSupplyState.h b/src/ngscopeclient/PowerSupplyState.h index cc2887e4f..8f2be69ec 100644 --- a/src/ngscopeclient/PowerSupplyState.h +++ b/src/ngscopeclient/PowerSupplyState.h @@ -2,7 +2,7 @@ * * * glscopeclient * * * -* Copyright (c) 2012-2022 Andrew D. Zonenberg * +* Copyright (c) 2012-2026 Andrew D. Zonenberg * * All rights reserved. * * * * Redistribution and use in source and binary forms, with or without modification, are permitted provided that the * @@ -45,6 +45,7 @@ class PowerSupplyState PowerSupplyState(size_t n = 0) { m_masterEnable = false; + m_channelNumber = n; m_channelVoltage = std::make_unique[] >(n); m_channelCurrent = std::make_unique[] >(n); @@ -52,6 +53,17 @@ class PowerSupplyState m_channelFuseTripped = std::make_unique[] >(n); m_channelOn = std::make_unique[] >(n); + m_needsUpdate = std::make_unique[] >(n); + + m_overcurrentShutdownEnabled = std::make_unique[] >(n); + m_softStartEnabled = std::make_unique[] >(n); + m_committedSetVoltage = std::make_unique(n); + m_setVoltage = std::make_unique(n); + m_committedSetCurrent = std::make_unique(n); + m_setCurrent = std::make_unique(n); + m_committedSSRamp = std::make_unique(n); + m_setSSRamp = std::make_unique(n); + for(size_t i=0; i[]> m_channelVoltage; std::unique_ptr[]> m_channelCurrent; std::unique_ptr[]> m_channelConstantCurrent; std::unique_ptr[]> m_channelFuseTripped; std::unique_ptr[]> m_channelOn; + std::unique_ptr[]> m_needsUpdate; + //UI state for dialogs etc + std::unique_ptr[]> m_overcurrentShutdownEnabled; + std::unique_ptr[]> m_softStartEnabled; + + std::unique_ptr m_committedSetVoltage; + std::unique_ptr m_setVoltage; + std::unique_ptr m_committedSetCurrent; + std::unique_ptr m_setCurrent; + std::unique_ptr m_committedSSRamp; + std::unique_ptr m_setSSRamp; + + std::atomic m_firstUpdateDone; std::atomic m_masterEnable; + + std::atomic m_channelNumber; }; #endif diff --git a/src/ngscopeclient/PreferenceSchema.cpp b/src/ngscopeclient/PreferenceSchema.cpp index 6ae14ff9c..b28b2223c 100644 --- a/src/ngscopeclient/PreferenceSchema.cpp +++ b/src/ngscopeclient/PreferenceSchema.cpp @@ -2,7 +2,7 @@ * * * ngscopeclient * * * -* Copyright (c) 2012-2025 Andrew D. Zonenberg * +* Copyright (c) 2012-2026 Andrew D. Zonenberg * * All rights reserved. * * * * Redistribution and use in source and binary forms, with or without modification, are permitted provided that the * @@ -165,6 +165,23 @@ void PreferenceManager::InitializeDefaults() .Description("Color for icon captions")); auto& stream = appearance.AddCategory("Stream Browser"); + stream.AddPreference( + Preference::Enum("numeric_value_display", NUMERIC_DISPLAY_MONO_FONT) + .Label("Numeric value display") + .Description( + "Select the way numeric values are displayed for DMM and PSU nodes.\n" + "- Console font: use Console font (monospace) defined in General preferences.\n" + "- 7 segment: use 7 segment style display.\n" + "- Default font: use Default font (proportional) defined in General preferences.\n" + ) + .EnumValue("Console font", NUMERIC_DISPLAY_MONO_FONT) + .EnumValue("7 segment", NUMERIC_DISPLAY_7SEGMENT) + .EnumValue("Default font", NUMERIC_DISPLAY_DEFAULT_FONT) + ); + stream.AddPreference( + Preference::Bool("show_block_border", true) + .Label("Show block border") + .Description("Add a visual border around stream browser blocks (e.g. channel properties)")); stream.AddPreference( Preference::Real("instrument_badge_latch_duration", 0.4) .Label("Intrument badge latch duration (seconds)") @@ -241,6 +258,10 @@ void PreferenceManager::InitializeDefaults() Preference::Color("psu_meas_label_color", ColorFromString("#00C100")) .Label("PSU measured label color") .Description("Color for PSU 'meas.' label")); + stream.AddPreference( + Preference::Color("psu_7_segment_color", ColorFromString("#B2FFFF")) + .Label("PSU 7 segment display color") + .Description("Color for PSU 7 segment style display")); stream.AddPreference( Preference::Color("awg_hiz_badge_color", ColorFromString("#666600")) .Label("Function Generator HI-Z badge color") @@ -270,7 +291,11 @@ void PreferenceManager::InitializeDefaults() general.AddPreference( Preference::Font("console_font", FontDescription(FindDataFile("fonts/DejaVuSansMono.ttf"), 13)) .Label("Console font") - .Description("Font used for SCPI console and log viewer")); + .Description("Font used for SCPI console, log viewer and PSU/DMM numeric values in Stream Browser")); + general.AddPreference( + Preference::Color("apply_button_color", ColorFromString("#4CCC4C")) + .Label("Apply button color") + .Description("Color for the apply value button")); auto& graphs = appearance.AddCategory("Graphs"); graphs.AddPreference( diff --git a/src/ngscopeclient/PreferenceTypes.h b/src/ngscopeclient/PreferenceTypes.h index 2c1612761..60a6cbfd2 100644 --- a/src/ngscopeclient/PreferenceTypes.h +++ b/src/ngscopeclient/PreferenceTypes.h @@ -68,4 +68,11 @@ enum HeadlessStartupMode HEADLESS_STARTUP_C1_ONLY }; +enum NumericValueDisplay +{ + NUMERIC_DISPLAY_MONO_FONT, + NUMERIC_DISPLAY_7SEGMENT, + NUMERIC_DISPLAY_DEFAULT_FONT +}; + #endif diff --git a/src/ngscopeclient/RFGeneratorDialog.cpp b/src/ngscopeclient/RFGeneratorDialog.cpp index 6f44efb98..9e56f8325 100644 --- a/src/ngscopeclient/RFGeneratorDialog.cpp +++ b/src/ngscopeclient/RFGeneratorDialog.cpp @@ -153,8 +153,7 @@ RFGeneratorDialog::RFGeneratorDialog( : Dialog( string("RF Generator: ") + generator->m_nickname, string("RF Generator: ") + generator->m_nickname, - ImVec2(400, 350)) - , m_session(session) + ImVec2(400, 350),session) , m_generator(generator) { Unit hz(Unit::UNIT_HZ); diff --git a/src/ngscopeclient/RFGeneratorDialog.h b/src/ngscopeclient/RFGeneratorDialog.h index 1ece330b0..3f1885389 100644 --- a/src/ngscopeclient/RFGeneratorDialog.h +++ b/src/ngscopeclient/RFGeneratorDialog.h @@ -124,9 +124,6 @@ class RFGeneratorDialog : public Dialog void DoChannel(size_t i); - ///@brief Session handle so we can remove the PSU when closed - Session* m_session; - ///@brief The generator we're controlling std::shared_ptr m_generator; diff --git a/src/ngscopeclient/Session.cpp b/src/ngscopeclient/Session.cpp index 0a931d4ae..63e02746a 100644 --- a/src/ngscopeclient/Session.cpp +++ b/src/ngscopeclient/Session.cpp @@ -2,7 +2,7 @@ * * * ngscopeclient * * * -* Copyright (c) 2012-2025 Andrew D. Zonenberg and contributors * +* Copyright (c) 2012-2026 Andrew D. Zonenberg and contributors * * All rights reserved. * * * * Redistribution and use in source and binary forms, with or without modification, are permitted provided that the * @@ -38,9 +38,7 @@ #include "../scopeprotocols/ExportFilter.h" #include "MainWindow.h" #include "BERTDialog.h" -#include "FunctionGeneratorDialog.h" #include "LoadDialog.h" -#include "MultimeterDialog.h" #include "PowerSupplyDialog.h" #include "RFGeneratorDialog.h" #include "PreferenceTypes.h" @@ -166,6 +164,24 @@ void Session::FlushConfigCache() lock_guard lock(m_scopeMutex); for(auto it : m_instrumentStates) it.first->FlushConfigCache(); + + // Also flush session states + for(auto it : m_psus) + { + it.second->FlushConfigCache(); + } + for(auto it : m_meters) + { + it.second->FlushConfigCache(); + } + for(auto it : m_oscilloscopes) + { + it.second->FlushConfigCache(); + } + for(auto it : m_awgs) + { + it.second->FlushConfigCache(); + } } /** @@ -220,8 +236,13 @@ void Session::Clear() //Delete scopes once we've terminated the threads //Detach waveforms before we destroy the scope, since history owns them //(but make sure they're actually *in* history first!) - m_history.AddHistory(m_oscilloscopes); - for(auto scope : m_oscilloscopes) + std::vector> scopes; + for(auto it : m_oscilloscopes) + { + scopes.push_back(it.first); + } + m_history.AddHistory(scopes); + for(auto scope : scopes) { for(size_t i=0; iGetChannelCount(); i++) { @@ -418,9 +439,9 @@ bool Session::LoadWaveformData(int version, const string& dataDir) } //Load data for each scope - for(size_t i=0; iGetChannelCount(); i++) { auto oc = scope->GetOscilloscopeChannel(i); @@ -2406,9 +2428,9 @@ bool Session::SerializeWaveforms(const string& dataDir) } //Write metadata files (by this point, data directories should have been created) - for(size_t i=0; i inst, bool createDialogs) auto state = make_shared(); m_meters[meter] = state; args.meterstate = state; + if(!(scope && (types & Instrument::INST_OSCILLOSCOPE))) + { // This is a standalone multimeter (not in an Oscilloscope) => start it by default + meter->StartMeter(); + m_meters[meter]->m_started = true; + } } if(load && (types & Instrument::INST_LOAD) ) { @@ -2994,7 +3021,9 @@ void Session::AddInstrument(shared_ptr inst, bool createDialogs) } if(scope && (types & Instrument::INST_OSCILLOSCOPE)) { - m_oscilloscopes.push_back(scope); + auto state = make_shared(scope); + m_oscilloscopes[scope] = state; + args.oscilloscopestate = state; if(m_oscilloscopes.size() > 1) m_multiScope = true; } @@ -3015,15 +3044,6 @@ void Session::AddInstrument(shared_ptr inst, bool createDialogs) { if(psu && (types & Instrument::INST_PSU) ) m_mainWindow->AddDialog(make_shared(psu, args.psustate, this)); - if(meter && (types & Instrument::INST_DMM) ) - m_mainWindow->AddDialog(make_shared(meter, args.meterstate, this)); - if(generator && (types & Instrument::INST_FUNCTION) ) - { - //If it's also a scope, don't show the generator dialog by default - //TODO: only if generator is currently producing a signal or something? - if(!scope) - m_mainWindow->AddDialog(make_shared(generator, args.awgstate, this)); - } if(load && (types & Instrument::INST_LOAD) ) m_mainWindow->AddDialog(make_shared(load, args.loadstate, this)); if(bert && (types & Instrument::INST_BERT) ) @@ -3052,13 +3072,22 @@ void Session::RemoveInstrument(shared_ptr inst) //Remove instrument-specific state auto psu = dynamic_pointer_cast(inst); + auto scope = dynamic_pointer_cast(inst); auto meter = dynamic_pointer_cast(inst); auto load = dynamic_pointer_cast(inst); auto bert = dynamic_pointer_cast(inst); if(psu) m_psus.erase(psu); + if(scope) + m_oscilloscopes.erase(scope); if(meter) + { + auto state = m_meters[meter]; + // Stop meter if needed + if(state && state->m_started) + meter->StopMeter(); m_meters.erase(meter); + } if(load) m_loads.erase(load); if(bert) @@ -3070,16 +3099,6 @@ void Session::RemoveInstrument(shared_ptr inst) m_instrumentStates.erase(inst); } -/** - @brief Adds a multimeter dialog to the session - - Low level helper, intended to be only used by file loading - */ -void Session::AddMultimeterDialog(shared_ptr meter) -{ - m_mainWindow->AddDialog(make_shared(meter, m_meters[meter], this)); -} - /** @brief Returns a list of all connected SCPI instruments, of any type @@ -3096,9 +3115,9 @@ set> Session::GetSCPIInstruments() if(s != nullptr) insts.emplace(s); } - for(auto& scope : m_oscilloscopes) + for(auto& it : m_oscilloscopes) { - auto s = dynamic_pointer_cast(scope); + auto s = dynamic_pointer_cast(it.first); if(s != nullptr) insts.emplace(s); } @@ -3140,8 +3159,8 @@ set> Session::GetInstruments() lock_guard lock(m_scopeMutex); set> insts; - for(auto& scope : m_oscilloscopes) - insts.emplace(scope); + for(auto& it : m_oscilloscopes) + insts.emplace(it.first); for(auto& it : m_psus) insts.emplace(it.first); for(auto& it : m_berts) @@ -3231,9 +3250,9 @@ void Session::StopTrigger(bool all) */ bool Session::HasOnlineScopes() { - for(auto scope : m_oscilloscopes) + for(auto it : m_oscilloscopes) { - if(!scope->IsOffline()) + if(!it.first->IsOffline()) return true; } return false; @@ -3648,9 +3667,9 @@ bool Session::OnMemoryPressure(MemoryPressureLevel level, MemoryPressureType typ if(!moreFreed) { std::lock_guard lock(m_scopeMutex); - for(auto scope : m_oscilloscopes) + for(auto it : m_oscilloscopes) { - if(scope->FreeWaveformPools()) + if(it.first->FreeWaveformPools()) moreFreed = true; } } diff --git a/src/ngscopeclient/Session.h b/src/ngscopeclient/Session.h index da3cffff1..5132cf8cf 100644 --- a/src/ngscopeclient/Session.h +++ b/src/ngscopeclient/Session.h @@ -2,7 +2,7 @@ * * * ngscopeclient * * * -* Copyright (c) 2012-2025 Andrew D. Zonenberg and contributors * +* Copyright (c) 2012-2026 Andrew D. Zonenberg and contributors * * All rights reserved. * * * * Redistribution and use in source and binary forms, with or without modification, are permitted provided that the * @@ -131,7 +131,6 @@ class Session bool SerializeSparseWaveform(SparseWaveformBase* wfm, const std::string& path); bool SerializeUniformWaveform(UniformWaveformBase* wfm, const std::string& path); - void AddMultimeterDialog(std::shared_ptr meter); std::shared_ptr AddPacketFilter(PacketDecoder* filter); void AddInstrument(std::shared_ptr inst, bool createDialogs = true); @@ -144,6 +143,15 @@ class Session MainWindow* GetMainWindow() { return m_mainWindow; } + /** + @brief Returns a pointer to the state for a function generator + */ + std::shared_ptr GetOscilloscopeState(std::shared_ptr scope) + { + std::lock_guard lock(m_scopeMutex); + return m_oscilloscopes[scope]; + } + /** @brief Returns a pointer to the state for a BERT */ @@ -171,6 +179,15 @@ class Session return m_awgs[awg]; } + /** + @brief Returns a pointer to the state for a Digital Multimeter + */ + std::shared_ptr GetDmmState(std::shared_ptr dmm) + { + std::lock_guard lock(m_scopeMutex); + return m_meters[dmm]; + } + /** @brief Returns a pointer to the existing packet manager for a protocol decode filter */ @@ -230,7 +247,12 @@ class Session const std::vector> GetScopes() { std::lock_guard lock(m_scopeMutex); - return m_oscilloscopes; + std::vector> scopes; + for(auto it : m_oscilloscopes) + { + scopes.push_back(it.first); + } + return scopes; } /** @@ -418,7 +440,7 @@ class Session bool m_modifiedSinceLastSave; ///@brief Oscilloscopes we are currently connected to - std::vector> m_oscilloscopes; + std::map, std::shared_ptr > m_oscilloscopes; ///@brief Power supplies we are currently connected to std::map, std::shared_ptr > m_psus; diff --git a/src/ngscopeclient/StreamBrowserDialog.cpp b/src/ngscopeclient/StreamBrowserDialog.cpp index b93ca459d..9e0032807 100644 --- a/src/ngscopeclient/StreamBrowserDialog.cpp +++ b/src/ngscopeclient/StreamBrowserDialog.cpp @@ -39,6 +39,9 @@ using namespace std; +#define ELLIPSIS_CHAR "…" +#define PLUS_CHAR "+" + //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// // StreamBrowserTimebaseInfo @@ -82,7 +85,7 @@ StreamBrowserTimebaseInfo::StreamBrowserTimebaseInfo(shared_ptr sc Unit hz(Unit::UNIT_HZ); m_rbw = scope->GetResolutionBandwidth(); - m_rbwText = hz.PrettyPrint(m_rbw); + m_rbwText = hz.PrettyPrintInt64(m_rbw); m_span = scope->GetSpan(); m_spanText = hz.PrettyPrint(m_span); @@ -116,9 +119,7 @@ StreamBrowserTimebaseInfo::StreamBrowserTimebaseInfo(shared_ptr sc // Construction / destruction StreamBrowserDialog::StreamBrowserDialog(Session& session, MainWindow* parent) - : Dialog("Stream Browser", "Stream Browser", ImVec2(550, 400)) - , m_session(session) - , m_parent(parent) + : Dialog("Stream Browser", "Stream Browser", ImVec2(550, 400), &session, parent) { } @@ -132,20 +133,6 @@ StreamBrowserDialog::~StreamBrowserDialog() //Helper methods for rendering widgets that appear in the StreamBrowserDialog. -/** - @brief Render a link of the "Sample rate: 4 GSa/s" type that shows up in the - scope properties box. -*/ -void StreamBrowserDialog::renderInfoLink(const char *label, const char *linktext, bool &clicked, bool &hovered) -{ - ImGui::PushID(label); // Prevent collision if several sibling links have the same linktext - ImGui::Text("%s: ", label); - ImGui::SameLine(0, 0); - clicked |= ImGui::TextLink(linktext); - hovered |= ImGui::IsItemHovered(); - ImGui::PopID(); -} - /** @brief prepare rendering context to display a badge at the end of current line */ @@ -166,7 +153,7 @@ void StreamBrowserDialog::startBadgeLine() */ void StreamBrowserDialog::renderInstrumentBadge(std::shared_ptr inst, bool latched, InstrumentBadge badge) { - auto& prefs = m_session.GetPreferences(); + auto& prefs = m_session->GetPreferences(); double now = GetTime(); if(latched) { @@ -244,12 +231,14 @@ void StreamBrowserDialog::renderBadge(ImVec4 color, ... /* labels, ending in NUL @brief Render a combo box with provided color and values @param label Label for the combo box + @param alignRight if true @param color the color of the combo box @param selected the selected value index (in/out) @param values the combo box values @param useColorForText if true, use the provided color for text (and a darker version of it for background color) @param cropTextTo if >0 crop the combo text up to this number of characters to have it fit the available space - @param hideArrow True to hide the dropdown arrow + @param hideArrow True to hide the dropdown arrow (defaults to true) + @param paddingRight the padding to leave at the right of the combo when alighRight is true (defaults to 0) @return true if the selected value of the combo has been changed */ @@ -261,7 +250,8 @@ bool StreamBrowserDialog::renderCombo( const vector &values, bool useColorForText, uint8_t cropTextTo, - bool hideArrow) + bool hideArrow, + float paddingRight) { if(selected >= (int)values.size() || selected < 0) { @@ -274,7 +264,7 @@ bool StreamBrowserDialog::renderCombo( if(alignRight) { - int padding = ImGui::GetStyle().ItemSpacing.x + ImGui::GetStyle().FramePadding.x * 2; + int padding = ImGui::GetStyle().ItemSpacing.x + ImGui::GetStyle().FramePadding.x * 2 + paddingRight; float xsz = ImGui::CalcTextSize(selectedLabel).x + padding; string resizedLabel; if ((m_badgeXCur - xsz) < m_badgeXMin) @@ -288,12 +278,12 @@ bool StreamBrowserDialog::renderCombo( resizedLabel = resizedLabel.substr(0,resizedLabel.size()-1); if(resizedLabel.size() < cropTextTo) break; // We don't want to make the text that short - xsz = ImGui::CalcTextSize((resizedLabel + "...").c_str()).x + padding; + xsz = ImGui::CalcTextSize((resizedLabel + ELLIPSIS_CHAR).c_str()).x + padding; } if((m_badgeXCur - xsz) < m_badgeXMin) return false; // Still no room // We found an acceptable size - resizedLabel = resizedLabel + "..."; + resizedLabel = resizedLabel + ELLIPSIS_CHAR; selectedLabel = resizedLabel.c_str(); } m_badgeXCur -= xsz - ImGui::GetStyle().ItemSpacing.x; @@ -304,7 +294,7 @@ bool StreamBrowserDialog::renderCombo( { // Use channel color for shape combo, but darken it to make text readable float bgmul = 0.4; - auto bcolor = ImGui::ColorConvertFloat4ToU32(ImVec4(color.x*bgmul, color.y*bgmul, color.z*bgmul, color.w) ); + auto bcolor = ImGui::ColorConvertFloat4ToU32(ImVec4(color.x*bgmul, color.y*bgmul, color.z*bgmul, color.w)); ImGui::PushStyleColor(ImGuiCol_FrameBg, bcolor); ImGui::PushStyleColor(ImGuiCol_Text, color); } @@ -346,10 +336,12 @@ bool StreamBrowserDialog::renderCombo( /** @brief Render a combo box with provded color and values + @param label Label for the combo box + @param alignRight true if the combo should be aligned to the right @param color the color of the combo box @param selected the selected value index (in/out) @param ... the combo box values - @return true true if the selected value of the combo has been changed + @return true if the selected value of the combo has been changed */ bool StreamBrowserDialog::renderCombo( const char* label, @@ -375,30 +367,23 @@ bool StreamBrowserDialog::renderCombo( /** @brief Render a toggle button combo + @param label Label for the combo box + @param alignRight true if the combo should be aligned to the right @param color the color of the toggle button @param curValue the value of the toggle button - @return the selected value for the toggle button - - TODO: replace with renderToggleEXT - */ -bool StreamBrowserDialog::renderToggle(const char* label, bool alignRight, ImVec4 color, bool curValue) -{ - int selection = (int)curValue; - renderCombo(label, alignRight, color, &selection, "OFF", "ON", nullptr); - return (selection == 1); -} - -/** - @brief Render a toggle button combo - - @param color the color of the toggle button - @param curValue the value of the toggle button + @param valueOff label for value off (optional, defaults to "OFF") + @param valueOn label for value on (optional, defaults to "ON") + @param cropTextTo if >0 crop the combo text up to this number of characters to have it fit the available space (optional, defaults to 0) + @param paddingRight the padding to leave at the right of the combo when alighRight is true (defaults to 0) @return true if selection has changed */ -bool StreamBrowserDialog::renderToggleEXT(const char* label, bool alignRight, ImVec4 color, bool& curValue) +bool StreamBrowserDialog::renderToggle(const char* label, bool alignRight, ImVec4 color, bool& curValue, const char* valueOff, const char* valueOn, uint8_t cropTextTo, float paddingRight) { int selection = (int)curValue; - bool ret = renderCombo(label, alignRight, color, &selection, "OFF", "ON", nullptr); + std::vector values; + values.push_back(string(valueOff)); + values.push_back(string(valueOn)); + bool ret = renderCombo(label, alignRight, color, selection, values, false, cropTextTo, true, paddingRight); curValue = (selection == 1); return ret; } @@ -406,35 +391,23 @@ bool StreamBrowserDialog::renderToggleEXT(const char* label, bool alignRight, Im /** @brief Render an on/off toggle button combo + @param label Label for the combo box + @param alignRight true if the combo should be aligned to the right @param curValue the value of the toggle button - @return the selected value for the toggle button - - TODO: replace with renderOnOffToggleEXT - */ -bool StreamBrowserDialog::renderOnOffToggle(const char* label, bool alignRight, bool curValue) -{ - auto& prefs = m_session.GetPreferences(); - ImVec4 color = ImGui::ColorConvertU32ToFloat4( - (curValue ? - prefs.GetColor("Appearance.Stream Browser.instrument_on_badge_color") : - prefs.GetColor("Appearance.Stream Browser.instrument_off_badge_color"))); - return renderToggle(label, alignRight, color, curValue); -} - -/** - @brief Render an on/off toggle button combo - - @param curValue the value of the toggle button + @param valueOff label for value off (optional, defaults to "OFF") + @param valueOn label for value on (optional, defaults to "ON") + @param cropTextTo if >0 crop the combo text up to this number of characters to have it fit the available space (optional, defaults to 0) + @param paddingRight the padding to leave at the right of the combo when alighRight is true (defaults to 0) @return true if value has changed */ -bool StreamBrowserDialog::renderOnOffToggleEXT(const char* label, bool alignRight, bool& curValue) +bool StreamBrowserDialog::renderOnOffToggle(const char* label, bool alignRight, bool& curValue, const char* valueOff, const char* valueOn, uint8_t cropTextTo, float paddingRight) { - auto& prefs = m_session.GetPreferences(); + auto& prefs = m_session->GetPreferences(); ImVec4 color = ImGui::ColorConvertU32ToFloat4( (curValue ? prefs.GetColor("Appearance.Stream Browser.instrument_on_badge_color") : prefs.GetColor("Appearance.Stream Browser.instrument_off_badge_color"))); - return renderToggleEXT(label, alignRight, color, curValue); + return renderToggle(label, alignRight, color, curValue, valueOff, valueOn, cropTextTo,paddingRight); } /** @@ -475,7 +448,7 @@ void StreamBrowserDialog::renderDownloadProgress(std::shared_ptr ins bool shouldRender = true; bool hasProgress = false; double elapsed = GetTime() - chan->GetDownloadStartTime(); - auto& prefs = m_session.GetPreferences(); + auto& prefs = m_session->GetPreferences(); // determine what label we should apply, and while we are at @@ -588,20 +561,25 @@ void StreamBrowserDialog::renderDownloadProgress(std::shared_ptr ins @param cc true if the PSU channel is in constant current mode, false for constant voltage mode @param chan the PSU channel to render properties for @param setValue the set value text + @param committedValue the last commited value @param measuredValue the measured value text @param clicked output param for clicked state @param hovered output param for hovered state + + @return true if the value has been modified */ -void StreamBrowserDialog::renderPsuRows( +bool StreamBrowserDialog::renderPsuRows( bool isVoltage, bool cc, PowerSupplyChannel* chan, - const char *setValue, - const char *measuredValue, + std::string& currentValue, + float& committedValue, + std::string& measuredValue, bool &clicked, bool &hovered) { - auto& prefs = m_session.GetPreferences(); + bool changed = false; + auto& prefs = m_session->GetPreferences(); // Row 1 ImGui::TableNextRow(); ImGui::TableSetColumnIndex(0); @@ -624,8 +602,16 @@ void StreamBrowserDialog::renderPsuRows( ImGui::PopID(); ImGui::TableSetColumnIndex(2); ImGui::PushID(isVoltage ? "sV" : "sC"); - clicked |= ImGui::TextLink(setValue); - hovered |= ImGui::IsItemHovered(); + + ImVec4 color = ImGui::ColorConvertU32ToFloat4(prefs.GetColor("Appearance.Stream Browser.psu_7_segment_color")); + + Unit unit(isVoltage ? Unit::UNIT_VOLTS : Unit::UNIT_AMPS); + + if(renderEditablePropertyWithExplicitApply(0,"##psuSetValue",currentValue,committedValue,unit,nullptr,color,true)) + { + changed = true; + } + ImGui::PopID(); // Row 2 ImGui::TableNextRow(); @@ -654,8 +640,117 @@ void StreamBrowserDialog::renderPsuRows( ImGui::PopID(); ImGui::TableSetColumnIndex(2); ImGui::PushID(isVoltage ? "mV" : "mC"); - clicked |= ImGui::TextLink(measuredValue); - hovered |= ImGui::IsItemHovered(); + + renderNumericValue(measuredValue,clicked,hovered,color,true,0,false); + + ImGui::PopID(); + return changed; +} + +/** + @brief Render DMM channel properties + @param dmm the DMM to render channel properties for + @param dmmchan the DMM channel to render properties for + @param clicked output param for clicked state + @param hovered output param for hovered state + */ +void StreamBrowserDialog::renderDmmProperties(std::shared_ptr dmm, MultimeterChannel* dmmchan, bool isMain, bool &clicked, bool &hovered) +{ + size_t streamIndex = isMain ? 0 : 1; + Unit unit = dmmchan->GetYAxisUnits(streamIndex); + float mainValue = dmmchan->GetScalarValue(streamIndex); + string valueText = unit.PrettyPrint(mainValue,dmm->GetMeterDigits()); + ImVec4 color = ImGui::ColorConvertU32ToFloat4(ColorFromString(dmmchan->m_displaycolor)); + string streamName = isMain ? "Main" : "Secondary"; + + ImGui::PushID(streamName.c_str()); + + // Get available operating and current modes + auto modemask = isMain ? dmm->GetMeasurementTypes() : dmm->GetSecondaryMeasurementTypes(); + auto curMode = isMain ? dmm->GetMeterMode() : dmm->GetSecondaryMeterMode(); + + // Stream name + bool open = ImGui::TreeNodeEx(streamName.c_str(), (curMode > 0) ? ImGuiTreeNodeFlags_DefaultOpen : 0); + + // Mode combo + startBadgeLine(); + ImGui::PushID(streamName.c_str()); + vector modeNames; + vector modes; + if(!isMain) + { + // Add None for secondary measurement to be able to disable it + modeNames.push_back("None"); + modes.push_back(Multimeter::MeasurementTypes::NONE); + } + int modeSelector = 0; + for(unsigned int i=0; i<32; i++) + { + auto mode = static_cast(1 << i); + if(modemask & mode) + { + modes.push_back(mode); + modeNames.push_back(dmm->ModeToText(mode)); + if(curMode == mode) + modeSelector = modes.size() - 1; + } + } + + if(renderCombo("##mode", true, color, modeSelector, modeNames,true,3,true)) + { + curMode = modes[modeSelector]; + if(isMain) + dmm->SetMeterMode(curMode); + else + { + dmm->SetSecondaryMeterMode(curMode); + // Open or close tree node according the secondary measure mode + ImGuiContext& g = *GImGui; + ImGui::TreeNodeSetOpen(g.LastItemData.ID,(curMode > 0)); + } + } + ImGui::PopID(); + + StreamDescriptor s(dmmchan, streamIndex); + if(ImGui::BeginDragDropSource()) + { + if(s.GetType() == Stream::STREAM_TYPE_ANALOG_SCALAR) + ImGui::SetDragDropPayload("Scalar", &s, sizeof(s)); + else + ImGui::SetDragDropPayload("Stream", &s, sizeof(s)); + + ImGui::TextUnformatted(s.GetName().c_str()); + ImGui::EndDragDropSource(); + } + else + DoItemHelp(); + + if(open) + ImGui::TreePop(); + + if(open) + { + renderNumericValue(valueText,clicked,hovered,color,true,ImGui::GetFontSize()*2,false); + + if(isMain) + { + auto dmmState = m_session->GetDmmState(dmm); + if(dmmState) + { + ImGui::PushID("autorange"); + // For main, also show the autorange combo + startBadgeLine(); + bool autorange = dmmState->m_autoRange.load(); + if(renderOnOffToggle("##autorange",true,autorange,"Manual Range","Autorange",3)) + { + dmm->SetMeterAutoRange(autorange); + dmmState->m_needsUpdate = true; + } + ImGui::PopID(); + } + } + } + ImGui::PopID(); } @@ -669,9 +764,11 @@ void StreamBrowserDialog::renderAwgProperties(std::shared_ptr { Unit volts(Unit::UNIT_VOLTS); Unit hz(Unit::UNIT_HZ); + Unit percent(Unit::UNIT_PERCENT); + Unit fs(Unit::UNIT_FS); size_t channelIndex = awgchan->GetIndex(); - auto awgState = m_session.GetFunctionGeneratorState(awg); + auto awgState = m_session->GetFunctionGeneratorState(awg); if(!awgState) return; @@ -696,24 +793,26 @@ void StreamBrowserDialog::renderAwgProperties(std::shared_ptr awgState->m_committedFrequency[channelIndex] = freq; awgState->m_strFrequency[channelIndex] = hz.PrettyPrint(freq); } - - auto& prefs = m_session.GetPreferences(); - - //Impedance - ImGui::SetNextItemWidth(dwidth); - /* - if(renderCombo( - "Sample Rate", - false, - ImGui::GetStyleColorVec4(ImGuiCol_FrameBg), - m_timebaseConfig[scope]->m_rate, m_timebaseConfig[scope]->m_rateNames)) + float dutyCycle = awgState->m_channelDutyCycle[channelIndex]; + if(dutyCycle != awgState->m_committedDutyCycle[channelIndex]) { - scope->SetSampleRate(m_timebaseConfig[scope]->m_rates[m_timebaseConfig[scope]->m_rate]); - refresh = true; + awgState->m_committedDutyCycle[channelIndex] = dutyCycle; + awgState->m_strDutyCycle[channelIndex] = percent.PrettyPrint(dutyCycle); + } + float riseTime = awgState->m_channelRiseTime[channelIndex]; + if(riseTime != awgState->m_committedRiseTime[channelIndex]) + { + awgState->m_committedRiseTime[channelIndex] = riseTime; + awgState->m_strRiseTime[channelIndex] = fs.PrettyPrint(riseTime); + } + float fallTime = awgState->m_channelFallTime[channelIndex]; + if(fallTime != awgState->m_committedFallTime[channelIndex]) + { + awgState->m_committedFallTime[channelIndex] = fallTime; + awgState->m_strFallTime[channelIndex] = fs.PrettyPrint(fallTime); } - */ - //shape = awgState->m_channelShape[channelIndex]; + auto& prefs = m_session->GetPreferences(); // Row 1 ImGui::Text("Waveform:"); @@ -723,12 +822,12 @@ void StreamBrowserDialog::renderAwgProperties(std::shared_ptr FunctionGenerator::WaveShape shape = awgState->m_channelShape[channelIndex]; int shapeIndex = awgState->m_channelShapeIndexes[channelIndex][shape]; if(renderCombo( - "#waveform", + "##waveform", true, ImGui::ColorConvertU32ToFloat4(ColorFromString(awgchan->m_displaycolor)), shapeIndex, awgState->m_channelShapeNames[channelIndex], true, - 3)) + 3,true)) { shape = awgState->m_channelShapes[channelIndex][shapeIndex]; awg->SetFunctionChannelShape(channelIndex, shape); @@ -738,14 +837,16 @@ void StreamBrowserDialog::renderAwgProperties(std::shared_ptr awgState->m_needsUpdate[channelIndex] = true; } + // Store current Y position for shape preview + float shapePreviewY = ImGui::GetCursorPosY(); + // Row 2 // Frequency label - ImGui::SetNextItemWidth(dwidth); - if(UnitInputWithImplicitApply( + if(renderEditableProperty(dwidth, "Frequency", awgState->m_strFrequency[channelIndex], awgState->m_committedFrequency[channelIndex], - hz)) + hz/*,"Frequency of the generated waveform"*/)) { awg->SetFunctionChannelFrequency(channelIndex, awgState->m_committedFrequency[channelIndex]); awgState->m_needsUpdate[channelIndex] = true; @@ -759,11 +860,21 @@ void StreamBrowserDialog::renderAwgProperties(std::shared_ptr ImGui::EndDragDropSource(); } else - */ DoItemHelp(); - HelpMarker("Frequency of the generated waveform"); + */ + + //Row 2 + //Duty cycle + if(renderEditableProperty(dwidth, + "Duty cycle", + awgState->m_strDutyCycle[channelIndex], + awgState->m_committedDutyCycle[channelIndex], + percent/*,"Duty cycle of the waveform, in percent. Not applicable to all waveform types."*/)) + { + awg->SetFunctionChannelDutyCycle(channelIndex, awgState->m_committedDutyCycle[channelIndex]); + awgState->m_needsUpdate[channelIndex] = true; + } - /* // Shape preview startBadgeLine(); auto height = ImGui::GetFontSize() * 2; @@ -772,45 +883,68 @@ void StreamBrowserDialog::renderAwgProperties(std::shared_ptr { // ok, we have enough space draw preview m_badgeXCur -= width; + // save current y position to restore it after drawing the preview + float currentY = ImGui::GetCursorPosY(); + // Continue layout on current line (row 3) ImGui::SameLine(m_badgeXCur); + // But use y position of row 2 + ImGui::SetCursorPosY(shapePreviewY); ImGui::Image( m_parent->GetTextureManager()->GetTexture(m_parent->GetIconForWaveformShape(shape)), ImVec2(width,height)); - // Go back one line since preview spans on two text lines - ImGuiWindow *window = ImGui::GetCurrentWindowRead(); - window->DC.CursorPos.y -= ImGui::GetFontSize(); + // Now that we're done with shape preview, restore y position of row 3 + ImGui::SetCursorPosY(currentY); + } + + if(awg->HasFunctionRiseFallTimeControls(channelIndex)) + { //Row 3 + //Fall Time + if(renderEditableProperty(dwidth, + "Rise Time", + awgState->m_strRiseTime[channelIndex], + awgState->m_committedRiseTime[channelIndex], + fs)) + { + awg->SetFunctionChannelRiseTime(channelIndex, awgState->m_committedRiseTime[channelIndex]); + awgState->m_needsUpdate[channelIndex] = true; + } + //Row 4 + //Fall Time + if(renderEditableProperty(dwidth, + "Fall Time", + awgState->m_strFallTime[channelIndex], + awgState->m_committedFallTime[channelIndex], + fs)) + { + awg->SetFunctionChannelFallTime(channelIndex, awgState->m_committedFallTime[channelIndex]); + awgState->m_needsUpdate[channelIndex] = true; + } } - */ - // Row 3 - ImGui::SetNextItemWidth(dwidth); - if(UnitInputWithExplicitApply( + // Row 5 + if(renderEditablePropertyWithExplicitApply(dwidth, "Amplitude", awgState->m_strAmplitude[channelIndex], awgState->m_committedAmplitude[channelIndex], - volts)) + volts,"Peak-to-peak amplitude of the generated waveform")) { awg->SetFunctionChannelAmplitude(channelIndex, awgState->m_committedAmplitude[channelIndex]); awgState->m_needsUpdate[channelIndex] = true; } - HelpMarker("Peak-to-peak amplitude of the generated waveform"); - //Row 4 + //Row 6 //Offset - ImGui::SetNextItemWidth(dwidth); - if(UnitInputWithExplicitApply( + if(renderEditablePropertyWithExplicitApply(dwidth, "Offset", awgState->m_strOffset[channelIndex], awgState->m_committedOffset[channelIndex], - volts)) + volts,"DC offset for the waveform above (positive) or below (negative) ground")) { awg->SetFunctionChannelOffset(channelIndex, awgState->m_committedOffset[channelIndex]); awgState->m_needsUpdate[channelIndex] = true; } - HelpMarker("DC offset for the waveform above (positive) or below (negative) ground"); - - //TODO: Duty cycle + //Row 7 //Impedance ImGui::SetNextItemWidth(dwidth); FunctionGenerator::OutputImpedance impedance = awgState->m_channelOutputImpedance[channelIndex]; @@ -826,6 +960,10 @@ void StreamBrowserDialog::renderAwgProperties(std::shared_ptr "Hi-Z", "50 Ω", nullptr); + HelpMarker( + "Select the expected load impedance.\n\n" + "If set incorrectly, amplitude and offset will be inaccurate due to reflections."); + if(changed) { @@ -849,13 +987,13 @@ void StreamBrowserDialog::renderAwgProperties(std::shared_ptr void StreamBrowserDialog::renderInstrumentNode(shared_ptr instrument) { // Get preferences for colors - auto& prefs = m_session.GetPreferences(); + auto& prefs = m_session->GetPreferences(); ImGui::PushID(instrument.get()); bool instIsOpen = ImGui::TreeNodeEx(instrument->m_nickname.c_str(), ImGuiTreeNodeFlags_DefaultOpen); startBadgeLine(); - auto state = m_session.GetInstrumentConnectionState(instrument); + auto state = m_session->GetInstrumentConnectionState(instrument); size_t channelCount = instrument->GetChannelCount(); @@ -898,7 +1036,7 @@ void StreamBrowserDialog::renderInstrumentNode(shared_ptr instrument if (psu) { //Get the state - auto psustate = m_session.GetPSUState(psu); + auto psustate = m_session->GetPSUState(psu); bool allOn = false; bool someOn = false; @@ -918,16 +1056,18 @@ void StreamBrowserDialog::renderInstrumentNode(shared_ptr instrument bool result; if(allOn || someOn) { - result = renderToggle( + result = true; + renderToggle( "###psuon", true, allOn ? ImGui::ColorConvertU32ToFloat4(prefs.GetColor("Appearance.Stream Browser.instrument_on_badge_color")) : - ImGui::ColorConvertU32ToFloat4(prefs.GetColor("Appearance.Stream Browser.instrument_partial_badge_color")), true); + ImGui::ColorConvertU32ToFloat4(prefs.GetColor("Appearance.Stream Browser.instrument_partial_badge_color")), result); } else { - result = renderOnOffToggle("###psuon", true, false); + result = false; + renderOnOffToggle("###psuon", true, result); } if(result != allOn) { @@ -945,11 +1085,15 @@ void StreamBrowserDialog::renderInstrumentNode(shared_ptr instrument if(instIsOpen) { + vector digitalBanks; + vector analogChannels; + vector otherChannels; size_t lastEnabledChannelIndex = 0; if (scope) { if(ImGui::TreeNodeEx("Timebase", ImGuiTreeNodeFlags_DefaultOpen)) { + BeginBlock("timebase"); if(scope->HasTimebaseControls()) DoTimebaseSettings(scope); if(scope->HasFrequencyControls()) @@ -957,20 +1101,62 @@ void StreamBrowserDialog::renderInstrumentNode(shared_ptr instrument auto spec = dynamic_pointer_cast(scope); if(spec) DoSpectrometerSettings(spec); + EndBlock(); ImGui::TreePop(); } + digitalBanks = scope->GetDigitalBanks(); + for(size_t i = 0; iIsChannelEnabled(i)) lastEnabledChannelIndex = i; + auto scopechan = scope->GetChannel(i); + auto streamType = scopechan->GetType(0); + if(streamType != Stream::STREAM_TYPE_DIGITAL) + { + if(streamType == Stream::STREAM_TYPE_ANALOG) + analogChannels.push_back(i); + else + otherChannels.push_back(i); + } } } - for(size_t i=0; i 0) + { // If digital banks are avaialble, gather digital channels in banks + for(size_t i : analogChannels) + { // Iterate on analog channel first + renderChannelNode(instrument,i,(i == lastEnabledChannelIndex)); + } + int bankNumber = 1; + for(auto bank : digitalBanks) + { // Iterate on digital banks + string nodeName = "Digital Bank " + to_string(bankNumber); + if(ImGui::TreeNodeEx(nodeName.c_str())) + { + ImGui::Unindent(ImGui::GetTreeNodeToLabelSpacing()); + for(auto channel : bank) + { // Iterate on bank's channel + size_t i = channel->GetIndex(); + renderChannelNode(instrument,i,(i == lastEnabledChannelIndex)); + } + ImGui::Indent(ImGui::GetTreeNodeToLabelSpacing()); + ImGui::TreePop(); + } + bankNumber++; + } + for(size_t i : otherChannels) + { // Finally iterate on other channels + renderChannelNode(instrument,i,(i == lastEnabledChannelIndex)); + } + } + else + { // Display all channels if no digital bank is available + for(size_t i=0; i scope) Unit hz(Unit::UNIT_HZ); // Resolution Bandwidh - ImGui::SetNextItemWidth(width); - if(UnitInputWithImplicitApply("Rbw", p->m_rbwText, p->m_rbw, hz)) + if(renderEditableProperty(width,"Rbw", p->m_rbwText, p->m_rbw, hz, "Resolution Bandwidth")) { scope->SetResolutionBandwidth(p->m_rbw); // Update with values from the device p->m_rbw = scope->GetResolutionBandwidth(); - p->m_rbwText = hz.PrettyPrint(p->m_rbw); + p->m_rbwText = hz.PrettyPrintInt64(p->m_rbw); } - HelpMarker("Resolution Bandwidth"); //Frequency bool changed = false; - ImGui::SetNextItemWidth(width); - if(UnitInputWithImplicitApply("Start", p->m_startText, p->m_start, hz)) + if(renderEditableProperty(width,"Start", p->m_startText, p->m_start, hz, "Start of the frequency sweep")) { double mid = (p->m_start + p->m_end) / 2; double span = (p->m_end - p->m_start); @@ -1038,26 +1221,20 @@ void StreamBrowserDialog::DoFrequencySettings(shared_ptr scope) scope->SetSpan(span); changed = true; } - HelpMarker("Start of the frequency sweep"); - ImGui::SetNextItemWidth(width); - if(UnitInputWithImplicitApply("Center", p->m_centerText, p->m_center, hz)) + if(renderEditableProperty(width,"Center", p->m_centerText, p->m_center, hz, "Midpoint of the frequency sweep")) { scope->SetCenterFrequency(0, p->m_center); changed = true; } - HelpMarker("Midpoint of the frequency sweep"); - ImGui::SetNextItemWidth(width); - if(UnitInputWithImplicitApply("Span", p->m_spanText, p->m_span, hz)) + if(renderEditableProperty(width,"Span", p->m_spanText, p->m_span, hz, "Width of the frequency sweep")) { scope->SetSpan(p->m_span); changed = true; } - HelpMarker("Width of the frequency sweep"); - ImGui::SetNextItemWidth(width); - if(UnitInputWithImplicitApply("End", p->m_endText, p->m_end, hz)) + if(renderEditableProperty(width,"End", p->m_endText, p->m_end, hz, "End of the frequency sweep")) { double mid = (p->m_start + p->m_end) / 2; double span = (p->m_end - p->m_start); @@ -1065,7 +1242,6 @@ void StreamBrowserDialog::DoFrequencySettings(shared_ptr scope) scope->SetSpan(span); changed = true; } - HelpMarker("End of the frequency sweep"); //Update everything if one setting is changed if(changed) @@ -1089,12 +1265,10 @@ void StreamBrowserDialog::DoSpectrometerSettings(shared_ptr sp auto config = m_timebaseConfig[scope]; auto width = ImGui::GetFontSize() * 5; - ImGui::SetNextItemWidth(width); Unit fs(Unit::UNIT_FS); - if(UnitInputWithImplicitApply("Integration time", config->m_integrationText, config->m_integrationTime, fs)) + if(renderEditableProperty(width, "Integration time", config->m_integrationText, config->m_integrationTime, fs, "Spectrometer integration / exposure time")) spec->SetIntegrationTime(config->m_integrationTime); - HelpMarker("Spectrometer integration / exposure time"); } /** @@ -1114,7 +1288,7 @@ void StreamBrowserDialog::DoTimebaseSettings(shared_ptr scope) ImGui::SetNextItemWidth(width); bool disabled = !scope->CanInterleave(); ImGui::BeginDisabled(disabled); - if(renderOnOffToggleEXT("Interleaving", false, config->m_interleaving)) + if(renderOnOffToggle("Interleaving", false, config->m_interleaving)) { scope->SetInterleaving(config->m_interleaving); refresh = true; @@ -1244,7 +1418,7 @@ void StreamBrowserDialog::DoTimebaseSettings(shared_ptr scope) void StreamBrowserDialog::renderChannelNode(shared_ptr instrument, size_t channelIndex, bool isLast) { // Get preferences for colors - auto& prefs = m_session.GetPreferences(); + auto& prefs = m_session->GetPreferences(); InstrumentChannel* channel = instrument->GetChannel(channelIndex); @@ -1253,11 +1427,13 @@ void StreamBrowserDialog::renderChannelNode(shared_ptr instrument, s auto psu = std::dynamic_pointer_cast(instrument); auto scope = std::dynamic_pointer_cast(instrument); auto awg = std::dynamic_pointer_cast(instrument); + auto dmm = std::dynamic_pointer_cast(instrument); bool singleStream = channel->GetStreamCount() == 1; auto scopechan = dynamic_cast(channel); auto psuchan = dynamic_cast(channel); auto awgchan = dynamic_cast(channel); + auto dmmchan = dynamic_cast(channel); bool renderProps = false; if (scopechan) { @@ -1265,7 +1441,11 @@ void StreamBrowserDialog::renderChannelNode(shared_ptr instrument, s } else if(awg && awgchan) { - renderProps = m_session.GetFunctionGeneratorState(awg)->m_channelActive[channelIndex]; + renderProps = m_session->GetFunctionGeneratorState(awg)->m_channelActive[channelIndex]; + } + else if(dmm && dmmchan) + { + renderProps = m_session->GetDmmState(dmm)->m_started; } bool hasChildren = !singleStream || renderProps; @@ -1276,8 +1456,8 @@ void StreamBrowserDialog::renderChannelNode(shared_ptr instrument, s int flags = 0; if(!hasChildren) flags |= ImGuiTreeNodeFlags_Leaf; - else - flags |= ImGuiTreeNodeFlags_OpenOnArrow; + + flags |= ImGuiTreeNodeFlags_OpenOnArrow; bool open = ImGui::TreeNodeEx( channel->GetDisplayName().c_str(), @@ -1294,8 +1474,6 @@ void StreamBrowserDialog::renderChannelNode(shared_ptr instrument, s m_parent->ShowChannelProperties(scopechan); else if(psuchan) m_parent->ShowInstrumentProperties(psu); - else if(awgchan) - m_parent->ShowInstrumentProperties(awg); else LogWarning("Don't know how to open channel properties yet\n"); } @@ -1350,32 +1528,38 @@ void StreamBrowserDialog::renderChannelNode(shared_ptr instrument, s { // PSU Channel //Get the state - auto psustate = m_session.GetPSUState(psu); + auto psustate = m_session->GetPSUState(psu); bool active = psustate->m_channelOn[channelIndex]; - bool result = renderOnOffToggle("###active", true, active); + bool result = active; + renderOnOffToggle("###active", true, result); if(result != active) psu->SetPowerChannelActive(channelIndex,result); } else if(awg && awgchan) { // AWG Channel : get the state - auto awgstate = m_session.GetFunctionGeneratorState(awg); - - bool active = awgstate->m_channelActive[channelIndex]; - bool result = renderOnOffToggle("###active", true, active); - if(result != active) + auto awgstate = m_session->GetFunctionGeneratorState(awg); + if(renderOnOffToggle("###active", true,awgstate->m_channelActive[channelIndex])) { - awg->SetFunctionChannelActive(channelIndex,result); - auto awgState = m_session.GetFunctionGeneratorState(awg); - if(awgState) - { - // Update state right now to cover from slow intruments - awgState->m_channelActive[channelIndex]=result; - // Tell intrument thread that the FunctionGenerator state has to be updated - awgState->m_needsUpdate[channelIndex] = true; - } + awg->SetFunctionChannelActive(channelIndex,awgstate->m_channelActive[channelIndex]); + // Tell intrument thread that the FunctionGenerator state has to be updated + awgstate->m_needsUpdate[channelIndex] = true; + } + } + else if(dmm && dmmchan) + { + // DMM Channel : get the state + auto dmmstate = m_session->GetDmmState(dmm); + if(renderOnOffToggle("###active", true, dmmstate->m_started)) + { + if(dmmstate->m_started) + dmm->StartMeter(); + else + dmm->StopMeter(); + // Tell intrument thread that the Dmm state has to be updated + dmmstate->m_needsUpdate = true; } } @@ -1385,46 +1569,106 @@ void StreamBrowserDialog::renderChannelNode(shared_ptr instrument, s if(psu) { // For PSU we will have a special handling for the 4 streams associated to a PSU channel - ImGui::BeginChild("psu_params", ImVec2(0, 0), - ImGuiChildFlags_AutoResizeY | ImGuiChildFlags_Borders); + if(BeginBlock("psu_params",true,"Open PSU channel properties")) + { + m_parent->ShowInstrumentProperties(psu); + } + auto svoltage_txt = Unit(Unit::UNIT_VOLTS).PrettyPrint(psuchan->GetVoltageSetPoint ()); auto mvoltage_txt = Unit(Unit::UNIT_VOLTS).PrettyPrint(psuchan->GetVoltageMeasured()); auto scurrent_txt = Unit(Unit::UNIT_AMPS).PrettyPrint(psuchan->GetCurrentSetPoint ()); auto mcurrent_txt = Unit(Unit::UNIT_AMPS).PrettyPrint(psuchan->GetCurrentMeasured ()); bool cc = false; - auto psuState = m_session.GetPSUState(psu); + auto psuState = m_session->GetPSUState(psu); if(psuState) + { cc = psuState->m_channelConstantCurrent[channelIndex].load(); - bool clicked = false; - bool hovered = false; + bool clicked = false; + bool hovered = false; - if (ImGui::BeginTable("table1", 3)) - { - // Voltage - renderPsuRows(true,cc,psuchan,svoltage_txt.c_str(),mvoltage_txt.c_str(),clicked,hovered); - // Current - renderPsuRows(false,cc,psuchan,scurrent_txt.c_str(),mcurrent_txt.c_str(),clicked,hovered); - // End table - ImGui::EndTable(); - if (clicked) + if (ImGui::BeginTable("table1", 3)) { - m_parent->ShowInstrumentProperties(psu); + // Voltage + if(renderPsuRows(true,cc,psuchan,psuState->m_setVoltage[channelIndex],psuState->m_committedSetVoltage[channelIndex],mvoltage_txt,clicked,hovered)) + { // Update set voltage + psu->SetPowerVoltage(channelIndex, psuState->m_committedSetVoltage[channelIndex]); + psuState->m_needsUpdate[channelIndex] = true; + } + // Current + if(renderPsuRows(false,cc,psuchan,psuState->m_setCurrent[channelIndex],psuState->m_committedSetCurrent[channelIndex],mcurrent_txt,clicked,hovered)) + { // Update set current + psu->SetPowerCurrent(channelIndex, psuState->m_committedSetCurrent[channelIndex]); + psuState->m_needsUpdate[channelIndex] = true; + } + // End table + ImGui::EndTable(); } - if (hovered) - m_parent->AddStatusHelp("mouse_lmb", "Open channel properties"); } - ImGui::EndChild(); + EndBlock(); } else if(awg && awgchan) { - ImGui::PushID("awgparams"); - renderAwgProperties(awg, awgchan); - ImGui::PopID(); + BeginBlock("awgparams"); + renderAwgProperties(awg, awgchan); + EndBlock(); + } + else if(dmm && dmmchan) + { + BeginBlock("dmm_params"); + // Always 2 streams for dmm channel => render properties on channel node + bool clicked = false; + bool hovered = false; + // Channel selection + size_t channelCount = instrument->GetChannelCount(); + if(channelCount > 1) + { + auto dmmState = m_session->GetDmmState(dmm); + if(dmmState) + { + ImGui::SetNextItemWidth(6*ImGui::GetFontSize()); + vector channelNames; + for(size_t i=0; iGetChannelCount(); i++) + channelNames.push_back(dmm->GetChannel(i)->GetDisplayName()); + if(renderCombo( + "Channel", + false, + ImGui::GetStyleColorVec4(ImGuiCol_FrameBg), + dmmState->m_selectedChannel, + channelNames, + false, + 0, + false)) + { + dmm->SetCurrentMeterChannel(dmmState->m_selectedChannel); + dmmState->m_needsUpdate = true; + } + HelpMarker("Select which input channel is being monitored."); + } + } + // Main measurement + renderDmmProperties(dmm,dmmchan,true,clicked,hovered); + // Secondary measurement + renderDmmProperties(dmm,dmmchan,false,clicked,hovered); + + EndBlock(); } else { + if(!singleStream) + { + auto scopeState = m_session->GetOscilloscopeState(scope); + if(scopeState) + { + if(BeginBlock("stream_params",true,"Open channel properties")) + { + m_parent->ShowChannelProperties(scopechan); + } + renderChannelProperties(scope,scopechan,channelIndex,scopeState); + EndBlock(); + } + } size_t streamCount = channel->GetStreamCount(); for(size_t j=0; j instrument, s ImGui::PopID(); } +/** + @brief Rendering of channel properties + + @param scope the scope + @param scopechan the scope channel + @param channelIndex the index of the channel + @param scopeState the OscilloscopeState + */ +void StreamBrowserDialog::renderChannelProperties(std::shared_ptr scope, OscilloscopeChannel* scopechan, size_t channelIndex, shared_ptr scopeState) +{ + float fontSize = ImGui::GetFontSize(); + float width = 6*fontSize; + + Unit counts(Unit::UNIT_COUNTS); + if(renderEditableProperty(width,"Attenuation",scopeState->m_strAttenuation[channelIndex],scopeState->m_committedAttenuation[channelIndex],counts, + "Attenuation setting for the probe (for example, 10 for a 10:1 probe)")) + { // Update offset + scopechan->SetAttenuation(scopeState->m_committedAttenuation[channelIndex]); + scopeState->m_needsUpdate[channelIndex] = true; + } + //Only show coupling box if the instrument has configurable coupling + if( (scopeState->m_couplings[channelIndex].size() > 1) && (scopeState->m_probeName[channelIndex] == "") ) + { + ImGui::SetNextItemWidth(width); + if(renderCombo( + "Coupling", + false, + ImGui::GetStyleColorVec4(ImGuiCol_FrameBg), + scopeState->m_channelCoupling[channelIndex], + scopeState->m_couplingNames[channelIndex], + false, + 0, + false)) + { + scope->SetChannelCoupling(channelIndex,scopeState->m_couplings[channelIndex][scopeState->m_channelCoupling[channelIndex]]); + scopeState->m_needsUpdate[channelIndex] = true; + } + HelpMarker("Coupling configuration for the input"); + } + //Bandwidth limiters (only show if more than one value available) + if(scopeState->m_bandwidthLimitNames[channelIndex].size() > 1) + { + ImGui::SetNextItemWidth(width); + if(renderCombo( + "Bandwidth", + false, + ImGui::GetStyleColorVec4(ImGuiCol_FrameBg), + scopeState->m_channelBandwidthLimit[channelIndex], + scopeState->m_bandwidthLimitNames[channelIndex], + false, + 0, + false)) + { + scopechan->SetBandwidthLimit(scopeState->m_bandwidthLimits[channelIndex][scopeState->m_channelBandwidthLimit[channelIndex]]); + scopeState->m_needsUpdate[channelIndex] = true; + } + HelpMarker("Hardware bandwidth limiter setting"); + } + //If the probe supports inversion, show a checkbox for it + if(scope->CanInvert(channelIndex)) + { + ImGui::SetNextItemWidth(width); + if(renderOnOffToggle("Invert",false,scopeState->m_channelInverted[channelIndex])) + { + scope->Invert(channelIndex,scopeState->m_channelInverted[channelIndex]); + scopeState->m_needsUpdate[channelIndex] = true; + } + HelpMarker( + "When ON, input value is multiplied by -1.\n" + "For a differential probe, this is equivalent to swapping the positive and negative inputs." + ); + } + +} + /** @brief Rendering of a stream node @@ -1484,6 +1803,7 @@ void StreamBrowserDialog::renderStreamNode(shared_ptr instrument, In // Channel/stream properties block if(renderProps && scopechan) { + // If no properties are available for this stream, only show a "Properties" link if it is the last stream of the channel/filter bool hasProps = false; switch (type) { @@ -1501,36 +1821,61 @@ void StreamBrowserDialog::renderStreamNode(shared_ptr instrument, In } if(hasProps) { - ImGui::BeginChild("stream_params", ImVec2(0, 0), - ImGuiChildFlags_AutoResizeY | ImGuiChildFlags_Borders); + auto scopeState = m_session->GetOscilloscopeState(scope); + if(scopeState) + { // For now, only show properties for scope channel / streams + if(BeginBlock("stream_params",true,"Open channel properties")) + { + m_parent->ShowChannelProperties(scopechan); + } - Unit unit = channel->GetYAxisUnits(streamIndex); - bool clicked; - bool hovered; - switch (type) - { - case Stream::STREAM_TYPE_ANALOG: - { - auto offset_txt = unit.PrettyPrint(scopechan->GetOffset(streamIndex)); - auto range_txt = unit.PrettyPrint(scopechan->GetVoltageRange(streamIndex)); - renderInfoLink("Offset", offset_txt.c_str(), clicked, hovered); - renderInfoLink("Vertical range", range_txt.c_str(), clicked, hovered); - } - break; - case Stream::STREAM_TYPE_DIGITAL: - if(scope) - { - auto threshold_txt = unit.PrettyPrint(scope->GetDigitalThreshold(scopechan->GetIndex())); - renderInfoLink("Threshold", threshold_txt.c_str(), clicked, hovered); + Unit unit = channel->GetYAxisUnits(streamIndex); + size_t channelIndex = scopechan->GetIndex(); + + switch (type) + { + case Stream::STREAM_TYPE_ANALOG: + { + if(!renderName) + { // No streams => display channel properties here + renderChannelProperties(scope,scopechan,channelIndex,scopeState); + } + if(renderEditablePropertyWithExplicitApply(0,"Offset",scopeState->m_strOffset[channelIndex][streamIndex],scopeState->m_committedOffset[channelIndex][streamIndex],unit)) + { // Update offset + scopechan->SetOffset(scopeState->m_committedOffset[channelIndex][streamIndex],streamIndex); + scopeState->m_needsUpdate[channelIndex] = true; + } + if(renderEditablePropertyWithExplicitApply(0,"Vertical range",scopeState->m_strRange[channelIndex][streamIndex],scopeState->m_committedRange[channelIndex][streamIndex],unit)) + { // Update offset + scopechan->SetVoltageRange(scopeState->m_committedRange[channelIndex][streamIndex],streamIndex); + scopeState->m_needsUpdate[channelIndex] = true; + } + } break; - } - //fall through - default: - { - } - break; + case Stream::STREAM_TYPE_DIGITAL: + if(scope) + { + if(scope->IsDigitalThresholdConfigurable()) + { + if(renderEditableProperty(0,"Threshold",scopeState->m_strDigitalThreshold[channelIndex],scopeState->m_committedDigitalThreshold[channelIndex],unit)) + { // Update offset + scopechan->SetDigitalThreshold(scopeState->m_committedDigitalThreshold[channelIndex]); + scopeState->m_needsUpdate[channelIndex] = true; + } + } + else + { + auto threshold_txt = unit.PrettyPrint(scope->GetDigitalThreshold(scopechan->GetIndex())); + renderReadOnlyProperty(0,"Threshold", threshold_txt); + } + break; + } + //fall through + default: + break; + } + EndBlock(); } - ImGui::EndChild(); } } ImGui::PopID(); @@ -1626,7 +1971,7 @@ void StreamBrowserDialog::FlushConfigCache() bool StreamBrowserDialog::DoRender() { //Add all instruments - auto insts = m_session.GetInstruments(); + auto insts = m_session->GetInstruments(); for(auto inst : insts) { renderInstrumentNode(inst); @@ -1651,3 +1996,59 @@ void StreamBrowserDialog::DoItemHelp() if(ImGui::IsItemHovered()) m_parent->AddStatusHelp("mouse_lmb_drag", "Add to filter graph or plot"); } + +bool StreamBrowserDialog::BeginBlock(const char* label, bool withButton, const char* tooltip) +{ + bool clicked = false; + auto& prefs = m_session->GetPreferences(); + ImGuiWindowFlags flags = ImGuiChildFlags_AutoResizeY; + bool withBorders = false; + if(prefs.GetBool("Appearance.Stream Browser.show_block_border")) + { + ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2(6, 6)); + flags |= ImGuiChildFlags_Borders; + withBorders = true; + } + ImGui::BeginChild(label, ImVec2(0, 0), flags); + if(withButton) + { // Create a "+" button on the top right corner of the box + ImVec2 oldPos = ImGui::GetCursorPos(); + float padding = ImGui::GetStyle().FramePadding.x; + float shift = withBorders ? padding*1.5 : 0; + float xsz = ImGui::GetFontSize(); + ImGui::SetCursorPosX(ImGui::GetWindowContentRegionMax().x - xsz + shift); + ImGui::SetCursorPosY(ImGui::GetCursorPosY()-shift); + // Use the same color as border for the button + ImVec4 border = ImGui::GetStyle().Colors[ImGuiCol_Border]; + ImVec4 hover = ImVec4(border.x * 1.2f, border.y * 1.2f, border.z * 1.2f, border.w); + ImVec4 active = ImVec4(border.x * 0.9f, border.y * 0.9f, border.z * 0.9f, border.w); + ImGui::PushStyleColor(ImGuiCol_Button, border); + ImGui::PushStyleColor(ImGuiCol_ButtonHovered, hover); + ImGui::PushStyleColor(ImGuiCol_ButtonActive, active); + ImGui::PushStyleVar(ImGuiStyleVar_FramePadding, ImVec2(0, 0)); + ImGui::PushStyleVar(ImGuiStyleVar_ButtonTextAlign, ImVec2(0.6f, 0.5f)); + clicked = ImGui::Button(PLUS_CHAR,ImVec2(xsz, xsz)); + if(ImGui::IsItemHovered()) + { // Hand cursor + ImGui::SetMouseCursor(ImGuiMouseCursor_Hand); + if(tooltip) + { + m_parent->AddStatusHelp("mouse_lmb", tooltip); + } + } + ImGui::PopStyleVar(2); + ImGui::PopStyleColor(3); + ImGui::SetCursorPos(oldPos); + } + return clicked; +} + +void StreamBrowserDialog::EndBlock() +{ + ImGui::EndChild(); + auto& prefs = m_session->GetPreferences(); + if(prefs.GetBool("Appearance.Stream Browser.show_block_border")) + { + ImGui::PopStyleVar(); + } +} \ No newline at end of file diff --git a/src/ngscopeclient/StreamBrowserDialog.h b/src/ngscopeclient/StreamBrowserDialog.h index cb71d44a4..b4b280a3e 100644 --- a/src/ngscopeclient/StreamBrowserDialog.h +++ b/src/ngscopeclient/StreamBrowserDialog.h @@ -121,8 +121,11 @@ class StreamBrowserDialog : public Dialog void DoItemHelp(); + // Block handling + bool BeginBlock(const char *label, bool withButton = false, const char* tooltip = nullptr); + void EndBlock(); + // Rendeding of StreamBrowserDialog elements - void renderInfoLink(const char *label, const char *linktext, bool &clicked, bool &hovered); void startBadgeLine(); void renderBadge(ImVec4 color, ... /* labels, ending in NULL */); void renderInstrumentBadge(std::shared_ptr inst, bool latched, InstrumentBadge badge); @@ -134,7 +137,8 @@ class StreamBrowserDialog : public Dialog const std::vector& values, bool useColorForText = false, uint8_t cropTextTo = 0, - bool hideArrow = true); + bool hideArrow = true, + float paddingRight = 0); bool renderCombo( const char* label, bool alignRight, @@ -145,17 +149,16 @@ class StreamBrowserDialog : public Dialog const char* label, bool alignRight, ImVec4 color, - bool curValue); - bool renderToggleEXT( - const char* label, - bool alignRight, - ImVec4 color, - bool& curValue); - bool renderOnOffToggle(const char* label, bool alignRight, bool curValue); - bool renderOnOffToggleEXT(const char* label, bool alignRight, bool& curValue); + bool& curValue, + const char* valueOff = "OFF", + const char* valueOn = "ON", + uint8_t cropTextTo = 0, + float paddingRight = 0); + bool renderOnOffToggle(const char* label, bool alignRight, bool& curValue, const char* valueOff = "OFF", const char* valueOn = "ON", uint8_t cropTextTo = 0, float paddingRight = 0); void renderDownloadProgress(std::shared_ptr inst, InstrumentChannel *chan, bool isLast); - void renderPsuRows(bool isVoltage, bool cc, PowerSupplyChannel* chan,const char *setValue, const char *measuredValue, bool &clicked, bool &hovered); + bool renderPsuRows(bool isVoltage, bool cc, PowerSupplyChannel* chan, std::string& currentValue, float& committedValue, std::string& measuredValue, bool &clicked, bool &hovered); void renderAwgProperties(std::shared_ptr awg, FunctionGeneratorChannel* awgchan); + void renderDmmProperties(std::shared_ptr dmm, MultimeterChannel* dmmchan, bool isMain, bool &clicked, bool &hovered); // Rendering of an instrument node void renderInstrumentNode(std::shared_ptr instrument); @@ -167,15 +170,14 @@ class StreamBrowserDialog : public Dialog // Rendering of a channel node void renderChannelNode(std::shared_ptr instrument, size_t channelIndex, bool isLast); + void renderChannelProperties(std::shared_ptr scope, OscilloscopeChannel* scopechan, size_t channelIndex, std::shared_ptr scopeState); + // Rendering of a stream node void renderStreamNode(std::shared_ptr instrument, InstrumentChannel* channel, size_t streamIndex, bool renderName, bool renderProps); // Rendering of an Filter node void renderFilterNode(Filter* filter); - Session& m_session; - MainWindow* m_parent; - ///@brief Positions for badge display float m_badgeXMin; // left edge over which we must not overrun float m_badgeXCur; // right edge to render the next badge against diff --git a/src/ngscopeclient/ngscopeclient.h b/src/ngscopeclient/ngscopeclient.h index de1e8fe5e..51338e8fb 100644 --- a/src/ngscopeclient/ngscopeclient.h +++ b/src/ngscopeclient/ngscopeclient.h @@ -2,7 +2,7 @@ * * * ngscopeclient * * * -* Copyright (c) 2012-2025 Andrew D. Zonenberg and contributors * +* Copyright (c) 2012-2026 Andrew D. Zonenberg and contributors * * All rights reserved. * * * * Redistribution and use in source and binary forms, with or without modification, are permitted provided that the * @@ -44,6 +44,7 @@ #include +#include "OscilloscopeState.h" #include "BERTState.h" #include "PowerSupplyState.h" #include "FunctionGeneratorState.h" @@ -67,6 +68,7 @@ class InstrumentThreadArgs Session* session; //Additional per-instrument-type state we can add + std::shared_ptr oscilloscopestate; std::shared_ptr loadstate; std::shared_ptr meterstate; std::shared_ptr bertstate;