A C++ toolbox repo until the pair uTL/dTL arives
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 

360 lines
15 KiB

  1. /*!
  2. * \file utils/sequencer.h
  3. * \brief
  4. * A terminal-like device communication automation tool
  5. *
  6. * \copyright Copyright (C) 2021 Christos Choutouridis <christos@choutouridis.net>
  7. *
  8. * <dl class=\"section copyright\"><dt>License</dt><dd>
  9. * The MIT License (MIT)
  10. *
  11. * Permission is hereby granted, free of charge, to any person obtaining a copy
  12. * of this software and associated documentation files (the "Software"), to deal
  13. * in the Software without restriction, including without limitation the rights
  14. * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
  15. * copies of the Software, and to permit persons to whom the Software is
  16. * furnished to do so, subject to the following conditions:
  17. *
  18. * The above copyright notice and this permission notice shall be included in all
  19. * copies or substantial portions of the Software.
  20. *
  21. * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
  22. * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
  23. * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
  24. * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
  25. * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
  26. * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
  27. * SOFTWARE.
  28. * </dd></dl>
  29. */
  30. #ifndef TBX_UTILS_SEQUENCER_H_
  31. #define TBX_UTILS_SEQUENCER_H_
  32. #include <core/core.h>
  33. #include <core/crtp.h>
  34. #include <ctime>
  35. #include <array>
  36. #include <string_view>
  37. namespace tbx {
  38. /*!
  39. * \class sequencer_t
  40. * \brief
  41. * A CRTP base class to provide the sequencer functionality.
  42. *
  43. * Sequencer can automate communication with a terminal-like device such as AT-command modems etc...
  44. * It can operate based on a script array and handle the outgoing commands and incoming responses.
  45. * The user can create matching rules on received data and hook handlers and actions on them.
  46. *
  47. * The derived class (implementation) has to provide:
  48. * 1) size_t get(Data_t* data);
  49. * This function return 0 or a number of Data_t items. The data points to buffer for the input data.
  50. * 2) size_t put(const Data_t* data, size_t n);
  51. * This function sends to implementation the data pointed by \c data witch have size \c n.
  52. * 3) clock_t clock();
  53. * This function return a number to be used as time. The units of this function may be arbitrary but they
  54. * match the units in \c record_t::timeout field.
  55. *
  56. * \tparam Impl_t The type of derived class
  57. * \tparam Data_t The char-like stream item type. Usually \c char
  58. * \tparam N The size of the sequence buffer to temporary store each line from get().
  59. */
  60. template <typename Impl_t, typename Data_t, size_t N>
  61. class sequencer_t {
  62. _CRTP_IMPL(Impl_t);
  63. using str_view_t = std::basic_string_view<Data_t>;
  64. //! \name Public types
  65. //! @{
  66. public:
  67. //! \enum status_t
  68. //! \brief The sequencer run status
  69. enum class status_t {
  70. OK, ERROR
  71. };
  72. //! \enum action_t
  73. //! \brief Possible response actions for the sequencer
  74. enum class action_t {
  75. NO, NEXT, GOTO, EXIT_OK, EXIT_ERROR
  76. };
  77. //! \enum control_t
  78. //! \brief The control type of the script entry.
  79. enum class control_t {
  80. NOP, //!< No command, dont send or expect anything, used for delays
  81. SEND, //!< Send data to implementation
  82. EXPECT //!< Expects data from implementation
  83. };
  84. //! \enum match_t
  85. //! \brief Token match types
  86. enum class match_t {
  87. NO, STARTS_WITH, ENDS_WITH, CONTAINS, nSTARTS_WITH, nENDS_WITH, nCONTAINS
  88. };
  89. /*!
  90. * Match handler function pointer type.
  91. * Expects a pointer to buffer and a size and returns status
  92. */
  93. using handler_ft = status_t (*) (const Data_t*, size_t);
  94. /*!
  95. * \struct block_t
  96. * \brief
  97. * The script line block.
  98. *
  99. * Each script "line" contains up to 2 blocks for matching functionality. Each block
  100. * has a token and a matching type. If the response matches the token, the sequencer calls
  101. * the handler and perform the action.
  102. */
  103. struct block_t {
  104. std::basic_string_view<Data_t>
  105. token; //!< The token for the match
  106. match_t match_type; //!< The matching type functionality
  107. handler_ft handler; //!< The handler to called if the match is successful.
  108. action_t action; //!< The action to be performer if the match is successful
  109. size_t idx; //!< The index for the action_t::GOTO action. Otherwise can be left 0.
  110. };
  111. /*!
  112. * \struct record_t
  113. * \brief
  114. * Describes the sequencer's script record entry (line).
  115. *
  116. * Each line consist from a control, 2 blocks and a timeout. The control says if we send or receive data.
  117. * The blocks contain the data and the matching information. And the timeout guards the entire line.
  118. *
  119. * The user can create arrays as the example bellow to act as a script.
  120. * \code
  121. * const std::array<Seq::record_t, 8> script = {{
  122. * / * 0 * / {Seq::control_t::NOP, {"", Seq::match_t::NO, nullptr, Seq::action_t::GOTO, 1}, 1000}, //delay 1000 clocks
  123. * / * 1 * / {Seq::control_t::SEND, {"ATE0\r\n", Seq::match_t::NO, nullptr, Seq::action_t::NEXT, 0}, 1000},
  124. * / * 2 * / {Seq::control_t::EXPECT, {{
  125. * {"OK\r\n", Seq::match_t::ENDS_WITH, nullptr, Seq::action_t::NEXT, 0},
  126. * {"ERROR", Seq::match_t::CONTAINS, nullptr, Seq::action_t::EXIT_ERROR, 0} }},
  127. * 1000
  128. * },
  129. * // ...
  130. * }};
  131. * \endcode
  132. */
  133. struct record_t {
  134. control_t control; //!< The type of the entry
  135. std::array<block_t, 2> block; //!< The matching block
  136. clock_t timeout; //!< Timeout in CPU time
  137. };
  138. //! @}
  139. //! \name Constructor / Destructor
  140. //!@{
  141. protected:
  142. ~sequencer_t () = default; //!< \brief Allow destructor from derived only
  143. sequencer_t () = default; //!< \brief A default constructor from derived only
  144. sequencer_t(const sequencer_t&) = delete; //!< No copies
  145. sequencer_t& operator= (const sequencer_t&) = delete; //!< No copy assignments
  146. //!@}
  147. //! \name Sequencer interface requirements for implementer
  148. //! @{
  149. private:
  150. size_t get_ (Data_t* data) { return impl().get (data); }
  151. size_t put_ (const Data_t* data, size_t n) { return impl().put (data, n); }
  152. clock_t clock_ () { return impl().clock(); }
  153. //! @}
  154. //! \name Private functionality
  155. //! @{
  156. private:
  157. /*!
  158. * \brief
  159. * Check if the \c stream starts with the \c prefix
  160. * \param stream The stream in witch we search
  161. * \param prefix What we search
  162. * \return True on success, false otherwise
  163. */
  164. bool starts_with_ (const str_view_t stream, const str_view_t prefix) {
  165. return (stream.rfind(prefix, 0) != str_view_t::npos);
  166. }
  167. /*!
  168. * \brief
  169. * Check if the \c stream ends with the \c postfix
  170. * \param stream The stream in witch we search
  171. * \param postfix What we search
  172. * \return True on success, false otherwise
  173. */
  174. bool ends_with_ (const str_view_t stream, const str_view_t postfix) {
  175. return (
  176. stream.compare(
  177. stream.size() - postfix.size(),
  178. postfix.size(),
  179. postfix) == 0
  180. );
  181. }
  182. /*!
  183. * \brief
  184. * Check if the \c haystack contains the \c needle
  185. * \param haystack The stream in witch we search
  186. * \param needle What we search
  187. * \return True on success, false otherwise
  188. */
  189. bool contains_ (const str_view_t haystack, const str_view_t needle) {
  190. return (haystack.find(needle) != str_view_t::npos);
  191. }
  192. /*!
  193. * \brief
  194. * Return the new sequencer's step value.
  195. *
  196. * Step is index to the sequencer's script array.
  197. *
  198. * \param current_idx The current step value
  199. * \param action The advancing type
  200. * \param go_idx The new value of the step in the case of GOTO type
  201. * \return The new sequencer's step value
  202. */
  203. size_t step_ (size_t current_idx, action_t action, size_t go_idx =0) {
  204. switch (action) {
  205. default:
  206. case action_t::NO: return current_idx;
  207. case action_t::NEXT: return ++current_idx;
  208. case action_t::GOTO: return go_idx;
  209. case action_t::EXIT_OK:
  210. case action_t::EXIT_ERROR:
  211. return 0;
  212. }
  213. }
  214. /*!
  215. * \brief
  216. * Checks if the \c needle matches the \c haystack.
  217. *
  218. * \param type The type of matching functionality
  219. * \param haystack The stream in witch we search
  220. * \param needle The stream we search
  221. * \return True on match
  222. */
  223. bool match_(match_t type, const str_view_t haystack, const str_view_t needle) {
  224. switch (type) {
  225. default:
  226. case match_t::NO: return true;
  227. case match_t::STARTS_WITH: return starts_with_(haystack, needle);
  228. case match_t::ENDS_WITH: return ends_with_(haystack, needle);
  229. case match_t::CONTAINS: return contains_(haystack, needle);
  230. case match_t::nSTARTS_WITH: return !starts_with_(haystack, needle);
  231. case match_t::nENDS_WITH: return !ends_with_(haystack, needle);
  232. case match_t::nCONTAINS: return !contains_(haystack, needle);
  233. }
  234. }
  235. //! @}
  236. public:
  237. /*!
  238. * \brief
  239. * Run the script array
  240. *
  241. * The main sequencer functionality. It starts with the first entry of the array.
  242. * - If the entry is \c NOP it executes the action after the timeout.
  243. * \c token and \c handler are discarded.
  244. * - If the entry is \c SEND it uses the first block's token to send and executes the action after that.
  245. * \c timeout is discarded.
  246. * - If the entry is \c EXCEPTS it continuously try to receive data using implementation's get until one
  247. * of the blocks match.
  248. * On match:
  249. * - Calls the handler if there is one
  250. * - Executes the action
  251. * - Skips the next block if there is one
  252. * If there is no match on timeout it return status_t::EXIT_ERROR
  253. *
  254. * \tparam Steps The number of steps of the script
  255. * \param script Reference to script to run
  256. * \return The status of entire operation as described above
  257. */
  258. template <size_t Steps>
  259. status_t run (const std::array<record_t, Steps>& script) {
  260. Data_t buffer[N];
  261. size_t resp_size;
  262. clock_t mark = clock_();
  263. for (size_t step =0, p_step =0 ; step < Steps ; ) {
  264. const record_t& it = script[step];
  265. if (step != p_step) {
  266. p_step = step;
  267. mark = clock_();
  268. }
  269. switch (it.control) {
  270. default:
  271. case control_t::NOP:
  272. if ((clock_() - mark) >= it.timeout) {
  273. switch (it.block[0].action) {
  274. case action_t::EXIT_OK: return status_t::OK;
  275. case action_t::EXIT_ERROR: return status_t::ERROR;
  276. default:
  277. step = step_(step, it.block[0].action, it.block[0].idx);
  278. break;
  279. }
  280. }
  281. break;
  282. case control_t::SEND:
  283. put_(it.block[0].token.data(), it.block[0].token.size());
  284. switch (it.block[0].action) {
  285. case action_t::EXIT_OK: return status_t::OK;
  286. case action_t::EXIT_ERROR: return status_t::ERROR;
  287. default:
  288. step = step_(step, it.block[0].action, it.block[0].idx);
  289. break;
  290. }
  291. break;
  292. case control_t::EXPECT:
  293. resp_size = get_(buffer);
  294. if (resp_size) {
  295. for (auto& block : it.block) {
  296. if (match_(block.match_type, buffer, block.token)) {
  297. if (block.handler != nullptr)
  298. block.handler(buffer, resp_size);
  299. switch (block.action) {
  300. case action_t::EXIT_OK: return status_t::OK;
  301. case action_t::EXIT_ERROR: return status_t::ERROR;
  302. default:
  303. step = step_(step, block.action, block.idx);
  304. break;
  305. }
  306. break;
  307. }
  308. }
  309. }
  310. if ((clock_() - mark) >= it.timeout)
  311. return status_t::ERROR;
  312. break;
  313. } // switch (it.control)
  314. }
  315. return status_t::OK;
  316. }
  317. };
  318. /*!
  319. * An "empty" block for convenience.
  320. */
  321. template <typename Impl_t, typename Data_t, size_t N>
  322. constexpr typename sequencer_t<Impl_t, Data_t, N>::block_t Sequencer_null_block = {
  323. "",
  324. sequencer_t<Impl_t, Data_t, N>::match_t::NO,
  325. nullptr,
  326. sequencer_t<Impl_t, Data_t, N>::action_t::NO,
  327. 0
  328. };
  329. }
  330. #endif /* TBX_UTILS_SEQUENCER_H_ */