diff --git a/include/DoocsProcessScalar.h b/include/DoocsProcessScalar.h index ba17e7f..a9f2a28 100644 --- a/include/DoocsProcessScalar.h +++ b/include/DoocsProcessScalar.h @@ -44,10 +44,31 @@ namespace ChimeraTK { */ void auto_init() override; + /** Set the EGU axis configuration for this scalar. + * Applied in auto_init() after the DOOCS framework loads persisted values from the .conf file. + * If set, the .EGU sub-property is made read-only, preventing runtime modification. */ + void setAxisConfig(int logarithmic, float start, float stop, const std::string& label); + protected: void updateDoocsBuffer(const TransferElementID& transferElementId) override; + /// Apply EGU axis configuration from XML after auto_init(). + /// If the EGU is defined in XML, force-sets the value via set_plot_value() and + /// makes the .EGU sub-property read-only. On mismatch with the .conf value, + /// logs a warning and overwrites with the XML value. + void applyAxisConfig(); + ScalarRegisterAccessor _processScalar; + + /// EGU axis configuration from the XML file, applied after auto_init() + struct AxisConfig { + std::string label; + int logarithmic{}; + float start{}; + float stop{}; + }; + AxisConfig _axisConfig; + bool _axisConfigSet{false}; }; /********************************************************************************************************************/ @@ -101,11 +122,80 @@ namespace ChimeraTK { /********************************************************************************************************************/ + /********************************************************************************************************************/ + + template + void DoocsProcessScalar::setAxisConfig( + int logarithmic, float start, float stop, const std::string& label) { + _axisConfig.logarithmic = logarithmic; + _axisConfig.start = start; + _axisConfig.stop = stop; + _axisConfig.label = label; + _axisConfigSet = true; + } + + /********************************************************************************************************************/ + + template + void DoocsProcessScalar::applyAxisConfig() { + if(!_axisConfigSet) { + return; + } + if constexpr(std::is_base_of_v) { + return; + } + else { + auto* hist = this->get_histPointer(); + if(hist == nullptr) { + return; + } + // Read the EGU loaded from the .conf file (may be empty on first start) + std::string confEgu = hist->egu(); + if(!confEgu.empty() && confEgu == _axisConfig.label) { + // .conf EGU matches XML → nothing to do, but make .EGU read-only to prevent + // runtime modification by clients. + auto* eguProp = getEqFct()->find_property(this->basename() + ".EGU"); + if(eguProp != nullptr) { + eguProp->set_ro_access(); + } + return; + } + if(!confEgu.empty() && confEgu != _axisConfig.label) { + // .conf EGU differs from XML → this indicates the .conf was manually modified + // or the XML was changed after the server was first started. + // Log a warning, force-set the XML value, and make .EGU read-only. + std::cerr << "WARNING: " << this->basename() << ": .conf EGU (\"" << confEgu << "\") differs from XML EGU (\"" + << _axisConfig.label << "\"). Overwriting with XML value." << std::endl; + // TODO: Uncomment to throw an error instead of silently overwriting: + // throw ChimeraTK::logic_error( + // this->basename() + ": .conf EGU (\"" + confEgu + "\") differs from XML EGU (\"" + + // _axisConfig.label + "\"). Cannot overwrite read-only XML unit."); + } + // Force-set the XML value (bypasses D_hist::egu()'s "set only if empty" guard) + hist->set_plot_value(_axisConfig.logarithmic, _axisConfig.start, _axisConfig.stop, + doocs::Timestamp::now().to_time_t(), _axisConfig.label.c_str()); + // Make .EGU read-only so runtime client writes are rejected + auto* eguProp = getEqFct()->find_property(this->basename() + ".EGU"); + if(eguProp != nullptr) { + eguProp->set_ro_access(); + } + } + } + + /********************************************************************************************************************/ + template void DoocsProcessScalar::auto_init() { doocsAdapter.beforeAutoInit(); DOOCS_T::auto_init(); + + // Apply EGU axis configuration from XML after DOOCS loaded persisted values from .conf. + // If the EGU is defined in XML, the .EGU sub-property is made read-only to prevent + // runtime modification by clients. On first start the XML value is applied; on + // subsequent restarts the XML value is compared with the .conf value and re-applied + // if different. + applyAxisConfig(); // send the current value to the device // property is writeable OR the target accessor is writable and the only one connected to this property // The second case is to have bi-directional variables that are used to persist settings into the config file diff --git a/include/DoocsSpectrum.h b/include/DoocsSpectrum.h index dfcf96c..c024a5e 100644 --- a/include/DoocsSpectrum.h +++ b/include/DoocsSpectrum.h @@ -20,6 +20,14 @@ namespace ChimeraTK { class DoocsSpectrum : public D_spectrum, public boost::noncopyable, public PropertyBase { public: + /** Struct to hold EGU axis configuration from XML */ + struct AxisConfig { + std::string label; + int logarithmic{}; + float start{}; + float stop{}; + }; + /** The constructor expects an NDRegisterAccessor of float, which usually will * be a decorator to the implementation type. The decorator cannot be * generated in the constructor because the ProcessVariable aka @@ -42,7 +50,7 @@ namespace ChimeraTK { * generated in the constructor because the ProcessVariable aka * TransferElement does not know about it's size, which is needed by the * D_spectrum constructor. This is not a big drawback because the properties - * are generated by a factory function anyway. + * are created by a factory function anyway. * * This version of the constructor shall be used for read-only spectra, as they * do not need to be persisted but might require multiple buffers for short-term @@ -54,6 +62,12 @@ namespace ChimeraTK { boost::shared_ptr> startAccessor, boost::shared_ptr> incrementAccessor, size_t numberOfBuffers); + /** Set the EGU axis configuration for this spectrum. + * Stores the config so it can be re-applied in auto_init() after the DOOCS framework + * overwrites it with persisted values from the .conf file. + * If set, the .EGU/.XEGU sub-properties are made read-only, preventing runtime modification. */ + void setAxisConfig(const std::string& axis, AxisConfig config); + /** * Overload the set function which is called by DOOCS to inject sending to the * device. @@ -80,6 +94,13 @@ namespace ChimeraTK { /// unit testing. void sendToDevice(bool getLock); + /// Re-apply EGU after .conf loading in auto_init(). EGU is set initially in + /// DoocsPVFactory but then D_spectrum::read() loads .conf data. + /// If EGU is defined in XML, force-sets via set_plot_y_value()/set_plot_x_value() and + /// makes the .EGU/.XEGU sub-properties read-only. On mismatch with .conf values, + /// logs a warning and overwrites with XML values. + void applyAxisConfig(); + public: /// Flag whether the value has been modified since the content has been saved to disk the last time (see write()). bool modified{false}; @@ -89,6 +110,9 @@ namespace ChimeraTK { ScalarRegisterAccessor _startAccessor; ScalarRegisterAccessor _incrementAccessor; size_t _nBuffers; + + /// EGU axis configuration from the XML file, re-applied after auto_init() + std::map _axisConfig; }; } // namespace ChimeraTK diff --git a/include/PropertyDescription.h b/include/PropertyDescription.h index e56f89c..f0cb4fa 100644 --- a/include/PropertyDescription.h +++ b/include/PropertyDescription.h @@ -131,7 +131,17 @@ namespace ChimeraTK { // FIXME: should sort by name to put it into a set? struct AutoPropertyDescription : public PropertyDescription { enum class DataType { Byte, Short, Int, Long, Float, Double, Bool, Void, Auto }; + + struct Axis { + std::string label; + int logarithmic{}; + float start{}; + float stop{}; + }; + ChimeraTK::RegisterPath source; + std::map axis; + explicit AutoPropertyDescription(ChimeraTK::RegisterPath const& source_ = "", std::string location_ = "", std::string name_ = "", DataType dataType_ = DataType::Auto, bool hasHistory_ = true, bool isWriteable_ = true) : PropertyDescription(std::move(location_), std::move(name_), PropertyAttributes(hasHistory_, isWriteable_)), diff --git a/src/DoocsPVFactory.cc b/src/DoocsPVFactory.cc index 491a5d3..0980aea 100644 --- a/src/DoocsPVFactory.cc +++ b/src/DoocsPVFactory.cc @@ -79,6 +79,13 @@ namespace ChimeraTK { doocsPV->setMacroPulseNumberSource(propertyDescription.macroPulseNumberSource); doocsPV->setIsWriteableSource(propertyDescription.isWriteableSource); + // set engineering units (EGU) if configured in the xml file + auto const yIt = propertyDescription.axis.find("y"); + if(yIt != propertyDescription.axis.cend()) { + auto const& axis = yIt->second; + doocsPV->setAxisConfig(axis.logarithmic, axis.start, axis.stop, axis.label); + } + return doocsPV; } @@ -161,16 +168,29 @@ namespace ChimeraTK { spectrum->description(spectrumDescription.description); } + // Store EGU axis configuration so it can be re-applied in auto_init() after .conf loading auto const xIt = spectrumDescription.axis.find("x"); if(xIt != spectrumDescription.axis.cend()) { auto const& axis = xIt->second; - spectrum->xegu(axis.logarithmic, axis.start, axis.stop, axis.label.c_str()); + DoocsSpectrum::AxisConfig cfg; + cfg.label = axis.label; + cfg.logarithmic = axis.logarithmic; + cfg.start = axis.start; + cfg.stop = axis.stop; + doocsPV->setAxisConfig("x", cfg); + doocsPV->xegu(axis.logarithmic, axis.start, axis.stop, axis.label.c_str()); } auto const yIt = spectrumDescription.axis.find("y"); if(yIt != spectrumDescription.axis.cend()) { auto const& axis = yIt->second; - spectrum->egu(axis.logarithmic, axis.start, axis.stop, axis.label.c_str()); + DoocsSpectrum::AxisConfig cfg; + cfg.label = axis.label; + cfg.logarithmic = axis.logarithmic; + cfg.start = axis.start; + cfg.stop = axis.stop; + doocsPV->setAxisConfig("y", cfg); + doocsPV->egu(axis.logarithmic, axis.start, axis.stop, axis.label.c_str()); } doocsPV->setMacroPulseNumberSource(spectrumDescription.macroPulseNumberSource); diff --git a/src/DoocsSpectrum.cc b/src/DoocsSpectrum.cc index f9b64d8..e0970d4 100644 --- a/src/DoocsSpectrum.cc +++ b/src/DoocsSpectrum.cc @@ -65,6 +65,70 @@ namespace ChimeraTK { /********************************************************************************************************************/ + void DoocsSpectrum::setAxisConfig(const std::string& axis, AxisConfig config) { + _axisConfig[axis] = std::move(config); + } + + /********************************************************************************************************************/ + + void DoocsSpectrum::applyAxisConfig() { + auto applyAxis = [&](const std::string& axisName, auto setPlotValue, auto getUnit) { + auto const it = _axisConfig.find(axisName); + if(it == _axisConfig.cend()) { + return; + } + auto const& c = it->second; + + // Read the EGU loaded from the .conf file + std::string confEgu = getUnit(); + if(!confEgu.empty() && confEgu == c.label) { + // .conf EGU matches XML → nothing to do, make sub-property read-only + std::string subPropName = this->basename() + (axisName == "x" ? ".XEGU" : ".EGU"); + auto* prop = getEqFct()->find_property(subPropName); + if(prop != nullptr) { + prop->set_ro_access(); + } + return; + } + if(!confEgu.empty() && confEgu != c.label) { + // Improve this by adding another lamda. + std::string subPropName = this->basename() + (axisName == "x" ? ".XEGU" : ".EGU"); + // .conf EGU differs from XML + // Comment / Uncomment to warn and silently overwrite instead of thrwoing: + // std::cerr << "WARNING: " << subPropName + // << ": .conf value (\"" << confEgu << "\") differs from XML value (\"" + // << c.label << "\"). Overwriting with XML value." + // << std::endl; + throw ChimeraTK::logic_error(subPropName + ": .conf EGU (\"" + confEgu + "\") differs from XML EGU (\"" + + c.label + "\"). Cannot overwrite read-only XML unit."); + } + // Force-set the XML value (bypasses egu()/xegu()'s "set only if empty" guard) + setPlotValue(c.logarithmic, c.start, c.stop, doocs::Timestamp::now().to_time_t(), c.label.c_str()); + // Make the sub-property read-only + std::string subPropName = this->basename() + (axisName == "x" ? ".XEGU" : ".EGU"); + auto* prop = getEqFct()->find_property(subPropName); + if(prop != nullptr) { + prop->set_ro_access(); + } + }; + + applyAxis( + "x", + [this](int linlog, float start, float stop, time_t tm, const char* label) { + this->set_plot_x_value(linlog, start, stop, tm, label); + }, + [this]() { return this->plot_x_unit(); }); + + applyAxis( + "y", + [this](int linlog, float start, float stop, time_t tm, const char* label) { + this->set_plot_y_value(linlog, start, stop, tm, label); + }, + [this]() { return this->plot_y_unit(); }); + } + + /********************************************************************************************************************/ + void DoocsSpectrum::auto_init() { doocsAdapter.beforeAutoInit(); @@ -79,6 +143,13 @@ namespace ChimeraTK { // send the current value to the device D_spectrum::read(); modified = false; + + // Re-apply EGU after .conf loading. EGU was set initially in DoocsPVFactory, but + // D_spectrum::read() above loads persisted EGU from the .conf file. + // If EGU is defined in XML, force-sets via set_plot_y_value()/set_plot_x_value() + // (bypassing the "set only if empty" guard) and makes sub-properties read-only. + applyAxisConfig(); + if(this->get_access() == 1 || (_processArray.isWriteable() && !hasOtherPropertiesToUpdate())) { // property is writeable sendToDevice(false); diff --git a/src/VariableMapper.cc b/src/VariableMapper.cc index 01d755e..ea7dc22 100644 --- a/src/VariableMapper.cc +++ b/src/VariableMapper.cc @@ -256,6 +256,39 @@ namespace ChimeraTK { processHistoryAndWritableAttributes(*autoPropertyDescription, property); + auto unitNodes = property->get_children("unit"); + for(auto* const unit : unitNodes) { + const auto* unitElement = asXmlElement(unit); + auto axis = getAttributeValue(unitElement, "axis"); + if(axis != "y") { + throw std::invalid_argument(R"(Unsupported axis in property, must be "y": )" + axis); + } + + std::string label; + if(not unit->get_children().empty()) { + label = getContentString(unit); + } + + autoPropertyDescription->axis[axis].label = label; + try { + autoPropertyDescription->axis[axis].logarithmic = std::stoi(getAttributeValue(unitElement, "logarithmic")); + } + catch(std::invalid_argument&) { + } + + try { + autoPropertyDescription->axis[axis].start = std::stof(getAttributeValue(unitElement, "start")); + } + catch(std::invalid_argument&) { + } + + try { + autoPropertyDescription->axis[axis].stop = std::stof(getAttributeValue(unitElement, "stop")); + } + catch(std::invalid_argument&) { + } + } + addDescription(autoPropertyDescription); }