#pragma once /* MOUNT CONTROL COMPONENTS LIBRARY */ /* BASIC NETWORK PROTOCOL DEFINITIONS */ #include #include #include "mcc_angle.h" #include "mcc_generics.h" #include "mcc_utils.h" namespace mcc::network { /* * The network protocol is the ASCII-based, case-sensitive textual protocol. * The "client-server" communication is performed through messages. * The message is a minimal unit of this communication. * The model of network communication is a simple "client-server" one, i.e., * client asks - server responds. * * network communication message format: * [[][][]...] * * where * - mandatory message keyword (one or more ASCII symbols) * * * e.g. * "TARGET 12:23:45.56 00:32:21.978\n" */ /* low-level network message format definitions */ static constexpr std::string_view MCC_COMMPROTO_STOP_SEQ = "\n"; static constexpr std::string_view MCC_COMMPROTO_KEYPARAM_DELIM_SEQ = " "; static constexpr std::string_view MCC_COMMPROTO_PARAMPARAM_DELIM_SEQ = ";"; static constexpr std::string_view MCC_COMMPROTO_RANGEPARAM_DELIM_SEQ = ","; /* server special keywords */ static constexpr std::string_view MCC_COMMPROTO_KEYWORD_SERVER_ACK_STR = "ACK"; // ACK static constexpr std::string_view MCC_COMMPROTO_KEYWORD_SERVER_ERROR_STR = "ERROR"; // mount operational error // pre-defined errors static constexpr std::string_view MCC_COMMPROTO_SERVER_ERROR_INVKEY_STR = "INVKEY"; // invalid keyword static constexpr std::string_view MCC_COMMPROTO_SERVER_ERROR_INVPAR_STR = "INVPAR"; // invalid parameter /* server control keywords */ static constexpr std::string_view MCC_COMMPROTO_KEYWORD_RESTART_SERVER_STR = "RESTART"; // restart server /* BELOW IS ONE OF THE PROTOCOL OPTIONS CORRESPONDING MCC_GENERIC_MOUNT_C CONCEPT */ /* predefined parameters */ static constexpr std::string_view MCC_COMMPROTO_COORD_KIND_RADEC_ICRS = "RADEC_ICRS"; // ICRS RA and DEC static constexpr std::string_view MCC_COMMPROTO_COORD_KIND_RADEC = "RADEC"; // apparent RA and DEC static constexpr std::string_view MCC_COMMPROTO_COORD_KIND_HADEC = "HADEC"; // apparent HA and DEC static constexpr std::string_view MCC_COMMPROTO_COORD_KIND_AZZD = "AZZD"; // azimuth and zenithal distance static constexpr std::string_view MCC_COMMPROTO_COORD_KIND_AZALT = "AZALT"; // azimuth and altitude static constexpr std::string_view MCC_COMMPROTO_COORD_KIND_XY = "XY"; // hardware (encoder) coordinates // static constexpr MccCoordPairKind mcc_str2pairkind(std::string_view spair) // { // return spair == MCC_COMMPROTO_COORD_KIND_RADEC_ICRS ? MccCoordPairKind::COORDS_KIND_RADEC_ICRS // : spair == MCC_COMMPROTO_COORD_KIND_RADEC ? MccCoordPairKind::COORDS_KIND_RADEC_APP // : spair == MCC_COMMPROTO_COORD_KIND_HADEC ? MccCoordPairKind::COORDS_KIND_HADEC_APP // : spair == MCC_COMMPROTO_COORD_KIND_AZZD ? MccCoordPairKind::COORDS_KIND_AZZD // : spair == MCC_COMMPROTO_COORD_KIND_AZALT ? MccCoordPairKind::COORDS_KIND_AZALT // : spair == MCC_COMMPROTO_COORD_KIND_XY ? MccCoordPairKind::COORDS_KIND_XY // : MccCoordPairKind::COORDS_KIND_GENERIC; // } template static constexpr MccCoordPairKind mcc_str2pairkind(R&& spair) { if constexpr (std::is_pointer_v>) { return mcc_str2pairkind(std::string_view{spair}); } const auto hash = mcc::utils::FNV1aHash(std::forward(spair)); return hash == mcc::utils::FNV1aHash(MCC_COMMPROTO_COORD_KIND_RADEC_ICRS) ? MccCoordPairKind::COORDS_KIND_RADEC_ICRS : hash == mcc::utils::FNV1aHash(MCC_COMMPROTO_COORD_KIND_RADEC) ? MccCoordPairKind::COORDS_KIND_RADEC_APP : hash == mcc::utils::FNV1aHash(MCC_COMMPROTO_COORD_KIND_HADEC) ? MccCoordPairKind::COORDS_KIND_HADEC_APP : hash == mcc::utils::FNV1aHash(MCC_COMMPROTO_COORD_KIND_AZZD) ? MccCoordPairKind::COORDS_KIND_AZZD : hash == mcc::utils::FNV1aHash(MCC_COMMPROTO_COORD_KIND_AZALT) ? MccCoordPairKind::COORDS_KIND_AZALT : hash == mcc::utils::FNV1aHash(MCC_COMMPROTO_COORD_KIND_XY) ? MccCoordPairKind::COORDS_KIND_XY : MccCoordPairKind::COORDS_KIND_GENERIC; } static constexpr std::string_view mcc_pairkind2str(MccCoordPairKind kind) { return kind == MccCoordPairKind::COORDS_KIND_RADEC_ICRS ? MCC_COMMPROTO_COORD_KIND_RADEC_ICRS : kind == MccCoordPairKind::COORDS_KIND_RADEC_APP ? MCC_COMMPROTO_COORD_KIND_RADEC : kind == MccCoordPairKind::COORDS_KIND_HADEC_APP ? MCC_COMMPROTO_COORD_KIND_HADEC : kind == MccCoordPairKind::COORDS_KIND_AZZD ? MCC_COMMPROTO_COORD_KIND_AZZD : kind == MccCoordPairKind::COORDS_KIND_AZALT ? MCC_COMMPROTO_COORD_KIND_AZALT : kind == MccCoordPairKind::COORDS_KIND_XY ? MCC_COMMPROTO_COORD_KIND_XY : "UNKNOWN"; } /* keywords */ // NOTE: THE COORDINATES AND TIME-RELATED QUANTITIES CAN BE EXPRESSED IN THE TWO FORMATS: // 1) fixed-point real number, e.g. 123.43987537359 or -0.09775 // 2) sexagesimal number, e.g. 10:43:43.12 or -123:54:12.435 // // IN THE FIRST CASE ALL NUMBERS MUST BE INTERPRETATED AS DEGREES, // IN THE SECOND CASE NUMBERS MUST BE INTERPRETATED ACCORDING TO ITS TYPE: // ALL TIME-RELATED QUANTITIES AND RA/HA COORDINATES MUST BE EXPRESSED // IN FORMAT 'HOURS:MINUTES:SECONDS', WHILE DEC/ALT/AZ/ZD COORDINATES MUST // BE EXPRESSED AS '+/-DEGREES:ARCMINUTES:ARCSECONDS' // // USER-ENTERED (FROM NETWORK CLIENTS) COORDINATE PAIR CAN BE PROVIDED IN A MIXED FORM, I.E., // 12.34436658678 10:32:11.432 or // 10:32:11.432 12.34436658678 // // SERVER-RESPONDED COORDINATES ARE ALWAYS IN THE SAME FORMAT, SEXAGESIMAL OR FIXED-POINT // // format of output coordinates: // "COORDFMT FMT-type\n" // e.g.: // "COORDFMT SGM\n" // "COORDFMT\n" // // server must return "ACK" or "ERROR INVPAR" in the case of 'set'-operation and // "ACK COORDFMT FMT-type" in the case of 'get'-operation // e.g.: // "COORDFMT FIX\n" -> "ACK\n" // "COORDFMT SXT\n" -> "ERROR INVPAR\n" (invalid parameter of format type) // "COORDFMT\n" -> "ACK COORDFMT FIX\n" // static constexpr std::string_view MCC_COMMPROTO_KEYWORD_COORDFMT_STR = "COORDFMT"; static constexpr std::string_view MCC_COMMPROTO_KEYWORD_COORDFMT_SEXGM_STR = "SGM"; // sexagesimal static constexpr std::string_view MCC_COMMPROTO_KEYWORD_COORDFMT_FIXED_STR = "FIX"; // fixed point // precision (number of decimal places) of returned coordinates: // "COORDPREC seconds-prec arcseconds-prec\n" // seconds-prec - precision of hour-based coordinates (RA and HA) or time-related quantities // arcseconds-prec - precision of degree-based coordinates (DEC, AZ, ZD, ALT) // precision must be given as non-negative integer number // e.g. // "COORDPREC 2 1\n" (output sexagesimal RA=12:34:56.67, DEC=32:54:21.9) // static constexpr std::string_view MCC_COMMPROTO_KEYWORD_COORDPREC_STR = "COORDPREC"; // set/get target coordinates // "TARGET X-coord Y-coord XY-kind\n", if 'XY-kind' is omitted then one should assume RADEC_ICRS // e.g.: // "TARGET 12.7683487 10:23:09.75 AZZD\n" // "TARGET HADEC\n" // "TARGET\n" // // server must return "ACK" or "ERROR INVPAR" in the case of 'set'-operation and // "ACK TARGET X-coord Y-coord XY-kind" in the case of 'get'-operation // e.g.: // "TARGET 12.7683487 10:23:09.75 AZZD\n" -> "ACK\n" // "TARGET 12.7683487 10:23:09.75 AZZE\n" -> "ERROR INVPAR\n" (invalid parameter of coordinates pair kind) // // "TARGET HADEC\n" -> "ACK TARGET 20:21:56.32 -01:32:34.2 HADEC\n" // "TARGET\n" -> "ACK TARGET 20:21:56.32 -01:32:34.2 RADEC_ICRS\n" // static constexpr std::string_view MCC_COMMPROTO_KEYWORD_TARGET_STR = "TARGET"; // get mount coordinates: // "MOUNT coord-kind", if 'coord-kind' is omitted then coordinates are according to mount type, // i.e., HADEC for equathorial-type mount and AZZD for alt-azimuthal one // e.g.: // "MOUNT RADEC\n" (get current apparent RA and DEC mount coordinates) // // server must return "ACK MOUNT X-coord Y-coord XY-kind" or "ERROR INVPAR" // e.g. // "MOUNT AZALT\n" -> "ACK MOUNT 1.2332325 54.23321312 AZALT\n" // "MOUNT AZAL\n" -> "ERROR INVPAR\n" (invalid parameter of coordinates pair kind) // "MOUNT\n" -> "ACK MOUNT 1.2332325 54.23321312 AZZD\n" for alt-azimuthal mount // "MOUNT\n" -> "ACK MOUNT 1.2332325 54.23321312 HADEC\n" for equathorial mount static constexpr std::string_view MCC_COMMPROTO_KEYWORD_MOUNT_STR = "MOUNT"; static constexpr std::string_view MCC_COMMPROTO_KEYWORD_TELEMETRY_STR = "TELEMETRY"; // init mount // "INIT\n" static constexpr std::string_view MCC_COMMPROTO_KEYWORD_INIT_STR = "INIT"; // stop any movements // "STOP\n" static constexpr std::string_view MCC_COMMPROTO_KEYWORD_STOP_STR = "STOP"; // slew mount and track target: // "SLEW\n" static constexpr std::string_view MCC_COMMPROTO_KEYWORD_SLEW_STR = "SLEW"; // slew mount and stop: // "MOVE\n" static constexpr std::string_view MCC_COMMPROTO_KEYWORD_MOVE_STR = "MOVE"; // track target // "TRACK\n" static constexpr std::string_view MCC_COMMPROTO_KEYWORD_TRACK_STR = "TRACK"; // get mount status // "STATUS\n" static constexpr std::string_view MCC_COMMPROTO_KEYWORD_STATUS_STR = "STATUS"; // valid keywords static constexpr std::array MCC_COMMPROTO_VALID_KEYS = { MCC_COMMPROTO_KEYWORD_SERVER_ACK_STR, MCC_COMMPROTO_KEYWORD_SERVER_ERROR_STR, MCC_COMMPROTO_KEYWORD_COORDFMT_STR, MCC_COMMPROTO_KEYWORD_COORDPREC_STR, MCC_COMMPROTO_KEYWORD_TARGET_STR, MCC_COMMPROTO_KEYWORD_MOUNT_STR, MCC_COMMPROTO_KEYWORD_TELEMETRY_STR, MCC_COMMPROTO_KEYWORD_INIT_STR, MCC_COMMPROTO_KEYWORD_STOP_STR, MCC_COMMPROTO_KEYWORD_SLEW_STR, MCC_COMMPROTO_KEYWORD_MOVE_STR, MCC_COMMPROTO_KEYWORD_TRACK_STR, MCC_COMMPROTO_KEYWORD_STATUS_STR}; // hashes of valid keywords static constexpr std::array MCC_COMMPROTO_VALID_KEYS_HASH = [](std::index_sequence) { return std::array{mcc::utils::FNV1aHash(MCC_COMMPROTO_VALID_KEYS[Is])...}; }(std::make_index_sequence()); static constexpr size_t MCC_COMMPROTO_KEYWORD_SERVER_ACK_HASH = mcc::utils::FNV1aHash(MCC_COMMPROTO_KEYWORD_SERVER_ACK_STR); static constexpr size_t MCC_COMMPROTO_KEYWORD_SERVER_ERROR_HASH = mcc::utils::FNV1aHash(MCC_COMMPROTO_KEYWORD_SERVER_ERROR_STR); static constexpr size_t MCC_COMMPROTO_KEYWORD_TARGET_HASH = mcc::utils::FNV1aHash(MCC_COMMPROTO_KEYWORD_TARGET_STR); static constexpr size_t MCC_COMMPROTO_KEYWORD_MOUNT_HASH = mcc::utils::FNV1aHash(MCC_COMMPROTO_KEYWORD_MOUNT_STR); template RT = std::vector> struct mcc_netmsg_parse_result_t { size_t keyword_hash; T keyword; RT params; }; // network message parsing result class concept template concept mcc_netmsg_parse_result_c = requires(T t) { requires std::same_as; // hash of keyword requires traits::mcc_char_range; // keyword char-range representation // a range of parameters char-range representations requires std::ranges::output_range; }; // the function returns hash of message keyword // if 'from_server' is true then given network message is considered as a server response, i.e., // valid keywords are "ACK" or "ERROR" // // the funtions returns false in the case of invalid message format and true otherwise // template bool mcc_parse_netmsg(const IR& netmsg, ResT& parse_res, bool from_server = false) { if (std::ranges::size(netmsg) == 0) { return false; }; auto found = std::ranges::search(netmsg, MCC_COMMPROTO_KEYPARAM_DELIM_SEQ); if (std::distance(netmsg.begin(), found.begin()) == 0) { return false; } const size_t hash = mcc::utils::FNV1aHash(netmsg.begin(), found.begin()); auto it = std::ranges::find(MCC_COMMPROTO_VALID_KEYS_HASH, hash); if (it == MCC_COMMPROTO_VALID_KEYS_HASH.end()) { return false; } if (from_server) { // only ACK or ERROR auto ok = hash == MCC_COMMPROTO_VALID_KEYS_HASH[0] || hash == MCC_COMMPROTO_VALID_KEYS_HASH[1]; if (!ok) { return false; } } parse_res.keyword_hash = hash; parse_res.keyword = {netmsg.begin(), found.begin()}; auto pars = netmsg | std::views::drop(std::distance(netmsg.begin(), found.end())) | std::views::split(MCC_COMMPROTO_PARAMPARAM_DELIM_SEQ); decltype(parse_res.params) res; for (auto const& el : pars) { // parameters std::back_inserter(res) = {el.begin(), el.end()}; } parse_res.params = std::move(res); return true; } // construct network message // the function returns false if input keyword is not valid one (see MCC_COMMPROTO_VALID_KEYS)! template bool mcc_netmsg_construct(traits::mcc_output_char_range auto& msg, traits::mcc_input_char_range auto const& keyword, PTs... params) { const size_t hash = mcc::utils::FNV1aHash(keyword); if (!std::ranges::contains(MCC_COMMPROTO_VALID_KEYS_HASH, hash)) { return false; } msg = {keyword.begin(), keyword.end()}; if constexpr (sizeof...(PTs)) { std::ranges::copy(MCC_COMMPROTO_KEYPARAM_DELIM_SEQ, std::back_inserter(msg)); [&msg](this auto&& self, const T& par, const Ts&... pars) { if constexpr (std::is_arithmetic_v) { std::ranges::copy(std::to_string(par), std::back_inserter(msg)); } else if constexpr (std::convertible_to) { std::ranges::copy(static_cast(par), std::back_inserter(msg)); } else if constexpr (std::constructible_from) { std::ranges::copy(std::string(par), std::back_inserter(msg)); } else if constexpr (traits::mcc_char_range) { std::ranges::copy(std::string(par.begin(), par.end()), std::back_inserter(msg)); } else if constexpr (std::same_as) { std::ranges::copy(mcc_pairkind2str(par), std::back_inserter(msg)); } else { static_assert(false, "UNSUPPORTED TYPE!!!"); } if constexpr (sizeof...(Ts)) { std::ranges::copy(MCC_COMMPROTO_PARAMPARAM_DELIM_SEQ, std::back_inserter(msg)); std::forward(self)(pars...); } }(params...); } return true; } // the function convert given network message parsing result class to // celestial point coordinates according to parsed message parameters. // // It is assumed that the coordinates and their type are contained in the consecutive elements of the input array // starting from the element 'from_idx' (zero-based): // // parse_res.params[..., X-COORD, Y-COORD, XY-KIND, ...] // // th last parameter 'XY-KIND' can be omitted and, in this case, 'default_kind' is assumed // // NOTE: IT IS ASSUMED THAT THE COORDINATES ARE REPRESENTED AS DEGREES EXPRESSED BY THE NUMBER WITH A FLOATING POINT // OR IN SEXAGESIMAL FORM. IN THE CASE OF SEXAGESIMAL FORM THE TYPE (DEGREES OR HOURS) OF THE COORDINATE // REPRESENTATION IS DETERMINED BY 'XY-KIND', E.G.: // parse_res.params[..., "12:34:52.123", "23:43:56.12", "HADEC", ...] // 'HADEC' STRING FOR 'XY-KIND' DETERMINES THE FIRST COORDINATE (HOUR ANGLE) // AS AN ANGLE IN HOUR FORM WHILE THE SECOND ONE (DECLINATION) IN DEGREES // // // The function returns false if it can not convert coordinate string to number or the 'XY-KIND' string is invalid // bool mcc_netmsg_get_cpoint(mcc_netmsg_parse_result_c auto const& parse_res, size_t from_idx, mcc_celestial_point_c auto& cpoint, MccCoordPairKind default_kind) requires std::ranges::contiguous_range { if (std::ranges::size(parse_res.params) < (from_idx + 2)) { return false; } MccCoordPairKind kind = default_kind; if (std::ranges::size(parse_res.params) > (from_idx + 2)) { kind = mcc_str2pairkind(parse_res.params[from_idx + 2]); if (kind == MccCoordPairKind::COORDS_KIND_GENERIC) { return false; } } std::optional ang1, ang2; switch (kind) { case mcc::MccCoordPairKind::COORDS_KIND_RADEC_ICRS: case mcc::MccCoordPairKind::COORDS_KIND_RADEC_APP: case mcc::MccCoordPairKind::COORDS_KIND_HADEC_APP: ang1 = utils::parsAngleString(parse_res.params[from_idx], true); break; default: ang1 = utils::parsAngleString(parse_res.params[from_idx]); } if (!ang1) { return false; } ang2 = utils::parsAngleString(parse_res.params[from_idx + 1]); if (!ang2) { return false; } if (kind != mcc::MccCoordPairKind::COORDS_KIND_RADEC_ICRS) { mcc_tp2tp(std::chrono::system_clock::now(), cpoint.time_point); } else { // J2000.0: 11:58:55.816 1 January 2000 UTC auto tp = std::chrono::sys_days(std::chrono::year_month_day(std::chrono::January / std::chrono::day(1) / std::chrono::year(2000))) + std::chrono::hours(11) + std::chrono::minutes(58) + std::chrono::milliseconds(55816); mcc_tp2tp(tp, cpoint.time_point); } cpoint.pair_kind = kind; cpoint.X = MccAngle(ang1.value(), mcc::MccDegreeTag{}); // to radians cpoint.Y = MccAngle(ang2.value(), mcc::MccDegreeTag{}); // to radians return true; } template concept mcc_netmsg_valid_keys_c = requires(T t) { // std::array of valid message keywords [](std::array) { // to ensure T::NETMSG_VALID_KEYWORDS can be used as compile-time constant static constexpr auto v0 = T::NETMSG_VALID_KEYWORDS[0]; return v0; }(T::NETMSG_VALID_KEYWORDS); // std::array of valid message keywords hashes [](std::array) { // to ensure T::NETMSG_VALID_KEYWORD_HASHES can be used as compile-time constant static constexpr auto v0 = T::NETMSG_VALID_KEYWORD_HASHES[0]; return v0; }(T::NETMSG_VALID_KEYWORD_HASHES); requires T::NETMSG_VALID_KEYWORDS.size() == T::NETMSG_VALID_KEYWORD_HASHES.size(); }; struct MccNetMessageValidKeywords { static constexpr std::array NETMSG_VALID_KEYWORDS = { MCC_COMMPROTO_KEYWORD_SERVER_ACK_STR, MCC_COMMPROTO_KEYWORD_SERVER_ERROR_STR, MCC_COMMPROTO_KEYWORD_COORDFMT_STR, MCC_COMMPROTO_KEYWORD_COORDPREC_STR, MCC_COMMPROTO_KEYWORD_TARGET_STR, MCC_COMMPROTO_KEYWORD_MOUNT_STR, MCC_COMMPROTO_KEYWORD_TELEMETRY_STR, MCC_COMMPROTO_KEYWORD_INIT_STR, MCC_COMMPROTO_KEYWORD_STOP_STR, MCC_COMMPROTO_KEYWORD_SLEW_STR, MCC_COMMPROTO_KEYWORD_MOVE_STR, MCC_COMMPROTO_KEYWORD_TRACK_STR, MCC_COMMPROTO_KEYWORD_STATUS_STR}; // hashes of valid keywords static constexpr std::array NETMSG_VALID_KEYWORD_HASHES = [](std::index_sequence) { return std::array{mcc::utils::FNV1aHash(NETMSG_VALID_KEYWORDS[Is])...}; }(std::make_index_sequence()); static std::expected isKeywordValid(std::string_view key) { const auto hash = utils::FNV1aHash(key); for (auto const& h : NETMSG_VALID_KEYWORD_HASHES) { if (h == hash) { return h; } } return std::unexpected(std::make_error_code(std::errc::invalid_argument)); } }; static_assert(mcc_netmsg_valid_keys_c, ""); template class MccNetMessage { public: typedef BASE_T valid_keys_t; typedef BYTEREPR_T byte_repr_t; template MccNetMessage(KT&& key, PTs&&... params) { construct(std::forward(key), std::forward(params)...); } template bool withKey(const KT& key) { return utils::FNV1aHash(key) == _keywordHash; } template std::expected params(DeserFuncT&& deser_func) { T val; auto ec = deser_func(_params, val); if (ec) { return std::unexpect(ec); } else { return val; } } template std::expected params() { return params(utils::MccSimpleDeserializer{}.setRangeDelim(MCC_COMMPROTO_RANGEPARAM_DELIM_SEQ)); } template std::error_code construct(KT&& key, PTs&&... params) { if (!std::ranges::size(key)) { return std::make_error_code(std::errc::invalid_argument); } auto r = valid_keys_t::isKeywordValid(key); if (!r) { return r.error(); } _keywordHash = r.value(); _msgBuffer = BYTEREPR_T{}; std::ranges::copy(key, std::back_inserter(_msgBuffer)); _keyword = {_msgBuffer.begin(), _msgBuffer.end()}; _params = std::string_view{}; if constexpr (sizeof...(PTs)) { std::ranges::copy(MCC_COMMPROTO_KEYPARAM_DELIM_SEQ, std::back_inserter(_msgBuffer)); [this](this auto&& self, const T& par, const Ts&... pars) { if constexpr (std::is_arithmetic_v) { std::ranges::copy(std::to_string(par), std::back_inserter(_msgBuffer)); } else if constexpr (std::convertible_to) { std::ranges::copy(static_cast(par), std::back_inserter(_msgBuffer)); } else if constexpr (std::constructible_from) { std::ranges::copy(std::string(par), std::back_inserter(_msgBuffer)); } else if constexpr (traits::mcc_char_range) { std::ranges::copy(std::string(par.begin(), par.end()), std::back_inserter(_msgBuffer)); } else if constexpr (std::same_as) { std::ranges::copy(mcc_pairkind2str(par), std::back_inserter(_msgBuffer)); } else if constexpr (traits::mcc_time_duration_c) { std::forward(self)(par.count()); } else { static_assert(false, "UNSUPPORTED TYPE!!!"); } if constexpr (sizeof...(Ts)) { std::ranges::copy(MCC_COMMPROTO_PARAMPARAM_DELIM_SEQ, std::back_inserter(_msgBuffer)); std::forward(self)(pars...); } }(std::forward(params)...); _params = {_msgBuffer.begin() + _keyword.size() + MCC_COMMPROTO_KEYPARAM_DELIM_SEQ.size(), _msgBuffer.end()}; } return {}; } protected: size_t _keywordHash{}; std::string_view _keyword{}; std::string_view _params{}; BYTEREPR_T _msgBuffer{}; }; } // namespace mcc::network