ifw-daq  1.0.0
IFW Data Acquisition modules
fitsController.cpp
Go to the documentation of this file.
1 /**
2  * @file
3  * @ingroup daq_ocm_libdaq
4  * @copyright 2021 ESO - European Southern Observatory
5  *
6  * @brief Contains declaration for for FitsControllerImpl
7  */
8 #include <daq/fitsController.hpp>
9 
10 #include <cstring>
11 #include <array>
12 #include <algorithm>
13 
14 #include <fitsio.h>
15 #include <fmt/format.h>
16 #include <fmt/ostream.h>
17 #include <log4cplus/loggingmacros.h>
18 #include <boost/filesystem.hpp>
19 
20 namespace {
21 
22 template <class>
23 inline constexpr bool always_false_v = false; // NOLINT
24 
25 /**
26  * Primary template for FITS value keyword formatter.
27  * It will truncate
28  */
29 template<class T>
30 struct FitsTraits;
31 
32 template<>
33 struct FitsTraits<std::string>{
34  static constexpr int DATA_TYPE = TSTRING;
35 };
36 template<>
37 struct FitsTraits<int64_t>{
38  static constexpr int DATA_TYPE = TLONGLONG;
39 };
40 template<>
41 struct FitsTraits<uint64_t>{
42  // @note ELTDev has an old cfitsio that does not support TULONGLONG
43  static constexpr int DATA_TYPE = TULONG;
44 };
45 template<>
46 struct FitsTraits<double>{
47  static constexpr int DATA_TYPE = TDOUBLE;
48 };
49 template<>
50 struct FitsTraits<bool>{
51  static constexpr int DATA_TYPE = TLOGICAL;
52 };
53 
54 /**
55  * Primitive formatting of EsoKeyword into a 80 character record
56  * This will accept invalid formatting and is a temporary solution until
57  * the dictionary system provides proper formatting.
58  */
59 void FormatEsoKeyword(daq::fits::EsoKeyword const& kw, std::array<char, FLEN_CARD> &record) {
60  std::visit(
61  [&](auto const& value) {
62  using T = std::decay_t<decltype(value)>;
63  std::string res;
64  if constexpr (std::is_same_v<T, std::string>) {
65  // New version of fmt support format_to_n which is better.
66  res = fmt::format("HIERARCH ESO {} = '{}' / {}", kw.name, value,
67  kw.comment.value_or(std::string()));
68  } else if constexpr (std::is_same_v<T, bool>) {
69  // New version of fmt support format_to_n which is better.
70  res = fmt::format("HIERARCH ESO {} = {} / {}", kw.name, value ? 'T' : 'F',
71  kw.comment.value_or(std::string()));
72  } else {
73  // New version of fmt support format_to_n which is better.
74  res = fmt::format("HIERARCH ESO {} = {} / {}", kw.name, value,
75  kw.comment.value_or(std::string()));
76  }
77  std::strncpy(record.data(), res.data(), record.size());
78  record.back() = '\0';
79  },
80  kw.value);
81 }
82 
83 } // namespace {}
84 
85 namespace daq {
86 
87 std::ostream& operator<<(std::ostream& os, FitsController const& ctl) {
88  os << "FitsController(id='" << ctl.GetId() << "', state=" << ctl.GetState() << ")";
89  return os;
90 }
91 
93  std::shared_ptr<ObservableEventLog> event_log,
94  std::function<fits::UniqueFitsFile(char const*)> fits_create)
95  : m_id(properties.id)
96  , m_out_path(properties.ocm_dppart_root)
97  , m_event_log(std::move(event_log))
98  , m_fits_create(std::move(fits_create))
99  , m_state(NotStarted{})
100  , m_keywords()
101  , m_comments()
102  , m_file_path()
103  , m_file(nullptr, fits::DefaultClose)
104  , m_result(properties.process_name.empty() ? "OCM" : properties.process_name, "")
105  , m_logger(log4cplus::Logger::getInstance("daq")) {
106  using boost::filesystem::absolute;
107  using boost::filesystem::path;
108  assert(m_event_log);
109  assert(m_fits_create);
110  LOG4CPLUS_DEBUG(m_logger, fmt::format("{}: Settings: out-path='{}', prefix='{}', ", *this,
111  m_out_path, properties.dp_name_prefix));
112 
113  char hostname[HOST_NAME_MAX + 1];
114  if (gethostname(&hostname[0], HOST_NAME_MAX + 1) != 0) {
115  throw std::runtime_error("failed to get hostname");
116  }
117  m_hostname.append(&hostname[0]);
118 
119  // Create target filename
120  auto file_path = absolute(path(m_out_path));
121  m_out_path = file_path.native();
122  file_path /= (properties.dp_name_prefix + m_id + "_ocm.fits");
123  m_file_path = absolute(file_path).native();
124  LOG4CPLUS_INFO(m_logger, fmt::format("{}: OCM target FITS file is '{}'", *this, m_file_path));
125  m_result.info = fmt::format("@{}:{}", m_hostname, m_file_path);
126 
127  // @todo set default keywords
128 }
129 
131  LOG4CPLUS_DEBUG(m_logger, fmt::format("{}: Start()", *this));
132  using boost::filesystem::absolute;
133  using boost::filesystem::path;
134 
135  if (!std::holds_alternative<NotStarted>(m_state)) {
136  auto msg = fmt::format("{}: Data Acquisition has already been started", *this);
137  LOG4CPLUS_ERROR(m_logger, msg);
138  throw std::runtime_error(msg);
139  }
140 
141  // Create FITS file and primary HDU
142  m_file = m_fits_create(m_file_path.c_str());
143  assert(m_file);
144  // Create primary HDU as an img with 0 axis, mearly to hold keywords
145  fits::InitPrimaryHduNoImage(m_file.get());
146 
147  m_state.emplace<Acquiring>();
148 }
149 
150 std::optional<DpPart> FitsControllerImpl::Stop(ErrorPolicy policy) {
151  LOG4CPLUS_DEBUG(m_logger, fmt::format("{}: Stop()", *this));
152  if (!std::holds_alternative<Acquiring>(m_state)) {
153  auto msg = fmt::format("{}: Data Acquisition is not acquiring", *this);
154  LOG4CPLUS_ERROR(m_logger, msg);
155  throw std::runtime_error(msg);
156  }
157  try {
158  WriteFitsfile();
159 
160  // On success, close FITS file
161  m_file.reset();
162  m_state.emplace<Stopped>();
163  // Close fits file
164  LOG4CPLUS_DEBUG(m_logger, fmt::format("{}: Stop() completed successfully: FITS file='{}'",
165  *this, m_result));
166  } catch (std::exception const& e) {
167  auto msg = fmt::format("{}: Writing to FITS '{}' failed", *this, m_file_path);
168  LOG4CPLUS_ERROR(m_logger, msg);
169  if (policy == ErrorPolicy::Strict) {
170  throw;
171  }
172  }
173 
174  return m_result;
175 }
176 
178  LOG4CPLUS_INFO(m_logger, fmt::format("{}: Abort()", *this));
179  if (std::holds_alternative<NotStarted>(m_state)) {
180  LOG4CPLUS_INFO(m_logger, fmt::format("{}: Aborting not started data acquisition", *this));
181  m_state.emplace<Aborted>();
182  return;
183  }
184 
185  if (!std::holds_alternative<Acquiring>(m_state)) {
186  auto msg = fmt::format("{}: Data Acquisition is not acquiring", *this);
187  LOG4CPLUS_ERROR(m_logger, msg);
188  throw std::runtime_error(msg);
189  }
190 
191  // Release FITS file
192  m_file.reset();
193  boost::filesystem::remove(m_file_path);
194 
195  m_state.emplace<Aborted>();
196  return;
197 }
198 
199 void FitsControllerImpl::UpdateKeywords(std::vector<fits::KeywordVariant> const& keywords) {
200  if (std::holds_alternative<Stopped>(m_state) || std::holds_alternative<Aborted>(m_state)) {
201  auto msg = fmt::format("{}: Cannot update keywords when Data Acquisition has already "
202  "completed", *this);
203  LOG4CPLUS_ERROR(m_logger, msg);
204  throw std::runtime_error(msg);
205  }
206  fits::UpdateKeywords(m_keywords, keywords);
207 }
208 
209 void FitsControllerImpl::AddComment(std::string comment) {
210  if (std::holds_alternative<Stopped>(m_state) || std::holds_alternative<Aborted>(m_state)) {
211  auto msg = fmt::format("{}: Cannot add comment when Data Acquisition has already "
212  "completed", *this);
213  LOG4CPLUS_ERROR(m_logger, msg);
214  throw std::runtime_error(msg);
215  }
216  m_comments.emplace_back(std::move(comment));
217 }
218 
219 std::string const& FitsControllerImpl::GetId() const DAQ_NOEXCEPT {
220  return m_id;
221 }
222 
223 std::optional<DpPart> FitsControllerImpl::GetResult() const DAQ_NOEXCEPT {
224  // Result is only valid once successfully stopped
225  if (std::holds_alternative<Stopped>(m_state)) {
226  return m_result;
227  }
228  return {};
229 }
230 
232  State s;
233  std::visit(
234  [&](auto const& var) {
235  using T = std::decay_t<decltype(var)>;
236  if constexpr (std::is_same_v<T, NotStarted>) {
237  s = State::NotStarted;
238  } else if constexpr (std::is_same_v<T, Acquiring>) {
239  s = State::Acquiring;
240  } else if constexpr (std::is_same_v<T, Stopped>) {
241  s = State::Stopped;
242  } else if constexpr (std::is_same_v<T, Aborted>) {
243  s = State::Aborted;
244  } else {
245  static_assert(always_false_v<T>, "non-exhaustive visitor!");
246  }
247  },
248  m_state);
249  return s;
250 }
251 
252 
253 void FitsControllerImpl::WriteFitsfile() {
254 
255  // Sort keywords
256  std::stable_sort(m_keywords.begin(), m_keywords.end());
257  // @todo Write to fits file using dictionary system formatting
258  for (auto& kw : m_keywords) {
259  std::visit(
260  [&](auto& var) {
261  using T = std::decay_t<decltype(var)>;
262  if constexpr (std::is_same_v<T, fits::ValueKeyword>) {
263  std::visit(
264  [&](auto& value) {
265  using T = std::decay_t<decltype(value)>;
266  int err = 0;
267  if constexpr (std::is_same_v<T, std::string>) {
268  // special handling for string
269  (void)fits_write_key(m_file.get(),
270  FitsTraits<T>::DATA_TYPE,
271  var.name.c_str(),
272  const_cast<char*>(value.c_str()),
273  (var.comment.has_value() ?
274  var.comment.value().c_str() : nullptr),
275  &err);
276  } else if constexpr (std::is_same_v<T, bool>) {
277  // special handling for bool
278  // in the definition of TLOGICAL it says logicals are 'int' for
279  // keywords
280  int logical = value;
281  (void)fits_write_key(m_file.get(),
282  FitsTraits<T>::DATA_TYPE,
283  var.name.c_str(),
284  &logical,
285  (var.comment.has_value() ?
286  var.comment.value().c_str() : nullptr),
287  &err);
288  } else {
289  (void)fits_write_key(m_file.get(),
290  FitsTraits<T>::DATA_TYPE,
291  var.name.c_str(),
292  &value,
293  (var.comment.has_value() ?
294  var.comment.value().c_str() : nullptr),
295  &err);
296  }
297  if (err != 0) {
298  char err_text[31]; // returns maximum 30 characters
299  fits_get_errstatus(err, &err_text[0]);
300  err_text[30] = '\0';
301  throw std::runtime_error(fmt::format("failed to write key: {}",
302  err_text));
303  }
304  },
305  var.value);
306  } else if constexpr (std::is_same_v<T, fits::EsoKeyword>) {
307  std::array<char, FLEN_CARD> record{};
308  FormatEsoKeyword(var, record);
309  int err = 0;
310  (void)fits_write_record(m_file.get(),
311  record.data(),
312  &err);
313  if (err != 0) {
314  char err_text[31]; // returns maximum 30 characters
315  fits_get_errstatus(err, &err_text[0]);
316  err_text[30] = '\0';
317  throw std::runtime_error(fmt::format("failed to write record '{:.80s}': {}",
318  record.data(),
319  err_text));
320  }
321  } else {
322  static_assert(always_false_v<T>, "non-exhaustive visitor!");
323  }
324  },
325  kw);
326  }
327 }
328 
329 } // namespace daq
DAQ_NOEXCEPT
#define DAQ_NOEXCEPT
Definition: config.hpp:16
daq::FitsControllerImpl::Abort
void Abort(ErrorPolicy policy) override
Aborts and deletes FITS file.
Definition: fitsController.cpp:177
daq::State
State
Observable states of the data acquisition process.
Definition: state.hpp:41
daq::DaqProperties::dp_name_prefix
std::string dp_name_prefix
Data product file name prefix.
Definition: daqProperties.hpp:44
daq::FitsControllerImpl::FitsControllerImpl
FitsControllerImpl(DaqProperties const &properties, std::shared_ptr< ObservableEventLog > event_log, std::function< fits::UniqueFitsFile(char const *)> fits_create=&fits::CreateEmpty)
Definition: fitsController.cpp:92
daq::DaqProperties
Structure carrying properties needed to start a DataAcquisition.
Definition: daqProperties.hpp:28
daq::fits::UpdateKeywords
void UpdateKeywords(KeywordVector &to, KeywordVector const &from)
Updates a with keywords from b.
Definition: keyword.cpp:120
daq
Definition: daqController.cpp:18
daq::FitsController::GetId
virtual std::string const & GetId() const DAQ_NOEXCEPT=0
Query FITS file path.
daq::FitsControllerImpl::GetId
std::string const & GetId() const DAQ_NOEXCEPT override
Query FITS file path.
Definition: fitsController.cpp:219
daq::fits::BasicKeyword
A type safe version of FormattedKeyword that consist of the three basic components of a FITS keyword ...
Definition: keyword.hpp:51
daq::FitsController::GetState
virtual State GetState() const DAQ_NOEXCEPT=0
Query state.
daq::fits::UniqueFitsFile
std::unique_ptr< fitsfile, void(*)(fitsfile *) noexcept > UniqueFitsFile
Defines unique ownership type to cfitsio fitsfile.
Definition: cfitsio.hpp:46
daq::fits::BasicKeyword::comment
std::optional< std::string > comment
Definition: keyword.hpp:81
fitsController.hpp
Contains declaration for for FitsController.
daq::FitsControllerImpl::UpdateKeywords
void UpdateKeywords(std::vector< fits::KeywordVariant >const &) override
Updates with provided keywords.
Definition: fitsController.cpp:199
daq::DaqProperties::process_name
std::string process_name
User defined process name.
Definition: daqProperties.hpp:39
daq::fits::BasicKeyword::value
ValueType value
Definition: keyword.hpp:80
daq::operator<<
std::ostream & operator<<(std::ostream &os, DaqController const &daq)
Definition: daqController.cpp:49
daq::FitsController
Create FITS file containing keywords from OCM for the Data Acquisition.
Definition: fitsController.hpp:58
daq::FitsControllerImpl::Stop
std::optional< DpPart > Stop(ErrorPolicy policy) override
Finalizes the FITS file.
Definition: fitsController.cpp:150
daq::FitsControllerImpl::Start
void Start() override
Creates FITS file and pupulates it with initial list of keywords.
Definition: fitsController.cpp:130
daq::fits::InitPrimaryHduNoImage
void InitPrimaryHduNoImage(fitsfile *ptr)
Initializes an empty FITS file with a primary HDU.
Definition: cfitsio.cpp:35
daq::State::NotStarted
@ NotStarted
Initial state of data acquisition.
daq::ErrorPolicy
ErrorPolicy
Error policy supported by certain operations.
Definition: error.hpp:25
daq::fits::DefaultClose
void DefaultClose(fitsfile *ptr) noexcept
Default close function that is used by UniqueFitsFile as a deleter.
Definition: cfitsio.cpp:23
daq::FitsControllerImpl::GetState
State GetState() const DAQ_NOEXCEPT override
Query state.
Definition: fitsController.cpp:231
daq::FitsControllerImpl::AddComment
void AddComment(std::string comment) override
Add comment.
Definition: fitsController.cpp:209
daq::fits::BasicKeyword::name
std::string name
Definition: keyword.hpp:79
daq::FitsControllerImpl::GetResult
std::optional< DpPart > GetResult() const DAQ_NOEXCEPT override
Query FITS file path.
Definition: fitsController.cpp:223