ChimeraTK-DeviceAccess 03.31.00
Loading...
Searching...
No Matches
chimeratk-device-to-jmap.cc
Go to the documentation of this file.
1// SPDX-FileCopyrightText: Deutsches Elektronen-Synchrotron DESY, MSK, ChimeraTK Project <chimeratk-support@desy.de>
2// SPDX-License-Identifier: LGPL-3.0-or-later
3
4#include "AccessMode.h"
5#include "BackendFactory.h"
7#include "DataDescriptor.h"
8#include "Device.h"
9#include "RegisterCatalogue.h"
10
11#include <nlohmann/json.hpp>
12
13#include <boost/program_options.hpp>
14
15#include <algorithm>
16#include <fstream>
17#include <iostream>
18#include <stdexcept>
19#include <string>
20#include <vector>
21
22namespace po = boost::program_options;
23using nlohmann::json;
24
25// Return the number of bytes per single element of the given data descriptor, using the configured fixed string length.
26size_t byteSizePerElement(const ChimeraTK::DataDescriptor& descriptor, size_t stringLength);
27
28// Build the jmap "representation" object from the generic data descriptor.
30
31// Return the jmap "access" string for a register from its read/write capability.
33
34// Insert a register entry into the addressSpace hierarchy, mirroring the module path via "children" objects.
35void insertRegister(json& children, const std::vector<std::string>& components, const json& entry);
36
37// Round a byte size up to a multiple of 4 (the dummy minimum transfer alignment).
38size_t padToAlignment(size_t size);
39
40/**********************************************************************************************************************/
41
42int main(int argc, char* argv[]) {
43 po::options_description options("Usage: chimeratk-device-to-jmap [options]");
44 options.add_options()("help", "print this help text")("device", po::value<std::string>()->required(),
45 "device to read the register catalogue from, given as a ChimeraTK device descriptor (CDD) or an alias name")(
46 "dmap", po::value<std::string>(), "dmap file path used to resolve alias names; does not affect --device")(
47 "output", po::value<std::string>()->required(), "path of the jmap file to write (never written to stdout)")(
48 "string-length", po::value<size_t>(),
49 "fixed byte size per element assumed for ASCII string registers (default 80)");
50
51 po::variables_map variableMap;
52 try {
53 po::store(po::parse_command_line(argc, argv, options), variableMap);
54 }
55 catch(const po::error& error) {
56 std::cerr << "Error: " << error.what() << "\n\n";
57 std::cerr << options << "\n";
58 return 1;
59 }
60
61 if(variableMap.count("help")) {
62 std::cout << options << "\n";
63 std::cout << "Limitations:\n";
64 std::cout << " - Two-dimensional registers are skipped (a single warning lists all affected registers).\n";
65 std::cout << " - Writable registers with AccessMode::wait_for_new_data become read-only interrupts (a single "
66 "warning lists all affected registers).\n";
67 std::cout << " - Addresses in the output are synthetic.\n";
68 std::cout << " - ASCII strings are written with a fixed length of --string-length bytes per element "
69 "(default 80).\n";
70 return 0;
71 }
72
73 try {
74 po::notify(variableMap);
75 }
76 catch(const po::error& error) {
77 std::cerr << "Error: " << error.what() << "\n\n";
78 std::cerr << options << "\n";
79 return 1;
80 }
81
82 if(variableMap.count("dmap")) {
83 ChimeraTK::BackendFactory::getInstance().setDMapFilePath(variableMap["dmap"].as<std::string>());
84 }
85
86 std::string deviceString = variableMap["device"].as<std::string>();
87 std::string outputPath = variableMap["output"].as<std::string>();
88 size_t stringLength = 80;
89 if(variableMap.count("string-length")) {
90 stringLength = variableMap["string-length"].as<size_t>();
91 }
92
94 // Opening the device can throw a ChimeraTK::logic_error (e.g. for an unknown backend or an invalid descriptor).
95 // Report it instead of terminating via an unhandled exception in main.
96 try {
97 device.open(deviceString);
98 }
99 catch(const ChimeraTK::logic_error& error) {
100 std::cerr << "Error: " << error.what() << "\n";
101 return 1;
102 }
103 auto catalogue = device.getRegisterCatalogue();
104
105 // Collect the registers and sort them alphabetically by full path for deterministic output.
106 std::vector<const ChimeraTK::BackendRegisterInfoBase*> registers;
107 registers.reserve(catalogue.getNumberOfRegisters());
108 for(const auto& registerInfo : catalogue) {
109 registers.push_back(&registerInfo);
110 }
111 std::ranges::sort(
112 registers, [](const ChimeraTK::BackendRegisterInfoBase* left, const ChimeraTK::BackendRegisterInfoBase* right) {
113 return left->getRegisterName() < right->getRegisterName();
114 });
115
116 json addressSpace = json::object();
117 std::vector<std::string> twoDimensionalRegisters;
118 std::vector<std::string> writableInterruptRegisters;
119 size_t interruptId = 0;
120 size_t addressOffset = 0;
121
122 try {
123 for(const auto* registerInfo : registers) {
124 auto name = registerInfo->getRegisterName();
125 if(registerInfo->getNumberOfDimensions() > 1) {
126 twoDimensionalRegisters.push_back(name);
127 continue;
128 }
129
130 auto descriptor = registerInfo->getDataDescriptor();
131 bool isVoid = (descriptor.fundamentalType() == ChimeraTK::DataDescriptor::FundamentalType::nodata);
132 bool waitForNewData = registerInfo->getSupportedAccessModes().has(ChimeraTK::AccessMode::wait_for_new_data);
133
134 if(waitForNewData && registerInfo->isWriteable() && !isVoid) {
135 writableInterruptRegisters.push_back(name);
136 }
137
138 json entry = json::object();
139 if(isVoid) {
140 // Pure interrupt source: void representation, no address, no access field.
141 entry["triggeredByInterrupt"] = json::array({interruptId++});
142 entry["representation"] = makeRepresentation(descriptor);
143 }
144 else {
145 auto elementBytes = byteSizePerElement(descriptor, stringLength);
146 if(waitForNewData) {
147 entry["triggeredByInterrupt"] = json::array({interruptId++});
148 }
149 else {
150 entry["access"] = accessString(*registerInfo);
151 }
152 entry["representation"] = makeRepresentation(descriptor);
153 entry["numberOfElements"] = registerInfo->getNumberOfElements();
154 entry["bytesPerElement"] = elementBytes;
155 entry["address"] = json::object({{"type", "IO"}, {"channel", 0}, {"offset", addressOffset}});
156 addressOffset += padToAlignment(elementBytes * registerInfo->getNumberOfElements());
157 }
158
159 insertRegister(addressSpace, name.getComponents(), entry);
160 }
161 }
162 // A std::logic_error can only be thrown by insertRegister on an internal inconsistency (e.g. a duplicate register
163 // path or a conflicting key). Report it instead of terminating via an unhandled exception in main.
164 catch(const std::logic_error& error) {
165 std::cerr << "Error: " << error.what() << "\n";
166 return 1;
167 }
168
169 json output;
170 output["mapFormatVersion"] = "1.0";
171 output["metadata"] = json::object();
172 output["interruptHandler"] = json::object();
173 output["addressSpace"] = addressSpace;
174
175 if(!twoDimensionalRegisters.empty()) {
176 std::cerr << "Warning: the following two-dimensional registers are unsupported and were skipped:\n";
177 for(const auto& reg : twoDimensionalRegisters) {
178 std::cerr << " " << reg << "\n";
179 }
180 }
181 if(!writableInterruptRegisters.empty()) {
182 std::cerr << "Warning: the following writable registers with AccessMode::wait_for_new_data were written as "
183 "read-only interrupts:\n";
184 for(const auto& reg : writableInterruptRegisters) {
185 std::cerr << " " << reg << "\n";
186 }
187 }
188
189 std::ofstream outputFile(outputPath);
190 if(!outputFile.is_open()) {
191 std::cerr << "Error: could not open output file '" << outputPath << "' for writing.\n";
192 return 1;
193 }
194 // Pretty-print with two-space indentation for human readability. Keys are already sorted because nlohmann::json
195 // stores objects in a std::map, and floats are serialised with round-trip precision by default, so output is
196 // deterministic.
197 outputFile << output.dump(2) << "\n";
198 return 0;
199}
200
201/**********************************************************************************************************************/
202
209size_t byteSizePerElement(const ChimeraTK::DataDescriptor& descriptor, size_t stringLength) {
210 switch(descriptor.fundamentalType()) {
212 return descriptor.minimumDataType().getNumberOfBytes();
214 // Boolean registers are written as 4-byte fixed-point values with a width of 1 (see makeRepresentation), i.e.
215 // the element size is fixed at 4 bytes rather than derived from sizeof(ChimeraTK::Boolean).
216 return 4;
218 return stringLength;
221 return 0;
222 }
223 return 0;
224}
225
226/**********************************************************************************************************************/
227
230 json representation = json::object();
231 switch(descriptor.fundamentalType()) {
233 representation["type"] = "string";
234 break;
236 // Boolean registers are written as 4-byte fixed-point values with a width of 1.
237 representation["type"] = "fixedPoint";
238 representation["width"] = 1;
239 representation["fractionalBits"] = 0;
240 representation["isSigned"] = false;
241 break;
243 auto minimumType = descriptor.minimumDataType();
244 if(minimumType.isIntegral()) {
245 representation["type"] = "fixedPoint";
246 representation["width"] = static_cast<unsigned>(minimumType.getNumberOfBytes()) * 8;
247 representation["fractionalBits"] = 0;
248 representation["isSigned"] = descriptor.isSigned();
249 }
250 else {
251 representation["type"] = "IEEE754";
252 representation["width"] = static_cast<unsigned>(minimumType.getNumberOfBytes()) * 8;
253 representation["isSigned"] = true;
254 }
255 break;
256 }
258 representation["type"] = "void";
259 break;
261 representation["type"] = "fixedPoint";
262 representation["width"] = 32;
263 representation["fractionalBits"] = 0;
264 representation["isSigned"] = descriptor.isSigned();
265 break;
266 }
267 return representation;
268}
269
270/**********************************************************************************************************************/
271
274 if(info.isReadable() && info.isWriteable()) {
275 return "RW";
276 }
277 if(info.isWriteable()) {
278 return "WO";
279 }
280 return "RO";
281}
282
283/**********************************************************************************************************************/
284
294void insertRegister(json& children, const std::vector<std::string>& components, const json& entry) {
295 auto& destination = children[components[0]];
296 if(components.size() > 1) {
297 // Module node: promote the destination to an object and ensure a "children" map for the remaining components. A
298 // pre-existing object (e.g. a register that also carries sub-registers) is extended in place by adding the
299 // "children" key; its old contents are never copied into a freshly built object.
300 if(!destination.is_object()) {
301 destination = json::object();
302 }
303 if(!destination.contains("children")) {
304 destination["children"] = json::object();
305 }
306 std::vector<std::string> rest(components.begin() + 1, components.end());
307 insertRegister(destination["children"], rest, entry);
308 return;
309 }
310 // Leaf node: merge the register fields. An overwrite of an existing key would indicate an internal inconsistency
311 // (e.g. a duplicate register path), so an existing object is never silently replaced.
312 for(const auto& [key, value] : entry.items()) {
313 if(destination.contains(key)) {
314 throw std::logic_error("Internal inconsistency: register path '" + entry.dump() +
315 "' conflicts with existing "
316 "key '" +
317 key + "'.");
318 }
319 destination[key] = value;
320 }
321}
322
323/**********************************************************************************************************************/
324
326size_t padToAlignment(size_t size) {
327 return ((size + 3) / 4) * 4;
328}
329
330/**********************************************************************************************************************/
int main()
nlohmann::json json
size_t byteSizePerElement(const ChimeraTK::DataDescriptor &descriptor, size_t stringLength)
Return the number of bytes per single element of the given data descriptor.
std::string accessString(const ChimeraTK::BackendRegisterInfoBase &info)
Return the jmap "access" string for a register from its read/write capability.
json makeRepresentation(const ChimeraTK::DataDescriptor &descriptor)
Build the jmap "representation" object from the generic data descriptor.
size_t padToAlignment(size_t size)
Round a byte size up to a multiple of 4 (the dummy minimum transfer alignment).
void insertRegister(json &children, const std::vector< std::string > &components, const json &entry)
Insert a register entry into the addressSpace hierarchy, mirroring the module path via "children" obj...
static BackendFactory & getInstance()
Static function to get an instance of factory.
void setDMapFilePath(std::string dMapFilePath)
This function sets the _DMapFilePath.
DeviceBackend-independent register description.
virtual bool isReadable() const =0
Return whether the register is readable.
virtual RegisterPath getRegisterName() const =0
Return full path name of the register (including modules)
virtual bool isWriteable() const =0
Return whether the register is writeable.
Class describing the actual payload data format of a register in an abstract manner.
FundamentalType fundamentalType() const
Get the fundamental data type.
DataType minimumDataType() const
Get the minimum data type required to represent the described data type in the host CPU.
bool isSigned() const
Return whether the data is signed or not.
size_t getNumberOfBytes() const
Return the number of bytes per element for the data type.
Class allows to read/write registers from device.
Definition Device.h:39
RegisterCatalogue getRegisterCatalogue() const
Return the register catalogue with detailed information on all registers.
Definition Device.cc:22
void open(std::string const &aliasName)
Open a device by the given alias name from the DMAP file.
Definition Device.cc:58
Exception thrown when a logic error has occured.
Definition Exception.h:51
@ wait_for_new_data
Make any read blocking until new data has arrived since the last read.
ctk::Device device