From 4d7a4b94d132ed605a10595e618262f633ff4bcb Mon Sep 17 00:00:00 2001 From: Alexander Krimm Date: Mon, 18 Nov 2024 09:55:43 +0100 Subject: [PATCH] TimingSource: basic implementation of snoop logic Adds timing block with unit-test and dokumentation. run `build/blocklib/timing/test/qa_timingSource timing-hardware` to run the actual hardware tests, which are disabled by default because they cannot work on the CI. Other changes: timing.hpp: use custom signal group to allow multiple threads Special thanks to @RalphSteinhagen and @slebedev for their feedback and for catching a few issues early on in the review. Signed-off-by: Alexander Krimm --- blocklib/picoscope/test/qa_Picoscope.cc | 2 +- blocklib/timing/CMakeLists.txt | 43 +- blocklib/timing/README.md | 80 +-- blocklib/timing/include/TimingSource.hpp | 476 ++++++++++++++++++ .../{src => include}/event_definitions.hpp | 82 +-- blocklib/timing/include/timing.hpp | 103 ++-- blocklib/timing/src/fairPlot.hpp | 4 +- blocklib/timing/src/test-timing.cpp | 6 +- blocklib/timing/test/CMakeLists.txt | 38 +- blocklib/timing/test/qa_timingSource.cpp | 234 +++++++++ 10 files changed, 922 insertions(+), 146 deletions(-) create mode 100644 blocklib/timing/include/TimingSource.hpp rename blocklib/timing/{src => include}/event_definitions.hpp (70%) create mode 100644 blocklib/timing/test/qa_timingSource.cpp diff --git a/blocklib/picoscope/test/qa_Picoscope.cc b/blocklib/picoscope/test/qa_Picoscope.cc index ff36f0b2..296b3946 100644 --- a/blocklib/picoscope/test/qa_Picoscope.cc +++ b/blocklib/picoscope/test/qa_Picoscope.cc @@ -83,7 +83,7 @@ void testStreamingBasics() { expect(ge(sink._nSamplesProduced, 80000UZ)); expect(le(sink._nSamplesProduced, 170000UZ)); - expect(eq(tagMonitor._tags.size(), 1UZ)); + expect(ge(tagMonitor._tags.size(), 1UZ)); if (tagMonitor._tags.size() == 1UZ) { const auto& tag = tagMonitor._tags[0]; expect(eq(tag.index, int64_t{0})); diff --git a/blocklib/timing/CMakeLists.txt b/blocklib/timing/CMakeLists.txt index 4e23c99e..aef4f30b 100644 --- a/blocklib/timing/CMakeLists.txt +++ b/blocklib/timing/CMakeLists.txt @@ -1,21 +1,28 @@ -if (NOT EMSCRIPTEN AND NOT CLANG) - add_library(timing INTERFACE) - target_include_directories(timing INTERFACE ${CMAKE_CURRENT_SOURCE_DIR}/include/) +if(NOT EMSCRIPTEN AND NOT CLANG) + add_library(timing INTERFACE include/timing.hpp include/TimingSource.hpp) + target_include_directories(timing + INTERFACE ${CMAKE_CURRENT_SOURCE_DIR}/include/) + target_link_libraries(timing INTERFACE PkgConfig::saftlib + PkgConfig::etherbone) - if (GR_DIGITIZERS_TOPLEVEL_PROJECT) - cmrc_add_resource_library( - ui_assets - NAMESPACE - ui_assets - WHENCE - ${imgui_SOURCE_DIR}/misc/fonts - ${imgui_SOURCE_DIR}/misc/fonts/Roboto-Medium.ttf) + if(GR_DIGITIZERS_TOPLEVEL_PROJECT) + cmrc_add_resource_library( + ui_assets NAMESPACE ui_assets WHENCE ${imgui_SOURCE_DIR}/misc/fonts + ${imgui_SOURCE_DIR}/misc/fonts/Roboto-Medium.ttf) - add_executable(test-timing src/test-timing.cpp) - target_link_libraries(test-timing PRIVATE gr-digitizers-options timing PkgConfig::saftlib PkgConfig::etherbone imgui implot gnuradio-algorithm gnuradio-core ui_assets) - endif () -endif () + add_executable(test-timing src/test-timing.cpp) + target_link_libraries( + test-timing + PRIVATE gr-digitizers-options + timing + imgui + implot + gnuradio-algorithm + gnuradio-core + ui_assets) + endif() +endif() -if (ENABLE_GR_DIGITIZERS_TESTING) - add_subdirectory(test) -endif () +if(ENABLE_GR_DIGITIZERS_TESTING) + add_subdirectory(test) +endif() diff --git a/blocklib/timing/README.md b/blocklib/timing/README.md index cee79d2c..122af4bc 100644 --- a/blocklib/timing/README.md +++ b/blocklib/timing/README.md @@ -3,6 +3,7 @@ This directory contains the timing realated gnuradio blocks along with some testing utilites and setup instructions. # Dependencies + For actually using the blocks and utilities built by this you will also need actual timing receiver hardware, the [wishbone kernel module](https://ohwr.org/project/fpga-config-space/tree/realtime_fixes/pcie-wb) has to be loaded and the saftlibd daemon has to be running and configured properly. @@ -19,7 +20,7 @@ sudo make install git clone --branch v3.0.3 --depth=1 https://github.com/GSI-CS-CO/saftlib.git cd saftlib ./autogen.sh -./configure +./configure make -j sudo make install ``` @@ -29,45 +30,44 @@ sudo make install - build etherbone and saftlib as described above - build gr-digitizers with `-DENABLE_TIMING=True` - load the required kernel modules if the card is connected via PCIe: `modprobe pcie_wb` - - verify they are loaded correctly: `lsmod | grep wb`, `dmesg | tail` + - verify they are loaded correctly: `lsmod | grep wb`, `dmesg | tail` - start saftd demon and attach timing card: `saftd tr0:dev/wbm0` (where `dev/wbm0` is the device path of the wishbone device without leading slash) - if there is no physical timing signal plugged into the card, the card has to be switched to master mode: - - `eb-console /dev/wbm0` and enter `mode master` and hit return. The console should print a message that the timing mode is changed. - - eb-console is included in [bel_projects](https://github.com/GSI-CS-CO/bel_projects). - If you only need the single tool and have installed etherbone on your system by other means you can build only eb-console: - ```bash - $ git clone https://github.com/GSI-CS-CO/bel_projects - $ cd bel_projects/tools - $ make EB=/usr/include eb-console # or point the EB variable to whatever prefix you installed etherbone to - $ ./eb-console dev/wbm0 # or dev/ttyUSB0 - ``` + - `eb-console /dev/wbm0` and enter `mode master` and hit return. The console should print a message that the timing mode is changed. + - eb-console is included in [bel_projects](https://github.com/GSI-CS-CO/bel_projects). + If you only need the single tool and have installed etherbone on your system by other means you can build only eb-console: + ```bash + $ git clone https://github.com/GSI-CS-CO/bel_projects + $ cd bel_projects/tools + $ make EB=/usr/include eb-console # or point the EB variable to whatever prefix you installed etherbone to + $ ./eb-console dev/wbm0 # or dev/ttyUSB0 + ``` - verify that everything works with saft-ctl: - - ``` bash - $ saft-ctl tr0 -ijkst - saftlib source version : saftlib 3.0.3 (6aab401-dirty): Aug 29 2023 09:50:19 - saftlib build info : built by unknown on Jan 1 1980 00:00:00 with localhost running - devices attached on this host : 1 - device: /de/gsi/saftlib/tr0, name: tr0, path: dev/wbm0, gatewareVersion : 6.1.2 - --gateware version info: - ---- Mon Aug 09 08:48:31 CEST 2021 - ---- fallout-v6.1.2 - ---- Arria V (5agxma3d4f27i3) - ---- CentOS Linux release 7.9.2009 (Core), kernel 3.10.0-1160.36.2.el7.x86_64 - ---- pexaria5 +db[12] +wrex1 - ---- Timing Group Jenkins - ---- tsl021.acc.gsi.de - ---- pci_control - ---- Version 18.1.0 Build 625 09/12/2018 SJ Standard Edition - ---- fallout-3847 - 6.1.2 - current temperature (Celsius): 48 - WR locked, time: 0x001181787bda0268 - receiver free conditions: 255, max (capacity of HW): 0(256), early threshold: 4294967296 ns, latency: 4096 ns - sinks instantiated on this host: 1 - /de/gsi/saftlib/tr0/software/_77 (minOffset: -1000000000 ns, maxOffset: 1000000000 ns) - -- actions: 0, delayed: 0, conflict: 0, late: 0, early: 0, overflow: 0 (max signalRate: 10Hz) - -- conditions: 0 - ``` - - snoop on all events: `saft-ctl tr0 snoop 0x0 0x0 0x0` and on another terminal inject an event: `saft-ctl tr0 inject 0x1154000140000000 0x15 1000000` to verify it arrives. - - finally launch `build/blocklib/timing/test-timing` to show the UI - + - ```bash + $ saft-ctl tr0 -ijkst + saftlib source version : saftlib 3.0.3 (6aab401-dirty): Aug 29 2023 09:50:19 + saftlib build info : built by unknown on Jan 1 1980 00:00:00 with localhost running + devices attached on this host : 1 + device: /de/gsi/saftlib/tr0, name: tr0, path: dev/wbm0, gatewareVersion : 6.1.2 + --gateware version info: + ---- Mon Aug 09 08:48:31 CEST 2021 + ---- fallout-v6.1.2 + ---- Arria V (5agxma3d4f27i3) + ---- CentOS Linux release 7.9.2009 (Core), kernel 3.10.0-1160.36.2.el7.x86_64 + ---- pexaria5 +db[12] +wrex1 + ---- Timing Group Jenkins + ---- tsl021.acc.gsi.de + ---- pci_control + ---- Version 18.1.0 Build 625 09/12/2018 SJ Standard Edition + ---- fallout-3847 + 6.1.2 + current temperature (Celsius): 48 + WR locked, time: 0x001181787bda0268 + receiver free conditions: 255, max (capacity of HW): 0(256), early threshold: 4294967296 ns, latency: 4096 ns + sinks instantiated on this host: 1 + /de/gsi/saftlib/tr0/software/_77 (minOffset: -1000000000 ns, maxOffset: 1000000000 ns) + -- actions: 0, delayed: 0, conflict: 0, late: 0, early: 0, overflow: 0 (max signalRate: 10Hz) + -- conditions: 0 + ``` + - snoop on all events: `saft-ctl tr0 snoop 0x0 0x0 0x0` and on another terminal inject an event: `saft-ctl tr0 inject 0x1154000140000000 0x15 1000000` to verify it arrives. + - finally launch `build/blocklib/timing/test-timing` to show the UI of the debug utility diff --git a/blocklib/timing/include/TimingSource.hpp b/blocklib/timing/include/TimingSource.hpp new file mode 100644 index 00000000..e1dabecc --- /dev/null +++ b/blocklib/timing/include/TimingSource.hpp @@ -0,0 +1,476 @@ +#ifndef GR_DIGITIZERS_TIMINGSOURCE_HPP +#define GR_DIGITIZERS_TIMINGSOURCE_HPP + +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include + +#include +#include +#include +#include + +#include "gnuradio-4.0/TriggerMatcher.hpp" + +#include "./event_definitions.hpp" +#include "./timing.hpp" + +namespace gr::timing { + +struct TimingSource : public gr::Block> { + template + using A = gr::Annotated; + using Description = Doc` character sequence. +The matcher has the format `[:[:BPC-START=<1|0>[:BEAM-IN=<1|0>[:[:BPID]]]]]`, where GID and EVT can be either Names or numeric values for +the timing group and event-number, BPC-START and BEAM-IN are boolean flags and SID and BPID are numeric values for the sequence id and the beam-process id. +Due to Hardware constraints it is not possible to only specify a value in the right part of the matcher without specifying all of the values left of it, +as they get mapped to prefix bit-mask. +The comma separated list of action can either contain the special value `PUBLISH()`, which will cause the matched events to be published as tags to the +output port, or the name of a port and a list of alternating delays and port-states in the parentheses: `IO1(,,...)` which allows to +e.g. trigger complex on/off patterns on one or multiple ports with on a single matching event or having one event switch an output on and another one +switching it off again. + +The following settings will generate a 8ms pulse on the DIO1 output of the timing receiver and publish a stream that outputs one sample with the timing +tag for every "BP_START" timing event in the SIS100 timing group. +``` +sample_rate: 0.0 +event_actions: + - "SIS100_RING:CMD_BP_START->IO1(400,on,8000,off)" + - "SIS100_RING->PUBLISH()" +``` +will lead to output in the following form: +``` +samples: [1, 3, ....] +tags: [ + 0 -> { BEAM-IN: true, BPC-START: false, BPCID: 0, BPCTS: 0, BPID: 0, EVENT-NAME: CMD_BP_START, EVENT-NO: 256, GID: 310, SID: 0, TIMING-GROUP: UNKNOWN-TIMING-GROUP, TIMING-ID: 1240196373932933120, TIMING-PARAM: 0, gr:trigger_offset: 0, trigger_name: CMD_BP_START/FAIR-TIMING:B=0.P=0.C=0.T=310, trigger_time: 633103525799216 } + 1 -> { IO-LEVEL: true, IO-NAME: IO1, TIMING-ID: 18446181123756130306, TIMING-PARAM: 0, gr:trigger_offset: 0, trigger_name: IO2_FALLING, trigger_time: 633103024372976 } + ... +] +``` +If you need a continuous stream of samples synchronised to the timing tags, you can set a non-zero sample rate, e.g. `sample_rate: 250.0` in Hz, which will cause the block to output +data at that average sampling rate. If no events are received, there is a configured delay at which the samples will be published anyway. +``` +samples: [0, 1, 0, 0, 3, 2, 2, ...] +tags: [ + 1 -> { BEAM-IN: true, BPC-START: false, BPCID: 0, BPCTS: 0, BPID: 0, EVENT-NAME: CMD_BP_START, EVENT-NO: 256, GID: 310, SID: 0, TIMING-GROUP: UNKNOWN-TIMING-GROUP, TIMING-ID: 1240196373932933120, TIMING-PARAM: 0, gr:trigger_offset: 0, trigger_name: CMD_BP_START/FAIR-TIMING:B=0.P=0.C=0.T=310, trigger_time: 633103525799216 } + 4 -> { IO-LEVEL: true, IO-NAME: IO1, TIMING-ID: 18446181123756130306, TIMING-PARAM: 0, gr:trigger_offset: 0, trigger_name: IO2_FALLING, trigger_time: 633103024372976 } + ... +] + ``` + +Also signal changes on the digital IOs will be added as tags to the output ports and change the value that will be output on the output port's bits left of the LSB(which indicates the presence of a tag): +``` +timingSource.out [uint8t]: 0b 0 0 0 0 1 1 1 1 +Timing Receiver IO port state: IO3 IO2 IO1 tag_present +``` + +This block does not change the configuration of the ports as input or output ports, so if you want to use a port as an output port you will have to configure +it accordingly using the `saft-io-ctl` utility. +)"">; + using ClockSourceType = std::chrono::tai_clock; + using TimePoint = std::chrono::time_point; + + PortOut out; + // TODO: PortIn< std::uint8_t, Async> DIO1..3 // Follow-up-PR + MsgPortIn ctxInfo; + + A, "event actions", Doc<"Configure which timing events should trigger IO port changes and/or get forwarded as timing tags. Syntax description and examples in the block documentation.">, Visible> event_actions; + A, Visible> sample_rate = 1000.f; + A> timing_device = ""; + A, Unit<"ns">> max_delay = 10'000'000; // 10 ms // todo: uint64t ns + A> verbose_console = false; + // TODO: enable/disable publishing on input port changes -> For now everything is published, add in follow-up PR + + GR_MAKE_REFLECTABLE(TimingSource, out, ctxInfo, event_actions, sample_rate, timing_device, max_delay, verbose_console); + + using ConditionsType = std::map, std::shared_ptr>>>; + Timing _timing; + using EventReaderType = decltype(_timing.snooped.new_reader()); + EventReaderType _event_reader = _timing.snooped.new_reader(); + ConditionsType _conditionProxies; + TimePoint _startTime; + std::size_t _publishedSamples = 0; + std::uint8_t _lastOutputState = 0; + std::uint8_t _outputState = 0; + std::uint8_t _nextOutputState = 0; + + void start() { + if (verbose_console) { + fmt::println("starting {}", this->name); + } + _timing.saftAppName = fmt::format("{}_{}", this->unique_name | std::views::filter([](auto c) { return std::isalnum(c); }) | std::ranges::to(), getpid()); + _timing.snoopID = 0xffffffffffffffffull; // Workaround: make the default condition of _timing not listen to anything + _timing.snoopMask = 0xffffffffffffffffull; + _timing.initialize(); + updateEventTriggers(); + _startTime = TimePoint(std::chrono::nanoseconds(_timing.currentTimeTAI())); + } + + void stop() { + if (verbose_console) { + fmt::println("stop {}", this->name); + } + this->requestStop(); + } + + static std::pair parseFilter(const std::string& newFilter) { + std::vector filterTokens = std::views::split(newFilter, ":"sv) | std::views::transform([](auto r) { return std::string_view(r.begin(), r.end()); }) | std::ranges::to(); + // initialize with the fixed format identifier + uint64_t filter = 0x1000000000000000UL; + uint64_t mask = 0xf000000000000000UL; + // timing group ID, given as string or numeric value + if (filterTokens.empty()) { + return {filter, mask}; + } + if (!filterTokens[0].empty()) { + uint64_t numericGroupId; + if (std::from_chars_result result = std::from_chars(filterTokens[0].begin(), filterTokens[0].end(), numericGroupId); result.ptr == filterTokens[0].end()) { + filter |= numericGroupId << 48; + } else if (auto gidFromName = timingGroupTable | std::views::filter([&e = filterTokens[0]](auto& m) { return e == m.second.first; }) | std::views::transform([](auto& m) { return m.first; }) | std::views::take(1) | std::ranges::to(); !gidFromName.empty()) { + filter |= static_cast(gidFromName[0]) << 48; + } else { + throw gr::exception(fmt::format("Illegal Timing Group Name/Number: {}", filterTokens[0])); + } + mask |= 0xfffull << 48; + } + // event by name or numeric ID + if (filterTokens.size() > 1) { + uint64_t numericEventId; + if (std::from_chars_result result = std::from_chars(filterTokens[1].begin(), filterTokens[1].end(), numericEventId); result.ptr == filterTokens[1].end() && !filterTokens[1].empty()) { + filter |= numericEventId << 36; + } else if (auto evtNrFromName = eventNrTable | std::views::filter([&e = filterTokens[1]](auto& m) { return e == m.second.first; }) | std::views::transform([](auto& m) { return m.first; }) | std::views::take(1) | std::ranges::to(); !evtNrFromName.empty()) { + filter |= static_cast(evtNrFromName[0]) << 36; + } else { + throw gr::exception(fmt::format("Illegal EventName/Number: {}", filterTokens[1])); + } + mask |= 0xfffull << 36; + } + // FLAGS BEAM-IN + if (filterTokens.size() > 2) { + if (filterTokens[2] == "BEAM-IN=1") { + filter |= 0x1ULL << 35; + } else if (filterTokens[2] == "BEAM-IN=0") { + filter |= 0x0ULL << 35; + } else { + throw gr::exception(fmt::format("BEAM-IN flag has to be 1 or 0, was {}", filterTokens[2])); + } + mask |= 0x1ULL << 35; + } + // FLAGS BPC-START + if (filterTokens.size() > 3) { + if (filterTokens[3] == "BPC-START=1") { + filter |= 0x1ULL << 34; + } else if (filterTokens[3] == "BPC-START=0") { + filter |= 0x0ULL << 34; + } else { + throw gr::exception(fmt::format("BPC-START flag has to be 1 or 0, was {}", filterTokens[3])); + } + mask |= 0x1ULL << 34; + } + // numeric Sequence id (to be extended to LSA pattern) + if (filterTokens.size() > 4) { + mask |= 0x3ULL << 32; // unused flag bits have to be set in mask for filter to be a prefix + uint64_t numericSid; + if (std::from_chars_result result = std::from_chars(filterTokens[4].begin(), filterTokens[4].end(), numericSid); result.ptr == filterTokens[4].end() && !filterTokens[1].empty()) { + filter |= numericSid << 20u; + mask |= 0xfffull << 20u; + } else { + throw gr::exception(fmt::format("Illegal Sequence ID: {}", filterTokens[4])); + } + } + // numeric beam process id (to be extended to LSA pattern) + if (filterTokens.size() > 5) { + uint64_t numericBP; + if (std::from_chars_result result = std::from_chars(filterTokens[5].begin(), filterTokens[5].end(), numericBP); result.ptr == filterTokens[5].end() && !filterTokens[1].empty()) { + filter |= numericBP << 6u; + mask |= 0x3fffull << 6u; + } else { + throw gr::exception(fmt::format("Illegal Beam Process ID: {}", filterTokens[5])); + } + } + if (filterTokens.size() > 6) { + throw gr::exception(fmt::format("Filter Pattern contains too many tokens: {}", std::span(filterTokens.begin() + 6, filterTokens.end()))); + } + return {filter, mask}; + } + + static auto parseTriggerAction(const std::string& actions) { + std::vector>>> parsedActions; + std::size_t actionPos = 0; + while (true) { + unsigned long actionStart = actions.find('(', actionPos); + if (actionStart == std::string::npos) { + throw gr::exception(fmt::format("Invalid action format, missing opening parenthesis, definition: {}, pos: {}", actions, actionPos)); + } + std::size_t actionEnd = actions.find(')', actionStart); + if (actionEnd == std::string::npos) { + throw gr::exception(fmt::format("Invalid action format, missing closing parenthesis, definition: {}, pos: {}", actions, actionPos)); + } + auto parsedAction = std::string_view(actions.begin() + static_cast(actionStart) + 1uz, actions.begin() + static_cast(actionEnd)) // + | std::views::split(","sv) | std::views::pairwise | std::views::stride(2) | std::views::transform([](auto t) { + auto [tt, a] = t; + std::uint64_t delay; + if (std::from_chars_result result = std::from_chars(tt.begin(), tt.end(), delay); result.ptr != tt.end() || tt.empty()) { + throw gr::exception(fmt::format("Invalid action format, cannot parse delay to value: {}, error-code: {}", tt, make_error_code(result.ec).value())); + } + return std::pair{delay, std::string(a.begin(), a.end())}; + }) | + std::ranges::to(); + parsedActions.push_back(std::pair{actions.substr(actionPos, actionStart - actionPos), std::move(parsedAction)}); + if (actionEnd + 1 >= actions.size()) { + break; + } else if (actions[actionEnd + 1] != ',') { + throw gr::exception(fmt::format("Invalid action format, actions should be separated by comma, definition: {}, pos: {}", actions, actionEnd)); + } else { + actionPos = actionEnd + 2; + } + } + return parsedActions; + } + + static auto splitTriggerActions(const std::string& newTrigger) { + const std::size_t pos = newTrigger.find("->"); + if (pos == std::string::npos) { + throw gr::exception(fmt::format("Invalid trigger definition (must contain '->' separator): {}", newTrigger)); + } + auto trigger = newTrigger.substr(0, pos); + auto actions = newTrigger.substr(pos + 2); + return std::pair{trigger, actions}; + } + + void updateEventTriggers() { + auto outputs = _timing.receiver->getOutputs() | std::views::transform([&sigGroup = _timing.saftSigGroup](auto kvp) { + auto& [name, path] = kvp; + return std::pair(name, saftlib::Output_Proxy::create(path, sigGroup)); + }) | std::ranges::to(); + + std::set keepMatcher; + for (auto& trigger : event_actions.value) { + if (verbose_console) { + fmt::print("adding new trigger: {}\n", trigger); + } + auto [triggerMatcher, triggerActions] = splitTriggerActions(trigger); + keepMatcher.insert(triggerMatcher); + if (!_conditionProxies.contains(triggerMatcher)) { + std::set keepAction; + auto [filter, mask] = parseFilter(triggerMatcher); + auto parsedActions = parseTriggerAction(triggerActions); + for (auto& [ioName, changes] : parsedActions) { + if (ioName == "PUBLISH") { + std::string triggerAction = ioName; + keepAction.insert(triggerAction); + if (!_conditionProxies[triggerMatcher].contains(triggerAction)) { + if (verbose_console) { + fmt::print("adding software condition for trigger {:#x}:{:#x}\n", filter, mask); + } + if (!changes.empty()) { + throw gr::exception(fmt::format("Illegal software condition specification: cannot have arguments: {}", parsedActions)); + } + auto condition = saftlib::SoftwareCondition_Proxy::create(_timing.sink->NewCondition(false, filter, mask, 0), _timing.saftSigGroup); + condition->setAcceptLate(true); + condition->setAcceptEarly(true); + condition->setAcceptConflict(true); + condition->setAcceptDelayed(true); + condition->SigAction.connect([this](uint64_t id, uint64_t param, const saftlib::Time& deadline, const saftlib::Time& executed, uint16_t flags) { + auto data = _timing.snoop_writer.reserve(1); + data[0] = Timing::Event{deadline.getTAI(), id, param, flags, executed.getTAI()}; + }); + condition->setActive(true); + _conditionProxies[triggerMatcher].insert({triggerAction, std::move(condition)}); + continue; + } + } + if (!outputs.contains(ioName)) { + if (verbose_console) { + fmt::print("ignoring non-existent output {}, available output ports: {}\n", ioName, outputs | std::views::keys); + } + continue; + } + for (auto& [delay, state] : changes) { + std::string triggerAction = fmt::format("{}({},{})", ioName, delay, state); + keepAction.insert(triggerAction); + if (!_conditionProxies[triggerMatcher].contains(triggerAction)) { + if (verbose_console) { + fmt::print("adding output condition for trigger {:#x}:{:#x}, setting {} to {} after {}ns\n", filter, mask, ioName, state, delay * 1000ull); + } + std::uint64_t io_edge = state == "on" ? 1ull : 0ull; + try { + _conditionProxies[triggerMatcher].insert({triggerAction, saftlib::OutputCondition_Proxy::create(outputs[ioName]->NewCondition(true, filter, mask, static_cast(delay * 1000ull), io_edge), _timing.saftSigGroup)}); + } catch (...) { + throw gr::exception(fmt::format("failed to add output condition for trigger {:#x}:{:#x}, setting {} to {} after {}ns\n", filter, mask, ioName, state, delay * 1000ull)); + if (verbose_console) { + fmt::print("failed to add output condition for trigger {:#x}:{:#x}, setting {} to {} after {}ns\n", filter, mask, ioName, state, delay * 1000ull); + } + } + } + } + } + std::erase_if(_conditionProxies[triggerMatcher], [&keepAction](const auto& item) { return !keepAction.contains(item.first); }); + } + } + std::erase_if(_conditionProxies, [&keepMatcher](const auto& item) { return !keepMatcher.contains(item.first); }); + } + + void settingsChanged(const property_map& oldSettings, const property_map& newSettings) { + using std::operator""sv; + // configure io ports based on settings + if (newSettings.contains("timing_device") && timing_device != std::get(oldSettings.at("timing_device"))) { + if (_timing.receiver) { + timing_device = std::get(oldSettings.at("timing_device")); + throw gr::exception(std::format("Changing the timing receiver device name while the timing source is running is not supported.")); + } + _timing.deviceName = timing_device; + } // end io triggers + if (newSettings.contains("event_actions") && event_actions != std::get>(oldSettings.at("event_actions"))) { + if (_timing.receiver) { + updateEventTriggers(); + } + } // end io triggers + } + + Tag eventToTag(const Timing::Event& event) { + Tag tag; + std::uint64_t id = event.id(); + tag.map.emplace(tag::TRIGGER_TIME.shortKey(), event.time); + tag.map.emplace("TIMING-ID", id); + tag.map.emplace("TIMING-PARAM", event.param()); + if (event.isIo) { + bool level = static_cast(id & 0x1ull); + std::string name = _timing.idToIoName(id); + tag.map.emplace("IO-NAME", name); + tag.map.emplace("IO-LEVEL", level); + tag.map.emplace(tag::TRIGGER_NAME.shortKey(), fmt::format("{}_{}", name, level ? "RISING" : "FALLING")); + tag.map.emplace(tag::TRIGGER_OFFSET, 0); + } else { + tag.map.emplace("GID", event.gid); + if (eventNrTable.contains(event.gid)) { + tag.map.emplace("TIMING-GROUP", timingGroupTable.at(event.gid).first); + } else { + tag.map.emplace("TIMING-GROUP", "UNKNOWN-TIMING-GROUP"); + } + tag.map.emplace("EVENT-NO", event.eventNo); + const std::string eventName = [&event]() -> std::string { + if (eventNrTable.contains(event.eventNo)) { + return eventNrTable.at(event.eventNo).first; + } else { + return "UNKNOWN-EVENT"s; + } + }(); + tag.map.emplace("EVENT-NAME", eventName); + tag.map.emplace(tag::TRIGGER_NAME.shortKey(), fmt::format("{}{}FAIR-TIMING:B={}.P={}.C={}.T={}", eventName, gr::trigger::SEPARATOR, event.bpcid, event.sid, event.bpid, event.gid)); + tag.map.emplace("SID", event.sid); + tag.map.emplace("BPID", event.bpid); + tag.map.emplace("BEAM-IN", event.flagBeamin); + tag.map.emplace("BPC-START", event.flagBpcStart); + tag.map.emplace("BPCID", event.bpcid); + tag.map.emplace("BPCTS", event.bpcts); + tag.map.emplace(tag::TRIGGER_OFFSET, 0); // TODO: correctly calculate offset + } + return tag; + } + + void updateOutputState(const Timing::Event& event) { + // check if the event changed the output state and update it accordingly + // this logic makes sure that a bit is only changed once per published sample -> even short pulses or drops will at least trigger one sample flank + std::uint8_t inputNr = static_cast(event.id() & ~Timing::ECA_EVENT_MASK_LATCH) >> 1; + std::uint8_t mask = static_cast(0x1 << (inputNr + 1)); + _nextOutputState = (_nextOutputState & ~mask) | static_cast((event.id() & 0x1ull) << (inputNr + 1)); + if ((_outputState & mask) == (_lastOutputState & mask)) { // if the bit was already changed since the last sample + _outputState = (_nextOutputState & mask) | (_outputState & ~mask); + } + } + + work::Status processBulk(OutputSpanLike auto& outSpan) noexcept { + // update context information + _timing.saftSigGroup.wait_for_signal(static_cast(max_delay >> 22ul)); // process saftlib events, max_delay >> 22 is ~ 1/4 of max delay in ms + auto timingEvents = _event_reader.template get(); + // publish to output port and message port + std::size_t toPublish = 0; + for (const Timing::Event& event : timingEvents) { + gr::Tag timingTag = eventToTag(event); + // TODO: make stable against rounding errors by using global time difference instead of just between 2 events + std::size_t samplesUntilCurrentEvent = 0; + if (sample_rate != 0.0f) { + long publishedNs = static_cast(std::ceil(static_cast(_publishedSamples) / static_cast(sample_rate) * 1e9)); + samplesUntilCurrentEvent = static_cast(std::floor(static_cast((TimePoint(std::chrono::nanoseconds(event.time)) - _startTime).count() - publishedNs) * static_cast(sample_rate) * 1e-9)); + if (samplesUntilCurrentEvent > 0) { + if (_nextOutputState != _outputState) { + _lastOutputState = _nextOutputState; + _outputState = _nextOutputState; + } + std::ranges::fill(std::span(outSpan).subspan(toPublish, samplesUntilCurrentEvent), _outputState); + toPublish += samplesUntilCurrentEvent; + } + std::uint64_t offset = event.time - static_cast(std::chrono::duration_cast(_startTime.time_since_epoch()).count()) - static_cast(static_cast(_publishedSamples + samplesUntilCurrentEvent) / static_cast(sample_rate) * 1e9); + timingTag.map[gr::tag::TRIGGER_OFFSET.shortKey()] = offset; + } else { + samplesUntilCurrentEvent = 1; + if (_nextOutputState != _outputState) { + _lastOutputState = _nextOutputState; + _outputState = _nextOutputState; + } + outSpan[toPublish] = _outputState + 1; + toPublish++; + } + if (event.isIo) { + updateOutputState(event); + outSpan[toPublish - 1] = _outputState + 1; + } + outSpan.publishTag(timingTag.map, static_cast(toPublish - 1)); + if (verbose_console) { + fmt::print("publishing tag, {} samples before, then sample with tag {}\n", samplesUntilCurrentEvent, timingTag.map); + } + } + + // publish more samples if last sample is further in the past than the maximum allowed latency + if (sample_rate != 0.0f) { + auto now = TimePoint(std::chrono::nanoseconds(_timing.currentTimeTAI())); + long publishedNs = static_cast(std::ceil(static_cast(_publishedSamples + toPublish) / static_cast(sample_rate) * 1e9)); + auto catchUpNs = (now - _startTime).count() - static_cast(max_delay) - publishedNs; + if (catchUpNs > 0) { + std::size_t samplesUntilCurrentEvent = static_cast(std::floor(static_cast(catchUpNs) * static_cast(sample_rate) * 1e-9)); + if (samplesUntilCurrentEvent > 0) { + if (_nextOutputState != _outputState) { + _lastOutputState = _nextOutputState; + _outputState = _nextOutputState; + } + std::ranges::fill(std::span(outSpan).subspan(toPublish, samplesUntilCurrentEvent), _outputState); + toPublish += samplesUntilCurrentEvent; + } + } + } + outSpan.publish(toPublish); + _publishedSamples += toPublish; + + return work::Status::OK; + } + + void processMessages(gr::MsgPortIn& portIn, std::span messages) { + if (portIn.name == ctxInfo.name) { // TODO: check why portIn cannot be directly compared + for (auto& msg : messages) { + if (msg.data.has_value()) { + // these messages may contain context information and should be used to update some lookup table inside the block and possibly update timing filters + // the exact format and content of messages and cache has yet to be determined + } + } + } + } +}; + +} // namespace gr::timing + +auto registerTimingSource = gr::registerBlock(gr::globalBlockRegistry()); +static_assert(gr::HasProcessBulkFunction); + +#endif // GR_DIGITIZERS_TIMINGSOURCE_HPP diff --git a/blocklib/timing/src/event_definitions.hpp b/blocklib/timing/include/event_definitions.hpp similarity index 70% rename from blocklib/timing/src/event_definitions.hpp rename to blocklib/timing/include/event_definitions.hpp index 1fc223b8..0f17fea4 100644 --- a/blocklib/timing/src/event_definitions.hpp +++ b/blocklib/timing/include/event_definitions.hpp @@ -1,53 +1,57 @@ #ifndef GR_DIGITIZERS_EVENT_DEFINITIONS_HPP #define GR_DIGITIZERS_EVENT_DEFINITIONS_HPP -static const std::array bpcidColors{ +struct TimingColor { + int r = 0x0, g = 0x0, b = 0x0, a = 0xff; +}; + +static const std::array bpcidColors{ // colors taken from: https://git.acc.gsi.de/fcc-applications/common-context-widget-reactor/src/branch/master/common-context-widget-fx/src/main/resources/de/gsi/fcc/applications/common/contextwidget/fx/StylesColorsBase.css - ImColor{0x00, 0x70, 0xC0}, // 0x0070C0 - materials - ImColor{0x00, 0xB0, 0x50}, // 0x00B050 - bio - ImColor{0x5C, 0xB7, 0xC3}, // 0x5CB7C3 - cbm - ImColor{0x70, 0x30, 0xA0}, // 0x7030A0 - nustar-basic - ImColor{0xC6, 0x59, 0x11}, // 0xC65911 - machine - ImColor{0xFF, 0xDA, 0x4A}, // 0xFFDA4A - nustar-r3b - ImColor{0xC4, 0x8B, 0xD0}, // 0xC48BD0 - nustar-she-c - ImColor{0xE1, 0x4C, 0xFF}, // 0xE14CFF - nustar-she-p - ImColor{0xFF, 0xFF, 0x00}, // 0xFFFF00 - nustar-hades - ImColor{0xFF, 0xA6, 0x32}, // 0xFFA632 - plasma - ImColor{0x00, 0xFC, 0xD6}, // 0x00FCD6 - appa-pp - ImColor{0x80, 0x80, 0x80}, // 0x808080 - undefined - ImColor{0x9e, 0x9e, 0x9e} // 0x9e9e9e - invalid + TimingColor{0x00, 0x70, 0xC0}, // 0x0070C0 - materials + TimingColor{0x00, 0xB0, 0x50}, // 0x00B050 - bio + TimingColor{0x5C, 0xB7, 0xC3}, // 0x5CB7C3 - cbm + TimingColor{0x70, 0x30, 0xA0}, // 0x7030A0 - nustar-basic + TimingColor{0xC6, 0x59, 0x11}, // 0xC65911 - machine + TimingColor{0xFF, 0xDA, 0x4A}, // 0xFFDA4A - nustar-r3b + TimingColor{0xC4, 0x8B, 0xD0}, // 0xC48BD0 - nustar-she-c + TimingColor{0xE1, 0x4C, 0xFF}, // 0xE14CFF - nustar-she-p + TimingColor{0xFF, 0xFF, 0x00}, // 0xFFFF00 - nustar-hades + TimingColor{0xFF, 0xA6, 0x32}, // 0xFFA632 - plasma + TimingColor{0x00, 0xFC, 0xD6}, // 0x00FCD6 - appa-pp + TimingColor{0x80, 0x80, 0x80}, // 0x808080 - undefined + TimingColor{0x9e, 0x9e, 0x9e} // 0x9e9e9e - invalid }; // generic colormap for timing ids -static const std::array colorlist{ +static const std::array colorlist{ // gemerated with http://medialab.github.io/iwanthue/ - ImColor{78, 172, 215, 120}, - ImColor{200, 76, 41, 120}, - ImColor{85, 199, 102, 120}, - ImColor{186, 84, 191, 120}, - ImColor{101, 173, 51, 120}, - ImColor{117, 98, 204, 120}, - ImColor{169, 180, 56, 120}, - ImColor{211, 76, 146, 120}, - ImColor{71, 136, 57, 120}, - ImColor{209, 69, 88, 120}, - ImColor{86, 192, 158, 120}, - ImColor{217, 132, 45, 120}, - ImColor{102, 125, 198, 120}, - ImColor{201, 160, 65, 120}, - ImColor{199, 135, 198, 120}, - ImColor{156, 176, 104, 120}, - ImColor{168, 85, 112, 120}, - ImColor{55, 132, 95, 120}, - ImColor{203, 126, 93, 120}, - ImColor{118, 109, 41, 120}, + TimingColor{78, 172, 215, 120}, + TimingColor{200, 76, 41, 120}, + TimingColor{85, 199, 102, 120}, + TimingColor{186, 84, 191, 120}, + TimingColor{101, 173, 51, 120}, + TimingColor{117, 98, 204, 120}, + TimingColor{169, 180, 56, 120}, + TimingColor{211, 76, 146, 120}, + TimingColor{71, 136, 57, 120}, + TimingColor{209, 69, 88, 120}, + TimingColor{86, 192, 158, 120}, + TimingColor{217, 132, 45, 120}, + TimingColor{102, 125, 198, 120}, + TimingColor{201, 160, 65, 120}, + TimingColor{199, 135, 198, 120}, + TimingColor{156, 176, 104, 120}, + TimingColor{168, 85, 112, 120}, + TimingColor{55, 132, 95, 120}, + TimingColor{203, 126, 93, 120}, + TimingColor{118, 109, 41, 120}, }; // https://www-acc.gsi.de/wiki/Timing/TimingSystemGroupsAndMachines#Groups_for_Operation -static const std::map> timingGroupTable{{200, {"YRT1_TO_YRT1LQ1 ", "from ion source 1 to merging quadrupole"}}, {201, {"YRT1LQ1_TO_YRT1LC1 ", "from merging quadrupole at ion sources to chopper"}}, {202, {"YRT1LC1_TO_YRT1MH2 ", "injector from chopper to merging dipole"}}, {203, {"YRT1MH2_TO_CRYRING ", "from merging dipole to ring"}}, {210, {"CRYRING_RING ", "CRYRING ring"}}, {211, {"CRYRING_COOLER ", "CRYRING electron cooler"}}, {212, {"CRYRING_TO_YRE ", "Experiment line from CRYRING to YRE"}}, {300, {"SIS18_RING ", "SIS18 ring"}}, {301, {"SIS18_COOLER ", "SIS18 electron cooler"}}, {310, {"SIS100_RING ", "SIS100 ring"}}, {340, {"ESR_RING ", "ESR ring"}}, {341, {"ESR_COOLER ", "ESR electron cooler"}}, {345, {"ESR_TO_HTR ", "ESR to HITRAP"}}, {350, {"CR_RING ", "CR ring"}}, {360, {"HESR_RING ", "HESR ring"}}, {400, {"TO_BE_DISCUSSED ", "PLINAC proton source"}}, {448, {"PZU_QR ", "UNILAC Timing - Source Right"}}, {449, {"PZU_QL ", "UNILAC Timing - Source Left"}}, {450, {"PZU_QN ", "UNILAC Timing - Source High Charge State Injector (HLI)"}}, {451, {"PZU_UN ", "UNILAC Timing - High Charge State Injector (HLI)"}}, {452, {"PZU_UH ", "UNILAC Timing - High Current Injector (HSI)"}}, {453, {"PZU_AT ", "UNILAC Timing - Alvarez Cavities"}}, {454, {"PZU_TK ", "UNILAC Timing - Transfer Line"}}, {464, {"GTK7BC1L_TO_PLTKMH2__GS ", "GTK7BC1L to PLTKMH2 - shared group controlled by SIS18 accelerator"}}, {465, {"GTK7BC1L_TO_PLTKMH2__GU ", "GTK7BC1L to PLTKMH2 - shared group controlled by UNILAC accelerator"}}, {466, {"PLTKMH2_TO_SIS18__GS ", "PLTKMH2 to SIS18 - shared group controlled by SIS18 accelerator"}}, {467, {"PLTKMH2_TO_SIS18__GU ", "PLTKMH2 to SIS18 - shared group controlled by UNILAC accelerator"}}, {500, {"SIS18_TO_GTS1MU1 ", "SIS extraction to GTS1MU1"}}, {501, {"GTS1MU1_TO_GTE3MU1 ", "GTS1MU1 to GTE3MU1"}}, {502, {"GTE3MU1_TO_GTS6MU1 ", "GTE3MU1 to GTS6MU1"}}, {503, {"GTS6MU1_TO_GTS6MU1 ", "GTS6MU1_TO_GTS6MU1"}}, - {504, {"GTS6MU1_TO_ESR ", "GTS6MU1 to ESR"}}, {505, {"GTS1MU1_TO_GTS3MU1 ", "GTS1MU1 to GTS3MU1"}}, {506, {"GTS3MU1_TO_HHD ", "GTS3MU1 to HHD Dump"}}, {507, {"GTS3MU1_TO_GHFSMU1 ", "GTS3MU1 to GHFSMU1"}}, {508, {"GHFSMU1_TO_HFS ", "Experiment line to HFS"}}, {509, {"GHFSMU1_TO_GTS6MU1 ", "GHFSMU1 to GTS6MU1"}}, {510, {"GTS6MU1_TO_GTS7MU1 ", "GTS6MU1 to GTS7MU1"}}, {511, {"GTE3MU1_TO_GHHTMU1 ", "GTE3MU1 to GHHTMU1"}}, {512, {"GHHTMU1_TO_HHT ", "Experiment line to the high temperature experiment HHT"}}, {513, {"GHHTMU1_TO_GTH3MU1 ", "GHHTMU1 to GTH3MU1"}}, {514, {"GTH3MU1_TO_GTP1MU1 ", "GTH3MU1 to pion branch GTV2MU3"}}, {515, {"GTP1MU1_TO_HADES ", "Experiment line to the electron-positron-detector HADES (High Acceptance Di-Electron Spectrometer)"}}, {516, {"GTH3MU1_TO_GTS7MU1 ", "GTH3MU1 to GTS7MU1"}}, {517, {"GTS7MU1_TO_GTH4MU1 ", "GTS7MU1 to GTH4MU1"}}, {518, {"GTH4MU1_TO_HTM ", "Experiment line to the particle therapy experiment HTM"}}, {519, {"GTH4MU1_TO_GTH4MU2 ", "GTH4MU1 to GTH4MU2"}}, {520, {"GTP1MU1_TO_GTH4MU2 ", "GTP1MU1 to GTH4MU2 pion transfer line"}}, {521, {"GTH4MU2_TO_GTV1MU1 ", "GTH4MU2 to GTV1MU1"}}, {522, {"GTV1MU1_TO_GHTDMU1 ", "GTV1MU1 to GHTDMU1"}}, {523, {"GTV1MU1_TO_GTV2MU2 ", "GTV1MU1 to GTV2MU2"}}, {524, {"ESR_TO_GTV2MU2 ", "ESR extraction to TV2MU2"}}, {525, {"GHTDMU1_TO_HTC ", "Experiment line to nuclear reaction experiments at HTC (exotic nuclei and nuclear dynamics)"}}, {526, {"GTV2MU2_TO_GTV2MU3 ", "GTV2MU2 to GTV2MU3"}}, {527, {"GTV2MU3_TO_HTA ", "Experiment line to HTA"}}, {528, {"GTV2MU3_TO_GHTBMU1 ", "GTV2MU3 to GHTBMU1"}}, {529, {"GHTBMU1_TO_HTP ", "Experiment line to HTP Dump"}}, {530, {"GHTDMU1_TO_HTD ", "Experiment line to HTD"}}, {531, {"GHTBMU1_TO_YRT1MH2 ", "GHTBMU1 to YRT1MH2"}}, {592, {"UR_TO_GUH1MU2 ", "HSI LEBT Right Source"}}, {593, {"UL_TO_GUH1MU2 ", "HSI LEBT Left Source"}}, {595, {"UN_TO_GUN3BC1L ", "HLI LEBT"}}, {608, {"GUH1MU2_TO_GUH2BC1L ", "GUH1MU2 to GUH2BC1L"}}, {609, {"GUH2BC1L_TO_GUS3MK1 ", "HSI"}}, - {610, {"GUS3MK1_TO_GUS4MK6 ", "GUS3MK1 to GUS4MK6"}}, {611, {"GUS3MK1_TO_GUS3MK2 ", "GUS3MK1 to GUS3MK2"}}, {612, {"GUS3MK2_TO_GUS4MK6 ", "GUS3MK2 to GUS4MK6"}}, {613, {"GUS3MK2_TO_US3 ", "Diagnostic line US3"}}, {624, {"GUN3BC1L_TO_GUN6MU2 ", "HLI"}}, {625, {"GUN6MU2_TO_GUS4MK6 ", "GUN6MU2 to GUS4MK6"}}, {626, {"GUN6MU2_TO_UCW ", "Development line to CW"}}, {640, {"GUS4MK6_TO_GUT1MK0 ", "Poststripper"}}, {641, {"GUT1MK0_TO_GUT1MK1 ", "GUT1MK0 to GUT1MK1"}}, {642, {"GUT1MK1_TO_GUT2MK2 ", "GUT1MK1 to GUT2MK2"}}, {656, {"GUT1MK1_TO_GTK3MV1 ", "GUT1MK1 to GTK3MV1"}}, {657, {"GTK3MV1_TO_GTK3MV4 ", "GTK3MV1 to GTK3MV4"}}, {658, {"GTK3MV1_TO_GTK3MV3 ", "GTK3MV1 to GTK3MV3"}}, {659, {"GTK3MV3_TO_GTK3MV4 ", "GTK3MV3 to GTK3MV4"}}, {660, {"GTK3MV3_TO_TKD ", "Diagnostic line TKD"}}, {661, {"GTK3MV4_TO_GTK7BC1L ", "GTK3MV4 to GTK7BC1L"}}, {672, {"GUT1MK0_TO_GUM2MU5 ", "GUT1MK0 to GUM2MU5"}}, {673, {"GUM2MU5_TO_UM1 ", "Experiment line to UM1"}}, {674, {"GUM2MU5_TO_GUM3MU6 ", "GUM2MU5 to GUM3MU6"}}, {675, {"GUM3MU6_TO_UM2 ", "Experiment line to UM2"}}, {676, {"GUM3MU6_TO_UM3 ", "Experiment line to UM3"}}, {677, {"GUT2MK2_TO_GUZCMU1 ", "GUT2MK2 to GUZCMU1"}}, {678, {"GUZCMU1_TO_UZ6 ", "Experiment line to UZ6"}}, {679, {"GUZCMU1_TO_UZ7 ", "Experiment line to UZ7"}}, {680, {"GUT2MK2_TO_UY7 ", "Experiment line to UY7"}}, {688, {"GUT2MK2_TO_GUXAMU3 ", "GUT2MK2 to GUXAMU3"}}, {689, {"GUXAMU3_TO_GUXAMU4 ", "GUXAMU3 to GUXAMU4"}}, {690, {"GUXAMU3_TO_GUXCMU3 ", "GUXAMU3 to GUXCMU3"}}, {691, {"GUXAMU4_TO_GUXBMU5 ", "GUXAMU4 to GUXBMU5"}}, {692, {"GUXAMU4_TO_UX3 ", "Experiment line to UX3"}}, {693, {"GUXBMU5_TO_UX2 ", "Experiment line to UX2"}}, {694, {"GUXCMU3_TO_GUXFMU1 ", "GUXCMU3 to GUXFMU1"}}, {695, {"GUXCMU3_TO_UX6 ", "Experiment line to UX6"}}, {696, {"GUXFMU1_TO_GUXHMU2 ", "GUXFMU1 to GUXHMU2"}}, {697, {"GUXFMU1_TO_UX7 ", "Experiment line to UX7"}}, {698, {"GUXHMU2_TO_UX0 ", "Experiment line to UX0"}}, {699, {"GUXHMU2_TO_UX8 ", "Experiment line to UX8"}}, - {928, {"SIS18_B2B_EXTRACT ", "B2B internal: extraction from SIS18"}}, {929, {"SIS18_B2B_ESR ", "B2B internal: transfer SIS18 to ESR"}}, {930, {"SIS18_B2B_SIS100 ", "B2B internal: transfer SIS18 to SIS100"}}, {933, {"ESR_B2B_EXTRACT ", "B2B internal: extraction from ESR"}}, {934, {"ESR_B2B_CRYRING ", "B2B internal: transfer ESR to CRYRING"}}, {938, {"CRYRING_B2B_EXTRACT ", "B2B internal: extraction from CRYRING"}}, {944, {"SIS100_B2B_EXTRACT ", "B2B internal: extraction from SIS100"}}, {0xFFF, {"LocalGroup", "never sent by data master"}}}; +static const std::map> timingGroupTable{{200, {"YRT1_TO_YRT1LQ1", "from ion source 1 to merging quadrupole"}}, {201, {"YRT1LQ1_TO_YRT1LC1", "from merging quadrupole at ion sources to chopper"}}, {202, {"YRT1LC1_TO_YRT1MH2", "injector from chopper to merging dipole"}}, {203, {"YRT1MH2_TO_CRYRING", "from merging dipole to ring"}}, {210, {"CRYRING_RING", "CRYRING ring"}}, {211, {"CRYRING_COOLER", "CRYRING electron cooler"}}, {212, {"CRYRING_TO_YRE", "Experiment line from CRYRING to YRE"}}, {300, {"SIS18_RING", "SIS18 ring"}}, {301, {"SIS18_COOLER", "SIS18 electron cooler"}}, {310, {"SIS100_RING", "SIS100 ring"}}, {340, {"ESR_RING", "ESR ring"}}, {341, {"ESR_COOLER", "ESR electron cooler"}}, {345, {"ESR_TO_HTR", "ESR to HITRAP"}}, {350, {"CR_RING", "CR ring"}}, {360, {"HESR_RING", "HESR ring"}}, {400, {"TO_BE_DISCUSSED", "PLINAC proton source"}}, {448, {"PZU_QR", "UNILAC Timing - Source Right"}}, {449, {"PZU_QL", "UNILAC Timing - Source Left"}}, {450, {"PZU_QN", "UNILAC Timing - Source High Charge State Injector (HLI)"}}, {451, {"PZU_UN", "UNILAC Timing - High Charge State Injector (HLI)"}}, {452, {"PZU_UH", "UNILAC Timing - High Current Injector (HSI)"}}, {453, {"PZU_AT", "UNILAC Timing - Alvarez Cavities"}}, {454, {"PZU_TK", "UNILAC Timing - Transfer Line"}}, {464, {"GTK7BC1L_TO_PLTKMH2__GS", "GTK7BC1L to PLTKMH2 - shared group controlled by SIS18 accelerator"}}, {465, {"GTK7BC1L_TO_PLTKMH2__GU", "GTK7BC1L to PLTKMH2 - shared group controlled by UNILAC accelerator"}}, {466, {"PLTKMH2_TO_SIS18__GS", "PLTKMH2 to SIS18 - shared group controlled by SIS18 accelerator"}}, {467, {"PLTKMH2_TO_SIS18__GU", "PLTKMH2 to SIS18 - shared group controlled by UNILAC accelerator"}}, {500, {"SIS18_TO_GTS1MU1", "SIS extraction to GTS1MU1"}}, {501, {"GTS1MU1_TO_GTE3MU1", "GTS1MU1 to GTE3MU1"}}, {502, {"GTE3MU1_TO_GTS6MU1", "GTE3MU1 to GTS6MU1"}}, {503, {"GTS6MU1_TO_GTS6MU1", "GTS6MU1_TO_GTS6MU1"}}, {504, {"GTS6MU1_TO_ESR", "GTS6MU1 to ESR"}}, + {505, {"GTS1MU1_TO_GTS3MU1", "GTS1MU1 to GTS3MU1"}}, {506, {"GTS3MU1_TO_HHD", "GTS3MU1 to HHD Dump"}}, {507, {"GTS3MU1_TO_GHFSMU1", "GTS3MU1 to GHFSMU1"}}, {508, {"GHFSMU1_TO_HFS", "Experiment line to HFS"}}, {509, {"GHFSMU1_TO_GTS6MU1", "GHFSMU1 to GTS6MU1"}}, {510, {"GTS6MU1_TO_GTS7MU1", "GTS6MU1 to GTS7MU1"}}, {511, {"GTE3MU1_TO_GHHTMU1", "GTE3MU1 to GHHTMU1"}}, {512, {"GHHTMU1_TO_HHT", "Experiment line to the high temperature experiment HHT"}}, {513, {"GHHTMU1_TO_GTH3MU1", "GHHTMU1 to GTH3MU1"}}, {514, {"GTH3MU1_TO_GTP1MU1", "GTH3MU1 to pion branch GTV2MU3"}}, {515, {"GTP1MU1_TO_HADES", "Experiment line to the electron-positron-detector HADES (High Acceptance Di-Electron Spectrometer)"}}, {516, {"GTH3MU1_TO_GTS7MU1", "GTH3MU1 to GTS7MU1"}}, {517, {"GTS7MU1_TO_GTH4MU1", "GTS7MU1 to GTH4MU1"}}, {518, {"GTH4MU1_TO_HTM", "Experiment line to the particle therapy experiment HTM"}}, {519, {"GTH4MU1_TO_GTH4MU2", "GTH4MU1 to GTH4MU2"}}, {520, {"GTP1MU1_TO_GTH4MU2", "GTP1MU1 to GTH4MU2 pion transfer line"}}, {521, {"GTH4MU2_TO_GTV1MU1", "GTH4MU2 to GTV1MU1"}}, {522, {"GTV1MU1_TO_GHTDMU1", "GTV1MU1 to GHTDMU1"}}, {523, {"GTV1MU1_TO_GTV2MU2", "GTV1MU1 to GTV2MU2"}}, {524, {"ESR_TO_GTV2MU2", "ESR extraction to TV2MU2"}}, {525, {"GHTDMU1_TO_HTC", "Experiment line to nuclear reaction experiments at HTC (exotic nuclei and nuclear dynamics)"}}, {526, {"GTV2MU2_TO_GTV2MU3", "GTV2MU2 to GTV2MU3"}}, {527, {"GTV2MU3_TO_HTA", "Experiment line to HTA"}}, {528, {"GTV2MU3_TO_GHTBMU1", "GTV2MU3 to GHTBMU1"}}, {529, {"GHTBMU1_TO_HTP", "Experiment line to HTP Dump"}}, {530, {"GHTDMU1_TO_HTD", "Experiment line to HTD"}}, {531, {"GHTBMU1_TO_YRT1MH2", "GHTBMU1 to YRT1MH2"}}, {592, {"UR_TO_GUH1MU2", "HSI LEBT Right Source"}}, {593, {"UL_TO_GUH1MU2", "HSI LEBT Left Source"}}, {595, {"UN_TO_GUN3BC1L", "HLI LEBT"}}, {608, {"GUH1MU2_TO_GUH2BC1L", "GUH1MU2 to GUH2BC1L"}}, {609, {"GUH2BC1L_TO_GUS3MK1", "HSI"}}, {610, {"GUS3MK1_TO_GUS4MK6", "GUS3MK1 to GUS4MK6"}}, + {611, {"GUS3MK1_TO_GUS3MK2", "GUS3MK1 to GUS3MK2"}}, {612, {"GUS3MK2_TO_GUS4MK6", "GUS3MK2 to GUS4MK6"}}, {613, {"GUS3MK2_TO_US3", "Diagnostic line US3"}}, {624, {"GUN3BC1L_TO_GUN6MU2", "HLI"}}, {625, {"GUN6MU2_TO_GUS4MK6", "GUN6MU2 to GUS4MK6"}}, {626, {"GUN6MU2_TO_UCW", "Development line to CW"}}, {640, {"GUS4MK6_TO_GUT1MK0", "Poststripper"}}, {641, {"GUT1MK0_TO_GUT1MK1", "GUT1MK0 to GUT1MK1"}}, {642, {"GUT1MK1_TO_GUT2MK2", "GUT1MK1 to GUT2MK2"}}, {656, {"GUT1MK1_TO_GTK3MV1", "GUT1MK1 to GTK3MV1"}}, {657, {"GTK3MV1_TO_GTK3MV4", "GTK3MV1 to GTK3MV4"}}, {658, {"GTK3MV1_TO_GTK3MV3", "GTK3MV1 to GTK3MV3"}}, {659, {"GTK3MV3_TO_GTK3MV4", "GTK3MV3 to GTK3MV4"}}, {660, {"GTK3MV3_TO_TKD", "Diagnostic line TKD"}}, {661, {"GTK3MV4_TO_GTK7BC1L", "GTK3MV4 to GTK7BC1L"}}, {672, {"GUT1MK0_TO_GUM2MU5", "GUT1MK0 to GUM2MU5"}}, {673, {"GUM2MU5_TO_UM1", "Experiment line to UM1"}}, {674, {"GUM2MU5_TO_GUM3MU6", "GUM2MU5 to GUM3MU6"}}, {675, {"GUM3MU6_TO_UM2", "Experiment line to UM2"}}, {676, {"GUM3MU6_TO_UM3", "Experiment line to UM3"}}, {677, {"GUT2MK2_TO_GUZCMU1", "GUT2MK2 to GUZCMU1"}}, {678, {"GUZCMU1_TO_UZ6", "Experiment line to UZ6"}}, {679, {"GUZCMU1_TO_UZ7", "Experiment line to UZ7"}}, {680, {"GUT2MK2_TO_UY7", "Experiment line to UY7"}}, {688, {"GUT2MK2_TO_GUXAMU3", "GUT2MK2 to GUXAMU3"}}, {689, {"GUXAMU3_TO_GUXAMU4", "GUXAMU3 to GUXAMU4"}}, {690, {"GUXAMU3_TO_GUXCMU3", "GUXAMU3 to GUXCMU3"}}, {691, {"GUXAMU4_TO_GUXBMU5", "GUXAMU4 to GUXBMU5"}}, {692, {"GUXAMU4_TO_UX3", "Experiment line to UX3"}}, {693, {"GUXBMU5_TO_UX2", "Experiment line to UX2"}}, {694, {"GUXCMU3_TO_GUXFMU1", "GUXCMU3 to GUXFMU1"}}, {695, {"GUXCMU3_TO_UX6", "Experiment line to UX6"}}, {696, {"GUXFMU1_TO_GUXHMU2", "GUXFMU1 to GUXHMU2"}}, {697, {"GUXFMU1_TO_UX7", "Experiment line to UX7"}}, {698, {"GUXHMU2_TO_UX0", "Experiment line to UX0"}}, {699, {"GUXHMU2_TO_UX8", "Experiment line to UX8"}}, {928, {"SIS18_B2B_EXTRACT", "B2B internal: extraction from SIS18"}}, + {929, {"SIS18_B2B_ESR", "B2B internal: transfer SIS18 to ESR"}}, {930, {"SIS18_B2B_SIS100", "B2B internal: transfer SIS18 to SIS100"}}, {933, {"ESR_B2B_EXTRACT", "B2B internal: extraction from ESR"}}, {934, {"ESR_B2B_CRYRING", "B2B internal: transfer ESR to CRYRING"}}, {938, {"CRYRING_B2B_EXTRACT", "B2B internal: extraction from CRYRING"}}, {944, {"SIS100_B2B_EXTRACT", "B2B internal: extraction from SIS100"}}, {0xFFF, {"LocalGroup", "never sent by data master"}}}; // https://www-acc.gsi.de/wiki/Timing/TimingSystemEventNumbers static const std::map> eventNrTable{{0, {"EVT_PZ_ChanEnd", "SIS/ESR PZ only: mark end of channel"}}, {1, {"EVT_START_RF", "Power to RF cavities"}}, {2, {"EVT_START_IQ", "Begin of beam production"}}, {3, {"EVT_IQ_HEATING", "Begin of ion source arc, ECR RF"}}, {4, {"EVT_PREP_BEAM_ON", "Switch on chopper, read act. values"}}, {5, {"EVT_IQ_GAS_ON", "Start of heading gas (ion source)"}}, {6, {"EVT_BEAM_ON", "Valid beam"}}, {8, {"EVT_BEAM_OFF", "End of beam production"}}, {10, {"EVT_STOP_IQ", "Switch IQ off"}}, {12, {"EVT_STOP_RF", "Switch RF off"}}, {16, {"EVT_PREP_NEXT_ACC", "Prepare next acc., write set values"}}, {17, {"EVT_AUX_PRP_NXT_ACC", "Set values in magnet prep. cycles"}}, {18, {"EVT_RF_PREP_NXT_ACC", "Begin of RF extra cycle"}}, {19, {"EVT_PREP_UNI_DIAG", "Prepare diagnostic devices, Unilac"}}, {20, {"EVT_PREP_AUX", "Trigger BIF beam diagnostics"}}, {21, {"EVT_UNLOCK_ALVAREZ", "Unlock A4 for next pulse"}}, {22, {"EVT_PREP_EXP", "Pretrigger for Experiments"}}, {24, {"EVT_RF_AUX_TRIG", "Trigger ADC in RF extra cycles"}}, {25, {"EVT_MAGN_DOWN", "Set magnets to zero current"}}, {26, {"EVT_SD_AUX_START", "Beam diagnostics aux start trigger"}}, {27, {"EVT_SD_AUX_STOP", "Beam diagnostics aux stop trigger"}}, {28, {"EVT_PRETRIG_BEAM", "Magnets on flattop, PG trigger"}}, {29, {"EVT_UNI_END_CYCLE", "End of a UNILAC cycle"}}, {30, {"EVT_READY_TO_SIS", "10 ms before beam transfer"}}, {31, {"EVT_SRC_DST_ID", "Source/Destination of beam"}}, {32, {"EVT_START_CYCLE", "First Event in a cycle"}}, {33, {"EVT_START_BFELD", "Start of B-Field"}}, {34, {"EVT_PEAK_UP", "Peaking trip up"}}, {35, {"EVT_INJECT", "B-field reached injection level"}}, {36, {"EVT_UNI_TRANS", "Demand UNILAC beam"}}, {37, {"EVT_UNI_ACKN", "Acknowledge from Unilac"}}, {38, {"EVT_UNI_READY", "Unilac ready, transfer in preparation"}}, {39, {"EVT_MB_LOAD", "Start Bumper magnet ramp up"}}, {40, {"EVT_MB_TRIGGER", "Start active Bumper Ramp (down)"}}, {41, {"EVT_INJECT_END", "Start of injection from unilac"}}, {42, {"EVT_TIMEOUT_1", "Trigger timeout channel 1"}}, {43, {"EVT_RAMP_START", "Start of acc/deacc ramp in magnets"}}, {44, {"EVT_PREP_INJ", "Prepare devices for next Injection"}}, {45, {"EVT_FLATTOP", "Magnets reached Flattop"}}, {46, {"EVT_EXTR_START_SLOW", "Start of extraction"}}, {47, {"EVT_MK_LOAD_1", "Load Kicker for HF-triggered puls"}}, {48, {"EVT_MK_LOAD_2", "Load Kicker for Timinggenerator-triggered puls"}}, {49, {"EVT_KICK_START_1", "Start Kicker for HF-triggered extraction"}}, {50, {"EVT_TIMEOUT_2", "Trigger timeout channel 2"}}, {51, {"EVT_EXTR_END", "End of extraction"}}, {52, {"EVT_FLATTOP_END", "End of Flattop (Magnets) reached"}}, {53, {"EVT_PREP_EXTR", "Prepare devices for next Extraction"}}, {54, {"EVT_PEAK_DOWN", "Peaking strip down"}}, {55, {"EVT_END_CYCLE", "End of a cycle"}}, {56, {"EVT_SYNCH", "Trigger all function generators"}}, {57, {"EVT_EXTR_BUMP", "Start of closed orbit distortion"}}, {58, {"EVT_SIS_ACK_TO_ESR", "SIS acknowledge to ESR"}}, {59, {"EVT_SIS_READY_1", "SIS ready for extraction to ESR"}}, {60, {"EVT_SIS_READY_2", "SIS ready for extraction to ESR"}}, {61, {"EVT_TRANS_START_1", "Begin of transmission to ESR"}}, {62, {"EVT_TRANS_START_2", "Begin of transmission to ESR"}}, {63, {"EVT_PHASE_SYNCH_GATE_1", "Begin of first phase synchronisation between ESR- and SIS-HF"}}, {64, {"EVT_TIMEOUT_3", "Trigger timeout channel 3"}}, {65, {"EVT_PHASE_SYNCH_GATE_2", "Begin of second phase synchronisation between ESR- and SIS-HF"}}, {66, {"EVT_TIMEOUT_4", "Trigger timeout channel 4"}}, {67, {"EVT_TIMEOUT_5", "Trigger timeout channel 5"}}, {68, {"EVT_TIMEOUT_6", "Trigger timeout channel 6"}}, {69, {"EVT_KICK_START_2", "Start Kicker for TG-triggered extraction"}}, {70, {"EVT_UNI_PREP", "Demand setting of TK (200 ms before beam)"}}, {71, {"EVT_INJ_BUMP", "Closed orbit destortion for reinjection"}}, {72, {"EVT_RE_INJ_START", "SIS is ready for reinjection"}}, diff --git a/blocklib/timing/include/timing.hpp b/blocklib/timing/include/timing.hpp index 0b60e11f..cab7fd97 100644 --- a/blocklib/timing/include/timing.hpp +++ b/blocklib/timing/include/timing.hpp @@ -29,8 +29,11 @@ static std::chrono::time_point taiNsToUtc(uint64_t in class Timing { public: - static const int milliToNano = 1000000; - static const int minTriggerOffset = 100; + static const int milliToNano = 1000000; + static const int minTriggerOffset = 100; + static const uint64_t ECA_EVENT_ID_LATCH = 0xfffe000000000000ull; /* FID=MAX & GRPID=MAX-1 */ + static const uint64_t ECA_EVENT_MASK_LATCH = 0xfffe000000000000ull; + static const uint64_t IO_CONDITION_OFFSET = 5000ull; /** * Structure to atuomatically encode and decode GSI/FAIR specific timing events as documented in: * https://www-acc.gsi.de/wiki/Timing/TimingSystemEvent @@ -53,7 +56,7 @@ class Timing { * 04-15 | 48-59 | 12 | GID | Timing group ID: see event_definitions.hpp for the existing timing groups * 16-27 | 36-47 | 12 | EVENTNO | Event Number: determines the type of event e.g CME_BP_START, see event_definitions.hpp * 28-31 | 32-35 | 4 | FLAGS | - * 28 | 34 | 1 | BEAM-IN | + * 28 | 35 | 1 | BEAM-IN | * 29 | 34 | 1 | BPC-START | First event in a new Beam Production Chain * 30 | 33 | 1 | RESERVED 1 | * 31 | 32 | 1 | RESERVED 2 | @@ -111,6 +114,7 @@ class Timing { uint64_t time = 0; uint64_t executed = 0; uint16_t flags = 0x0; + bool isIo = false; Event(const Event&) = default; Event(Event&&) = default; @@ -131,11 +135,11 @@ class Timing { return ((value & ((1UL << bitsize) - 1)) << position); } - explicit Event(uint64_t timestamp = 0, uint64_t id = 1UL << 60, uint64_t param = 0, uint16_t _flags = 0, uint64_t _executed = 0) + explicit Event(uint64_t timestamp = 0, uint64_t id = 1UL << 60, uint64_t param = 0, uint16_t _flags = 0, uint64_t _executed = 0, bool _isIo = false) : // id fid{extractField(id)}, gid{extractField(id)}, eventNo{extractField(id)}, flagBeamin{extractField(id)}, flagBpcStart{extractField(id)}, flagReserved1{extractField(id)}, flagReserved2{extractField(id)}, sid{extractField(id)}, bpid{extractField(id)}, reserved{extractField(id)}, reqNoBeam{extractField(id)}, virtAcc{extractField(id)}, // param - bpcid{extractField(param)}, bpcts{extractField(param)}, time{timestamp}, executed{_executed}, flags{_flags} {} + bpcid{extractField(param)}, bpcts{extractField(param)}, time{timestamp}, executed{_executed}, flags{_flags}, isIo{_isIo} {} [[nodiscard]] uint64_t id() const { // clang-format:off @@ -205,13 +209,16 @@ class Timing { std::vector> outputs; std::map triggers; std::vector events = {}; + saftbus::SignalGroup saftSigGroup; + std::shared_ptr sink; + decltype(snooped.new_writer()) snoop_writer = snooped.new_writer(); private: - decltype(snooped.new_writer()) snoop_writer = snooped.new_writer(); - bool tried = false; - std::shared_ptr saftd; - std::shared_ptr sink; - std::shared_ptr condition; + bool tried = false; + std::shared_ptr saftd; + std::shared_ptr condition; + std::shared_ptr ioCondition; + std::map map_PrefixName; /* Translation table IO name <> prefix */ public: bool initialized = false; @@ -219,12 +226,14 @@ class Timing { uint64_t snoopID = 0x0; uint64_t snoopMask = 0x0; std::shared_ptr receiver; + std::string saftAppName = "gr_timing_example"; + std::string deviceName; private: - void updateExistingTrigger(const Trigger& trigger, const std::map::iterator& existing, const std::string& output) const { - auto proxy = saftlib::Output_Proxy::create(output); + void updateExistingTrigger(const Trigger& trigger, const std::map::iterator& existing, const std::string& output) { + auto proxy = saftlib::Output_Proxy::create(output, saftSigGroup); if (trigger.delay != existing->second.delay || trigger.flattop != existing->second.flattop) { // update condition for rising edge - auto matchingConditions = proxy->getAllConditions() | std::views::transform([](const auto& cond) { return saftlib::OutputCondition_Proxy::create(cond); }) | std::views::filter([&trigger](const auto& cond) { return cond->getID() == trigger.id && cond->getMask() == std::numeric_limits::max(); }); + auto matchingConditions = proxy->getAllConditions() | std::views::transform([this](const auto& cond) { return saftlib::OutputCondition_Proxy::create(cond, saftSigGroup); }) | std::views::filter([&trigger](const auto& cond) { return cond->getID() == trigger.id && cond->getMask() == std::numeric_limits::max(); }); std::ranges::for_each(matchingConditions, [&trigger](const auto& cond) { if (cond->getOn()) { cond->setOffset(static_cast(trigger.delay) * milliToNano + minTriggerOffset); @@ -235,14 +244,14 @@ class Timing { } } - void removeHardwareTrigger(const Trigger& trigger, const std::string& output) const { - auto proxy = saftlib::Output_Proxy::create(output); - auto matchingConditions = proxy->getAllConditions() | std::views::transform([](const auto& cond) { return saftlib::OutputCondition_Proxy::create(cond); }) | std::views::filter([&trigger](const auto& cond) { return cond->getID() == trigger.id && cond->getMask() == std::numeric_limits::max(); }); + void removeHardwareTrigger(const Trigger& trigger, const std::string& output) { + auto proxy = saftlib::Output_Proxy::create(output, saftSigGroup); + auto matchingConditions = proxy->getAllConditions() | std::views::transform([this](const auto& cond) { return saftlib::OutputCondition_Proxy::create(cond, saftSigGroup); }) | std::views::filter([&trigger](const auto& cond) { return cond->getID() == trigger.id && cond->getMask() == std::numeric_limits::max(); }); std::ranges::for_each(matchingConditions, [](const auto& cond) { cond->Destroy(); }); } - void newHardwareTrigger(const Trigger& trigger, const std::string& output) const { - auto proxy = saftlib::Output_Proxy::create(output); + void newHardwareTrigger(const Trigger& trigger, const std::string& output) { + auto proxy = saftlib::Output_Proxy::create(output, saftSigGroup); proxy->NewCondition(true, trigger.id, std::numeric_limits::max(), static_cast(trigger.delay) * milliToNano + minTriggerOffset, true); proxy->NewCondition(true, trigger.id, std::numeric_limits::max(), static_cast(trigger.delay + trigger.flattop) * milliToNano + minTriggerOffset, false); } @@ -255,7 +264,7 @@ class Timing { if (condition) { condition->Destroy(); } - condition = SoftwareCondition_Proxy::create(sink->NewCondition(false, snoopID, snoopMask, 0)); + condition = SoftwareCondition_Proxy::create(sink->NewCondition(false, snoopID, snoopMask, 0), saftSigGroup); condition->setAcceptLate(true); condition->setAcceptEarly(true); condition->setAcceptConflict(true); @@ -265,6 +274,31 @@ class Timing { data[0] = Timing::Event{deadline.getTAI(), id, param, flags, executed.getTAI()}; }); condition->setActive(true); + // Digital IO ports + if (!ioCondition) { + for (auto& [ioName, ioAddress] : receiver->getInputs()) { + uint64_t prefix = ECA_EVENT_ID_LATCH + (map_PrefixName.size() << 1); // fixed prefix for software condition, rest of the bits identify IO, least significant bit is io level + map_PrefixName[prefix] = ioName; + auto input = saftlib::Input_Proxy::create(ioAddress, saftSigGroup); + input->setEventEnable(false); + input->setEventPrefix(prefix); + input->setEventEnable(true); + } + ioCondition = saftlib::SoftwareCondition_Proxy::create(sink->NewCondition(true, ECA_EVENT_ID_LATCH, ECA_EVENT_MASK_LATCH, 10000), saftSigGroup); + ioCondition->SigAction.connect([this](uint64_t id, uint64_t param, const saftlib::Time& deadline, const saftlib::Time& executed, uint16_t flags) { + auto data = this->snoop_writer.reserve(1); + data[0] = Timing::Event{deadline.getTAI(), id, param, flags, executed.getTAI(), true}; + }); + } + } + + std::string idToIoName(const std::uint64_t id) { + std::uint64_t filter = id & (0xffffffffffffffffull - 1); + if (map_PrefixName.contains(filter)) { + return map_PrefixName.at(filter); + } else { + return "UNKNOWN-IO"; + } } void initialize() { @@ -272,18 +306,29 @@ class Timing { initialized = true; } else { try { - saftd = SAFTd_Proxy::create(); + saftd = SAFTd_Proxy::create("/de/gsi/saftlib", saftSigGroup); // get a specific device std::map devices = saftd->getDevices(); - if (devices.empty()) { - std::cerr << "" << std::endl; - fmt::print("No devices attached to saftd, continuing with simulated timing\n"); - simulate = true; - initialized = true; - return; + if (deviceName.empty()) { + if (devices.empty()) { + std::cerr << "" << std::endl; + fmt::print("No devices attached to saftd, continuing with simulated timing\n"); + simulate = true; + initialized = true; + return; + } + receiver = TimingReceiver_Proxy::create(devices.begin()->second, saftSigGroup); + } else { + if (!devices.contains(deviceName)) { + std::cerr << "" << std::endl; + fmt::print("Could not find device {}, continuing with simulated timing\n", deviceName); + simulate = true; + initialized = true; + return; + } + receiver = TimingReceiver_Proxy::create(devices.at(deviceName), saftSigGroup); } - receiver = TimingReceiver_Proxy::create(devices.begin()->second); - sink = SoftwareActionSink_Proxy::create(receiver->NewSoftwareActionSink("gr_timing_example")); + sink = SoftwareActionSink_Proxy::create(receiver->NewSoftwareActionSink(saftAppName), saftSigGroup); updateSnoopFilter(); for (const auto& [i, output] : receiver->getOutputs() | std::views::enumerate) { const auto& [name, port] = output; @@ -308,7 +353,7 @@ class Timing { while (true) { auto duration = std::chrono::duration_cast(startTime - std::chrono::system_clock::now() + std::chrono::milliseconds(5)).count(); if (duration > 0) { - saftlib::wait_for_signal(static_cast(std::clamp(duration, 50L, std::numeric_limits::max() + 0L))); + saftSigGroup.wait_for_signal(static_cast(std::clamp(duration, 50L, std::numeric_limits::max() + 0L))); } else { break; } diff --git a/blocklib/timing/src/fairPlot.hpp b/blocklib/timing/src/fairPlot.hpp index 49b459a0..585cd764 100644 --- a/blocklib/timing/src/fairPlot.hpp +++ b/blocklib/timing/src/fairPlot.hpp @@ -140,8 +140,8 @@ void plotNamedEvents(const char* label_id, const FairPlot::ScrollingBuffer& p int bpcidColormap() { static std::array colorData = [&bpcidColors = bpcidColors]() { std::array colors{}; - for (std::size_t i = 0; i < bpcidColors.size(); i++) { - colors[i] = bpcidColors[i]; + for (const auto& [i, c] : std::views::zip(std::views::iota(0), bpcidColors)) { + colors[i] = ImColor{c.r, c.g, c.b, c.a}; } return colors; }(); diff --git a/blocklib/timing/src/test-timing.cpp b/blocklib/timing/src/test-timing.cpp index fe94e2ee..89410d2a 100644 --- a/blocklib/timing/src/test-timing.cpp +++ b/blocklib/timing/src/test-timing.cpp @@ -22,10 +22,9 @@ #include #include -#include "event_definitions.hpp" +#include "../include/event_definitions.hpp" #include "fairPlot.hpp" #include "fair_header.h" -#include #include enum class InjectState { STOPPED, RUNNING, SINGLE }; @@ -112,7 +111,8 @@ std::size_t getStableBPCIDColorIndex(T id) { template ImColor getStableBPCIDColor(T id) { - return ImColor{bpcidColors[getStableBPCIDColorIndex(static_cast(id))]}; + TimingColor c = bpcidColors[getStableBPCIDColorIndex(static_cast(id))]; + return ImColor{c.r, c.g, c.b, c.a}; } std::pair TimingGroupFilterDropdown() { diff --git a/blocklib/timing/test/CMakeLists.txt b/blocklib/timing/test/CMakeLists.txt index 5f9f0ada..4296a606 100644 --- a/blocklib/timing/test/CMakeLists.txt +++ b/blocklib/timing/test/CMakeLists.txt @@ -1,18 +1,28 @@ function(add_ut_test TEST_NAME) - add_executable(${TEST_NAME} ${TEST_NAME}.cpp) - if ((CMAKE_CXX_COMPILER_ID MATCHES ".*Clang")) # needed for clang15 (false positives, fixed in clang16) - target_compile_options(${TEST_NAME} PRIVATE -Wall) - target_link_options(${TEST_NAME} PRIVATE -Wall) - else () - target_compile_options(${TEST_NAME} PRIVATE -fsanitize=address -Wall) - target_link_options(${TEST_NAME} PRIVATE -fsanitize=address -Wall) - endif () - target_include_directories(${TEST_NAME} PRIVATE ${CMAKE_BINARY_DIR}/include ${CMAKE_CURRENT_BINARY_DIR}) - target_link_libraries(${TEST_NAME} PRIVATE gr-digitizers fmt ut) - add_test(NAME ${TEST_NAME} COMMAND ${CMAKE_CROSSCOMPILING_EMULATOR} ${CMAKE_CURRENT_BINARY_DIR}/${TEST_NAME}) + add_executable(${TEST_NAME} ${TEST_NAME}.cpp) + if((CMAKE_CXX_COMPILER_ID MATCHES ".*Clang")) + # needed for clang15 (false positives, fixed in clang16) + target_compile_options(${TEST_NAME} PRIVATE -Wall) + target_link_options(${TEST_NAME} PRIVATE -Wall) + else() + target_compile_options(${TEST_NAME} PRIVATE -fsanitize=address -Wall) + target_link_options(${TEST_NAME} PRIVATE -fsanitize=address -Wall) + endif() + target_include_directories(${TEST_NAME} PRIVATE ${CMAKE_BINARY_DIR}/include + ${CMAKE_CURRENT_BINARY_DIR}) + target_link_libraries(${TEST_NAME} PRIVATE gr-digitizers fmt ut) + add_test(NAME ${TEST_NAME} COMMAND ${CMAKE_CROSSCOMPILING_EMULATOR} + ${CMAKE_CURRENT_BINARY_DIR}/${TEST_NAME}) endfunction() -if(NOT EMSCRIPTEN AND NOT CLANG) ## clang/libc++ does not support the require features to convert from TAI to UTC and 'std::views::enumerate' - add_ut_test(qa_timing) - target_link_libraries(qa_timing PRIVATE timing PkgConfig::saftlib PkgConfig::etherbone) +# clang/libc++ does not support the required features to convert from +# TAI to UTC and 'std::views::enumerate' +if(NOT EMSCRIPTEN AND NOT CLANG) + add_ut_test(qa_timing) + target_link_libraries(qa_timing PRIVATE timing) + + add_ut_test(qa_timingSource) + target_link_libraries( + qa_timingSource PRIVATE gnuradio-algorithm gnuradio-core gr-basic + gr-testing timing) endif() diff --git a/blocklib/timing/test/qa_timingSource.cpp b/blocklib/timing/test/qa_timingSource.cpp new file mode 100644 index 00000000..7d658c03 --- /dev/null +++ b/blocklib/timing/test/qa_timingSource.cpp @@ -0,0 +1,234 @@ +#include +#include +#include +#include +#include +#include +#include +#include + +using namespace std::string_literals; +using namespace std::chrono_literals; +using namespace boost::ut; + +namespace fair::timing::test { + +const suite TimingBlockHelpers = [] { + "filter-parsing"_test = []() { + auto expectTiming = [](auto a, auto b) { + auto aa = gr::timing::TimingSource::parseFilter(a); + expect(aa == b) << fmt::format("checking: {}\ngot: {:#x}, {:#x}\nexp: {:#x}, {:#x}\n", a, aa.first, aa.second, b.first, b.second); + }; + + expectTiming("", std::pair{0x1000000000000000ul, 0xf000000000000000ul}); + expectTiming("SIS100_RING:CMD_BP_START", std::pair{0x1136100000000000ul, 0xfffffff000000000ul}); + expectTiming("SIS100_RING:CMD_BP_START:BEAM-IN=1", std::pair{0x1136100800000000ul, 0xfffffff800000000ul}); + expectTiming("SIS100_RING:CMD_BP_START:BEAM-IN=1:BPC-START=0", std::pair{0x1136100800000000ul, 0xfffffffc00000000ul}); + expectTiming("SIS100_RING:CMD_BP_START:BEAM-IN=1:BPC-START=0:0", std::pair{0x1136100800000000ul, 0xfffffffffff00000ul}); + expectTiming("SIS100_RING:CMD_BP_START:BEAM-IN=1:BPC-START=0:0:2", std::pair{0x1136100800000080ul, 0xffffffffffffffc0ul}); + expectTiming("301:256", std::pair{0x112d100000000000ul, 0xfffffff000000000ul}); + expect(throws([]() { gr::timing::TimingSource::parseFilter("INVALID_TIMING_GROUP"); })) << "Unknown TimingGroup should throw"; + expect(throws([]() { gr::timing::TimingSource::parseFilter("SIS100_RING:UNKNOWN_EVENT"); })) << "Unknown Event should throw"; + expect(throws([]() { gr::timing::TimingSource::parseFilter("SIS100_RING:CMD_CUSTOM_DIAG1:5"); })) << "Illegal Flags should throw"; + expect(throws([]() { gr::timing::TimingSource::parseFilter("SIS100_RING:CMD_CUSTOM_DIAG1:5"); })) << "Illegal Flags should throw"; + expect(throws([]() { gr::timing::TimingSource::parseFilter("SIS100_RING:CMD_CUSTOM_DIAG1:BEAM-IN=1:BPC-START=4"); })) << "Illegal Flags should throw"; + expect(throws([]() { gr::timing::TimingSource::parseFilter("SIS100_RING:CMD_CUSTOM_DIAG1:BEAM-IN=0:BPC-START=1:x"); })) << "Illegal sequence id should throw"; + expect(throws([]() { gr::timing::TimingSource::parseFilter("SIS100_RING:CMD_CUSTOM_DIAG1:BEAM-IN=0:BPC-START=1:5:test"); })) << "Illegal peam process id should throw"; + expect(throws([]() { gr::timing::TimingSource::parseFilter("SIS100_RING::BEAM-IN=0:BPC-START=1:5:7"); })) << "Zero length filter token should throw"; + }; + + "trigger-action-parsing"_test = []() { + auto expectTrigger = [](const auto a, const auto& b) { + const auto [trigger, actions] = gr::timing::TimingSource::splitTriggerActions(a); + const auto aa = gr::timing::TimingSource::parseTriggerAction(actions); + expect(std::ranges::equal(aa, b)) << fmt::format("checking: {}\ngot: {}\nexp: {}\n", a, aa, b); + }; + + expectTrigger("SIS100_RING->DIO1(20,on,5,off),DIO5(5,off)", // + std::vector{std::pair("DIO1"s, std::vector{std::pair(20ul, "on"s), std::pair(5ul, "off"s)}), // + std::pair("DIO5"s, std::vector{std::pair(5ul, "off"s)})}); + expectTrigger("SIS100_RING:CMD_BP_START:BEAM-IN=1:BPC-START=0:0:2->IO1(20000,on,30000,off),IO2(5000,on,6000,off)", // + std::vector{std::pair("IO1"s, std::vector{std::pair(20000ul, "on"s), std::pair(30000ul, "off"s)}), // + std::pair("IO2"s, std::vector{std::pair(5000ul, "on"s), std::pair{6000ul, "off"s}})}); + expectTrigger("301:256->IO3(10,on,11,off)", std::vector{std::pair("IO3"s, std::vector{std::pair(10ul, "on"s), std::pair(11ul, "off"s)})}); + expectTrigger("301:256->PUBLISH()", std::vector{std::pair("PUBLISH"s, std::vector>())}); + expect(throws([]() { gr::timing::TimingSource::splitTriggerActions("RING:EVENT--getOutputs() | std::views::transform([&timing](auto kvp) { + auto [name, path] = kvp; + auto result = saftlib::Output_Proxy::create(path, timing.saftSigGroup); + result->setOutputEnable(true); + result->WriteOutput(false); + return result; + }) | std::ranges::to(); + std::uint64_t outputState = 0x0ull; + std::uint8_t i = 0; + auto start = timing.currentTimeTAI() + schedule_offset; + std::this_thread::sleep_for(50ms); + while (i < n) { + while (start + i * schedule_dt < timing.currentTimeTAI() + schedule_offset) { + // publish event + Timing::Event ev; + ev.gid = (i % 2 == 0) ? 310 : 301; + ev.eventNo = (i % 3 == 0) ? 256 : 16; + ev.bpid = i; + ev.flagBeamin = true; + ev.time = i * schedule_dt; + timing.injectEvent(ev, start); + // change output level for all IOs except IO1 which will be triggered by events + for (std::size_t j = 0; j < outputs.size() - 1; j++) { + if ((outputState & (0x1ull << (j + 1))) != ((outputState + 1) & (0x1ull << (j + 1)))) { + const bool state = ((outputState + 1) & (0x1ull << (j + 1))) > 0; + outputs[j + 1]->WriteOutput(state); + // fmt::print("iteration {}, setting output {}({}) to {}\n", i, j + 1, outputs[j + 1]->getInput(), state); + } + } + outputState = (outputState + 1) & ((0x1ull << (outputs.size() - 1)) - 1); + + i++; + if (i >= n) { + break; + } + } + timing.saftSigGroup.wait_for_signal(1); + } + fmt::print("finished replay of timing data\n"); + }); +} + +const suite TimingBlock = [] { + tag("timing-hardware") / "test_events_and_samples"_test = [] { + using namespace gr; + using namespace gr::testing; + + // publish timing events from a separate forked process because saftlib does not like us to spawn multiple threads + const bool verbose = true; + constexpr float sample_rate = 250.f; + Graph testGraph; + auto& timingSrc = testGraph.emplaceBlock({ + {"event_actions", std::vector({"SIS100_RING:CMD_BP_START:BEAM-IN=1:BPC-START=0:0->IO1(400,on,8000,off)", // + "301:256->IO1(100,on,110,off,140,on,150,off)", // + "SIS100_RING:CMD_BP_START->PUBLISH()"})}, // + {"sample_rate", sample_rate}, // + {"verbose_console", verbose} // + }); + auto& sink = testGraph.emplaceBlock>({{"name", "TagSink"}, {"verbose_console", verbose}}); + + expect(eq(ConnectionResult::SUCCESS, testGraph.connect<"out">(timingSrc).to<"in">(sink))); + + scheduler::Simple sched{std::move(testGraph)}; + fmt::print("starting flowgraph\n"); + expect(sched.changeStateTo(gr::lifecycle::State::INITIALISED).has_value()); + auto res = sched.changeStateTo(gr::lifecycle::State::RUNNING); + if (!res) { + fmt::print("failed to start: {}\n", res.error()); + } + expect(res.has_value()); + + std::thread testEventDispatcherThread = dispatchSimulatedTiming(); + std::this_thread::sleep_for(6s); + expect(sched.changeStateTo(gr::lifecycle::State::REQUESTED_STOP).has_value()); + fmt::print("stopped flowgraph\n"); + + testEventDispatcherThread.join(); + + expect(approx(sink._samples.size(), 1500UZ, 100UZ)) << "samples do not approximately correspond to the configured sample rate"; + expect(approx(sink._tags.size(), 50UZ, 10UZ)) << "expected approximately 50 tags"; + expect(std::ranges::equal(sink._tags | std::views::filter([](auto& tag) { return tag.map.contains("BPID"); }) | std::views::transform([](auto& tag) { return std::get(tag.map["BPID"]); }), std::array{0, 6, 12, 18, 24})) << "Did not receive the correct timing tags with the correct BP indices"; + + expect(std::ranges::equal(sink._samples | std::views::transform([](auto v) { return v >> 2; }) // drop irrelevant bits (LSB: tagPresent, LSB+1:output which toggles on timing events) + | std::views::chunk_by(std::equal_to{}) // merge and count identical samples + | std::views::transform([](auto subrange) { return std::make_pair(subrange[0], subrange.size()); }) // get sample value and count as a pair + | std::views::filter([](auto pair) { return pair.second > 95UL && pair.second < 105UL; }) // output changes are 200ms * 2 * 250S/s = 100 Samples + | std::views::transform([](auto pair) { return pair.first; }), // get sample values + std::vector{1, 2, 3, 0, 1, 2, 3, 0, 1, 2, 3, 0, 1, 2})) + << "expected almost 4 iterations of the pattern. the first sample is dropped, because it has less than 100 samples"; + + if (verbose) { + fmt::print("received{} timing tags and {} samples!\noutput: {}\n", sink._tags.size(), sink._samples.size(), sink._samples); + if (!sink._tags.empty()) { + const auto firstTimestamp = std::get(sink._tags[0].map[gr::tag::TRIGGER_TIME.shortKey()]); + for (auto& tag : sink._tags) { + fmt::print(" {} - {}s: {}\n", tag.index, (static_cast(std::get(tag.map[gr::tag::TRIGGER_TIME.shortKey()]) - firstTimestamp)) * 1e-9, tag.map); + } + } + } + }; + + tag("timing-hardware") / "test_events_only"_test = [] { + using namespace gr; + using namespace gr::testing; + + // publish timing events from a separate forked process because saftlib does not like us to spawn multiple threads + const bool verbose = true; + Graph testGraph; + auto& timingSrc = testGraph.emplaceBlock({ + {"sample_rate", 0.0f}, // + {"event_actions", std::vector({"SIS100_RING:CMD_BP_START:BEAM-IN=1:BPC-START=0:0->IO1(400,on,8000,off)", // + "301:256->IO1(100,on,110,off,140,on,150,off)", // + "SIS100_RING:CMD_BP_START->PUBLISH()"})}, // + {"verbose_console", verbose} // + }); + auto& sink = testGraph.emplaceBlock>({{"name", "TagSink"}, {"verbose_console", verbose}}); + + expect(eq(ConnectionResult::SUCCESS, testGraph.connect<"out">(timingSrc).to<"in">(sink))); + + scheduler::Simple sched{std::move(testGraph)}; + fmt::print("starting flowgraph\n"); + expect(sched.changeStateTo(gr::lifecycle::State::INITIALISED).has_value()); + auto res = sched.changeStateTo(gr::lifecycle::State::RUNNING); + if (!res && verbose) { + fmt::print("failed to start: {}\n", res.error()); + } + expect(res.has_value()); + + std::thread testEventDispatcherThread = dispatchSimulatedTiming(); + std::this_thread::sleep_for(6s); + + expect(sched.changeStateTo(gr::lifecycle::State::REQUESTED_STOP).has_value()); + fmt::print("stopped flowgraph\n"); + + testEventDispatcherThread.join(); + + expect(eq(sink._samples.size(), sink._tags.size())) << "For sample_rate=0.0f the number of samples and tags should be identical"; + expect(approx(sink._tags.size(), 60UZ, 10UZ)) << "Expected approximately 60 tags"; + expect(std::ranges::equal(sink._tags | std::views::filter([](auto& tag) { return tag.map.contains("BPID"); }) | std::views::transform([](auto& tag) { return std::get(tag.map["BPID"]); }), std::array{0, 6, 12, 18, 24})) << "Did not receive the correct timing tags with the correct BP indices"; + expect(approx( // + std::ranges::distance(std::views::zip(sink._samples, sink._tags) // + | std::views::filter([](auto pair) { return std::get<1>(pair).map.contains("IO-NAME") && (std::get<1>(pair).map.at("IO-NAME") == "IO2" || std::get<1>(pair).map.at("IO-NAME") == "IO3"); })), // + 24, 2)) + << "Wrong numnber of IO tags"; + expect(approx( // + std::ranges::distance(std::views::zip(sink._samples, sink._tags) // + | std::views::filter([](auto pair) { return std::get<1>(pair).map.contains("IO-NAME") && std::get<1>(pair).map.at("IO-NAME") == "IO1"; })), // + 30, 2)) + << "Wrong numnber of event triggered IO tags"; + + if (verbose) { + fmt::print("received{} timing tags and {} samples!\noutput: {}\n", sink._tags.size(), sink._samples.size(), sink._samples); + if (!sink._tags.empty()) { + const auto firstTimestamp = std::get(sink._tags[0].map[gr::tag::TRIGGER_TIME.shortKey()]); + for (auto& tag : sink._tags) { + fmt::print(" {} - {}s: {}\n", tag.index, (static_cast(std::get(tag.map[gr::tag::TRIGGER_TIME.shortKey()]) - firstTimestamp)) * 1e-9, tag.map); + } + } + } + }; +}; + +} // namespace fair::timing::test + +int main() { /* tests are statically executed */ }