diff --git a/asibfm700/asibfm700_mount.cpp b/asibfm700/asibfm700_mount.cpp index c1a37c0..2056976 100644 --- a/asibfm700/asibfm700_mount.cpp +++ b/asibfm700/asibfm700_mount.cpp @@ -52,6 +52,7 @@ Asibfm700Mount::error_t Asibfm700Mount::initMount() logInfo(" IERS Bulletin A filename: {}", _mountConfig.bulletinAFilename); logInfo(""); + logDebug("Delete previously defined prohobited zones"); clearPZones(); logInfo("Add prohibited zones ..."); @@ -104,9 +105,16 @@ Asibfm700Mount::error_t Asibfm700Mount::initMount() logInfo(" YPIDV: [P: {}, I: {}, D: {}]", _mountConfig.servoControllerConfig.devConfig.YPIDV.P, _mountConfig.servoControllerConfig.devConfig.YPIDV.I, _mountConfig.servoControllerConfig.devConfig.YPIDV.D); - auto hw_err = hardwareInit(); + // auto hw_err = hardwareInit(); + // if (hw_err) { + // errorLogging("", hw_err); + // } + + // call base class initMount method + auto hw_err = base_gm_class_t::initMount(); if (hw_err) { errorLogging("", hw_err); + return hw_err; } return mcc::MccGenericMountErrorCode::ERROR_OK; diff --git a/asibfm700/asibfm700_netserver.h b/asibfm700/asibfm700_netserver.h new file mode 100644 index 0000000..df7004b --- /dev/null +++ b/asibfm700/asibfm700_netserver.h @@ -0,0 +1,783 @@ +#pragma once + +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include + +#if __has_include() // POSIX +#define FORK_EXISTS 1 +#include +#include +#include +#endif + + + +#include "asibfm700_netserver_endpoint.h" +#include "control_proto.h" +#include "mcc_traits.h" +// #include "mount.h" + +namespace asibfm700 +{ + + +namespace traits +{ + +template +concept netserver_endpoint_c = + std::derived_from || std::derived_from || + std::derived_from || + std::derived_from; + +template +static constexpr bool is_serial_proto = std::derived_from; + +// template +// static constexpr bool is_tcp_proto = std::derived_from; + +// template +// static constexpr bool is_local_stream_proto = +// std::derived_from; + +// template +// static constexpr bool is_local_seqpack_proto = +// std::derived_from; + + +template +static constexpr bool is_tcp_proto = + std::derived_from || std::derived_from; + +template +static constexpr bool is_local_stream_proto = std::derived_from || + std::derived_from; + +template +static constexpr bool is_local_seqpack_proto = std::derived_from || + std::derived_from; + + +} // namespace traits + + +class MccMountServer +{ +public: + static constexpr std::chrono::duration DEFAULT_RCV_TIMEOUT = std::chrono::hours(12); + static constexpr std::chrono::duration DEFAULT_SND_TIMEOUT = std::chrono::milliseconds(2000); + + MccMountServer(asio::io_context& ctx, std::shared_ptr logger = spdlog::null_logger_mt("NULL")) + : _asioContext(ctx), _serverLogger(logger), _stopSignal(ctx), _restartSignal(ctx) + { + std::stringstream st; + st << std::this_thread::get_id(); + + _serverLogger->set_pattern("[%Y-%m-%d %T.%e][%l]: %v"); + + _serverLogger->info("Create mount server instance (thread ID = {})", st.str()); + } + + ~MccMountServer() + { + std::stringstream st; + st << std::this_thread::get_id(); + + _serverLogger->info("Delete mount server instance (thread ID = {}) ...", st.str()); + + stopListening(); + disconnectClients(); + } + + + template + asio::awaitable listen(std::derived_from auto endpoint, CtorArgTs&&... ctor_args) + { + if (!endpoint.isValid()) { + _serverLogger->error("Cannot start listening! Invalid endpoint string representation ('{}')!", + endpoint.endpoint()); + co_return; + } + + // add root path to endpoint one + std::filesystem::path pt("/"); + + + if (endpoint.isLocalSerial()) { + pt += endpoint.path(); + + asio::serial_port s_port(_asioContext); + + std::error_code ec; + + if constexpr (sizeof...(CtorArgTs)) { // options + setSerialOpts(s_port, std::forward(ctor_args)...); + } + + s_port.open(pt.string(), ec); + if (ec) { + _serverLogger->error("Cannot open serial device '{}' (Error = '{}')!", pt.string(), ec.message()); + co_return; + } + + // asio::co_spawn(_asioContext, listen(std::move(s_port)), asio::detached); + co_await listen(std::move(s_port)); + } else if (endpoint.isLocal()) { + // create abstract namespace socket endpoint if its path starts from '@' symbol + endpoint.makeAbstract('@'); + + // if (endpoint.path()[0] == '\0') { // abstract namespace + // std::string p; + // std::ranges::copy(endpoint.path(), std::back_inserter(p)); + // p.insert(p.begin() + 1, '/'); // insert after '\0' symbol + // pt = p; + // } else { + // pt += endpoint.path(); + // } + + if (endpoint.isLocalStream()) { + co_await listen(asio::local::stream_protocol::endpoint(endpoint.path(pt.string()))); + } else if (endpoint.isLocalSeqpacket()) { + co_await listen(asio::local::seq_packet_protocol::endpoint(endpoint.path(pt.string()))); + } else { + co_return; // it must not be!!!! + } + } else if (endpoint.isTCP()) { + // resolve hostname + try { + asio::ip::tcp::resolver res(_asioContext); + + auto r_result = co_await res.async_resolve(endpoint.host(), endpoint.portView(), asio::use_awaitable); + + _serverLogger->info("Resolve hostname <{}> to {} IP-addresses", endpoint.host(), r_result.size()); + + + bool exit_flag = false; + asio::ip::tcp::acceptor acc(_asioContext); + + for (auto const& epn : r_result) { + try { + // std::stringstream st; + + // _serverLogger->debug("Create connection acceptor for endpoint <{}> ...", + // epn.address().to_string()); + acc = asio::ip::tcp::acceptor(_asioContext, epn); + // st << acc.local_endpoint(); + exit_flag = true; + break; + } catch (const std::system_error& err) { + _serverLogger->error("An error occuring while creating connection acceptor (ec = {})", + err.what()); + continue; + } + } + if (!exit_flag) { + _serverLogger->error("Cannot start listening on any resolved endpoints!"); + co_return; + } + + _tcpAcceptors.emplace_back(&acc); + + _serverLogger->info("Start listening at <{}> endpoint ...", acc.local_endpoint().address().to_string()); + + // start accepting connections + for (;;) { + auto sock = co_await acc.async_accept(asio::use_awaitable); + // start new client session + asio::co_spawn(_asioContext, startSession(std::move(sock)), asio::detached); + } + + } catch (const std::system_error& err) { + _serverLogger->error("An error occured while trying to start accepting connections! ec = '{}'", + err.what()); + } + } + } + + template + asio::awaitable listen(EpnT endpoint) + { + using epn_t = std::decay_t; + + std::error_code ec; + + if constexpr (traits::is_serial_proto) { + // first, check if port is open + if (!endpoint.is_open()) { + if (ec) { + // ?????????? + _serverLogger->error("Serial port was not open! Do not start waiting for commands!"); + } + } else { + asio::co_spawn(_asioContext, startSession(std::move(endpoint)), asio::detached); + } + + } else if constexpr (traits::is_tcp_proto || traits::is_local_stream_proto || + traits::is_local_seqpack_proto) { + try { + std::stringstream st; + + st << endpoint; + _serverLogger->debug("Create connection acceptor for endpoint <{}> ...", st.str()); + auto acc = typename epn_t::protocol_type::acceptor(_asioContext, endpoint); + + st.str(""); + st << acc.local_endpoint(); + _serverLogger->info("Start listening at <{}> endpoint ...", st.str()); + + + if constexpr (traits::is_tcp_proto) { + _tcpAcceptors.emplace_back(&acc); + } else if constexpr (traits::is_local_stream_proto) { + _localStreamAcceptors.emplace_back(&acc); + } else if constexpr (traits::is_local_seqpack_proto) { + _localSeqpackAcceptors.emplace_back(&acc); + } else { + static_assert(false, "INVALID ENDPOINT!!!"); + } + + // start accepting connections + for (;;) { + auto sock = co_await acc.async_accept(asio::use_awaitable); + // start new client session + asio::co_spawn(_asioContext, startSession(std::move(sock)), asio::detached); + } + + + } catch (const std::system_error& err) { + _serverLogger->error("An error occured while trying to start accepting connections! ec = '{}'", + err.what()); + } + } else { + static_assert(false, "INVALID ENDPOINT!!!"); + } + + co_return; + } + + + // close listening on all endpoints + void stopListening() + { + std::error_code ec; + + _serverLogger->info("Close all listening endpoints ..."); + + auto num = + _serialPorts.size() + _tcpAcceptors.size() + _localStreamAcceptors.size() + _localSeqpackAcceptors.size(); + if (!num) { + _serverLogger->info("There are no listening ports/sockets!"); + return; + } + + auto close_func = [this](auto& acc_ptrs, std::string_view desc) { + size_t N = 0, M = 0; + std::error_code ec; + + if (acc_ptrs.size()) { + _serverLogger->info("Close {} acceptors ...", desc); + + for (auto& acc : acc_ptrs) { + acc->close(ec); + if (ec) { + _serverLogger->error("Cannot close {} acceptor! ec = '{}'", desc, ec.message()); + } else { + ++M; + } + ++N; + } + + _serverLogger->debug("{} from {} {} acceptors were closed!", M, N, desc); + + // pointers are invalidated here, so clear its container + acc_ptrs.clear(); + } + }; + + close_func(_tcpAcceptors, "TCP socket"); + close_func(_localStreamAcceptors, "local stream socket"); + close_func(_localSeqpackAcceptors, "local seqpack socket"); + + + _serverLogger->info("The all server listening endpoints were closed!"); + } + + void disconnectClients() + { + auto disconn_func = [this](std::ranges::input_range auto& ptrs) { + std::error_code ec; + for (auto& ptr : ptrs) { + // ptr->cancel(ec); + // if (ec) { + // _serverLogger->warn("socket_base::cancel: an error occured (ec = {})", ec.message()); + // } + + ptr->shutdown(asio::socket_base::shutdown_both, ec); + if (ec) { + _serverLogger->warn("socket_base::shutdown: an error occured (ec = {})", ec.message()); + } + + ptr->close(ec); + if (ec) { + _serverLogger->warn("socket_base::close: an error occured (ec = {})", ec.message()); + } + } + }; + + _serverLogger->info("Close all client connections ..."); + + if (_serialPorts.empty() && _localStreamSockets.empty() && _localSeqpackSockets.empty() && + _tcpSockets.empty()) { + _serverLogger->info("There were no active client connections! Skip!"); + } + + if (_serialPorts.size()) { + std::lock_guard lock_g(_serialPortsMutex); + + std::error_code ec; + _serverLogger->info("Close serial port clients ({} in total) ...", _serialPorts.size()); + for (auto& ptr : _serialPorts) { + ptr->cancel(ec); + if (ec) { + _serverLogger->warn("serial_port::cancel: an error occured (ec = {})", ec.message()); + } + ptr->close(ec); + if (ec) { + _serverLogger->warn("serial_port::close: an error occured (ec = {})", ec.message()); + } + } + } + + if (_localStreamSockets.size()) { + std::lock_guard lock_g(_localStreamSocketsMutex); + + _serverLogger->info("Close local stream socket-type clients ({} in total) ...", _localStreamSockets.size()); + disconn_func(_localStreamSockets); + } + + if (_localSeqpackSockets.size()) { + std::lock_guard lock_g(_localSeqpackSocketsMutex); + + _serverLogger->info("Close local seqpack socket-type clients ({} in total) ...", + _localSeqpackSockets.size()); + disconn_func(_localSeqpackSockets); + } + + if (_tcpSockets.size()) { + std::lock_guard lock_g(_tcpSocketsMutex); + + _serverLogger->info("Close TCP socket-type clients ({} in total) ...", _tcpSockets.size()); + disconn_func(_tcpSockets); + } + + _serverLogger->info("Client connection were closed!"); + } + + void daemonize() + { +#ifdef FORK_EXISTS + _serverLogger->info("Daemonize the server ..."); + + _asioContext.notify_fork(asio::execution_context::fork_prepare); + + auto tmp_path = std::filesystem::temp_directory_path(); + if (tmp_path.empty()) { + tmp_path = std::filesystem::current_path().root_path(); + } + + + if (pid_t pid = fork()) { + if (pid > 0) { + exit(0); + } else { + // throw std::system_error(errno, std::generic_category(), "CANNOT FORK 1-STAGE"); + _serverLogger->error("CANNOT FORK 1-STAGE! The server was not daemonized!"); + return; + } + } + + if (setsid() == -1) { + // throw std::system_error(errno, std::generic_category(), "CANNOT FORK SETSID"); + _serverLogger->error("CANNOT FORK SETSID! The server was not daemonized!"); + return; + } + + _serverLogger->info("Try to set the daemon current path to '{}' ...", tmp_path.string()); + + std::error_code ec{}; + + std::filesystem::current_path(tmp_path, ec); + if (!ec) { + _serverLogger->warn("Cannot change current path to '{}'! Ignore!", tmp_path.string()); + } + + umask(0); + + if (pid_t pid = fork()) { + if (pid > 0) { + exit(0); + } else { + // throw std::system_error(errno, std::generic_category(), "CANNOT FORK 2-STAGE"); + _serverLogger->error("CANNOT FORK 2-STAGE! The server was not daemonized!"); + return; + } + } + + // stdin, stdout, stderr + close(0); + close(1); + close(2); + + _asioContext.notify_fork(asio::io_context::fork_child); + + _serverLogger->info("The server was daemonized successfully!"); + +#else + _serverLogger->warn("Host platform is not POSIX one, so cannot daemonize the server!"); +#endif + } + + template , std::ranges::range RRT = std::vector> + void setupSignals(const RST& stop_sig_num = {SIGINT, SIGTERM}, const RRT& restart_sig_num = {SIGUSR1}) + requires(std::convertible_to, int> && + std::convertible_to, int>) + { + for (const int sig : stop_sig_num) { + _stopSignal.add(sig); + } + + _stopSignal.async_wait([this](std::error_code, int signo) { + _serverLogger->info("Stop signal was received (signo = {})", signo); + + stopListening(); + disconnectClients(); + + _asioContext.stop(); + }); + + for (const int sig : restart_sig_num) { + _restartSignal.add(sig); + } + + _restartSignal.async_wait([this](std::error_code, int signo) { + _serverLogger->info("Restart signal was received (signo = {})", signo); + restart(); + }); + } + + void restart() + { + disconnectClients(); + + _restartSignal.async_wait([this](std::error_code, int signo) { + _serverLogger->info("Restart signal was received (signo = {})", signo); + restart(); + }); + } + +private: + asio::io_context& _asioContext; + std::shared_ptr _serverLogger; + + asio::signal_set _stopSignal, _restartSignal; + + std::set _serialPorts; + // std::vector _serialPorts; + + std::vector _tcpAcceptors; + std::vector _localStreamAcceptors; + std::vector _localSeqpackAcceptors; + + std::set _tcpSockets; + std::set _localStreamSockets; + std::set _localSeqpackSockets; + // std::vector _tcpSockets; + // std::vector _localStreamSockets; + // std::vector _localSeqpackSockets; + + std::mutex _serialPortsMutex, _tcpSocketsMutex, _localStreamSocketsMutex, _localSeqpackSocketsMutex; + + // helpers + template + void setSerialOpts(asio::serial_port& s_port, OptT&& opt, OptTs&&... opts) + { + std::error_code ec; + + s_port.set_option(opt, ec); + if (ec) { + std::string_view opt_name; + + if constexpr (std::same_as) { + opt_name = "baud rate"; + } else if constexpr (std::same_as) { + opt_name = "parity"; + } else if constexpr (std::same_as) { + opt_name = "flow control"; + } else if constexpr (std::same_as) { + opt_name = "stop bits"; + } else if constexpr (std::same_as) { + opt_name = "char size"; + } + + _serverLogger->error("Cannot set serial port '{}' option! Just skip!", opt_name); + } + + if constexpr (sizeof...(OptTs)) { + setSerialOpts(s_port, std::forward(opts)...); + } + } + + + std::vector handleClientCommand(std::string_view command) + { + std::vector resp{BM700::CONTROL_PROTO_STR_RESP_ACK.begin(), BM700::CONTROL_PROTO_STR_RESP_ACK.end()}; + + return resp; + } + + + template + asio::awaitable startSession(auto socket, + const RCVT& rcv_timeout = DEFAULT_RCV_TIMEOUT, + const SNDT& snd_timeout = DEFAULT_SND_TIMEOUT) + { + using namespace asio::experimental::awaitable_operators; + + using sock_t = std::decay_t; + + + auto look_for_whole_msg = [](auto const& bytes) { + auto found = std::ranges::search(bytes, BM700::CONTROL_PROTO_STOP_SEQ); + return found.empty() ? std::span(bytes.begin(), bytes.begin()) : std::span(bytes.begin(), found.end()); + }; + + + asio::streambuf sbuff; + size_t nbytes; + std::stringstream st; + std::string r_epn; + + st << std::this_thread::get_id(); + std::string thr_id = st.str(); + st.str(""); + + if constexpr (traits::is_serial_proto) { + st << "serial port: " << socket.native_handle(); + } else { // network sockets + st << socket.remote_endpoint(); + } + + r_epn = st.str(); + if (r_epn.empty()) { // UNIX domain sockets + r_epn = "local"; + } + + _serverLogger->info("Start client session: remote endpoint <{}> (session thread ID = {})", r_epn, thr_id); + + try { + if constexpr (!traits::is_serial_proto) { + _serverLogger->trace("Set socket option KEEP_ALIVE to TRUE"); + socket.set_option(asio::socket_base::keep_alive(true)); + } + + if constexpr (traits::is_serial_proto) { + std::lock_guard lock_g(_serialPortsMutex); + _serialPorts.insert(&socket); + } else if constexpr (traits::is_tcp_proto) { + std::lock_guard lock_g(_tcpSocketsMutex); + // _tcpSockets.emplace_back(&socket); + _tcpSockets.insert(&socket); + } else if constexpr (traits::is_local_stream_proto) { + std::lock_guard lock_g(_localStreamSocketsMutex); + // _localStreamSockets.emplace_back(&socket); + _localStreamSockets.insert(&socket); + } else if constexpr (traits::is_local_seqpack_proto) { + std::lock_guard lock_g(_localSeqpackSocketsMutex); + // _localSeqpackSockets.emplace_back(&socket); + _localSeqpackSockets.insert(&socket); + } else { + static_assert(false, "INVALID SOCKET TTYPE!!!"); + } + + + // send buffer sequence + // initiate the second element by "stop-sequence" symbols + std::vector snd_buff_seq{ + {}, {BM700::CONTROL_PROTO_STOP_SEQ.data(), BM700::CONTROL_PROTO_STOP_SEQ.size()}}; + + asio::steady_timer timeout_timer(_asioContext); + std::variant op_res; + + std::error_code ec; + + bool do_read = true; + + // main client request -- server respond cycle + for (;;) { + // receive message + + if (do_read) { + _serverLogger->trace("Start socket/port reading operation with timeout {} ...", rcv_timeout); + + if constexpr (traits::is_serial_proto) { + nbytes = 1024; + } else { + nbytes = socket.available(); + } + + auto buff = sbuff.prepare(nbytes ? nbytes : 1); + + // timeout_timer.expires_after(std::chrono::seconds(5)); + timeout_timer.expires_after(rcv_timeout); + + if constexpr (traits::is_local_seqpack_proto) { + asio::socket_base::message_flags oflags; + + op_res = co_await ( + socket.async_receive(buff, oflags, asio::redirect_error(asio::use_awaitable, ec)) || + timeout_timer.async_wait(asio::use_awaitable)); + + } else { + op_res = co_await (asio::async_read(socket, buff, asio::transfer_at_least(1), + asio::redirect_error(asio::use_awaitable, ec)) || + timeout_timer.async_wait(asio::use_awaitable)); + } + + if (ec) { + throw std::system_error(ec); + } + + if (op_res.index()) { + throw std::system_error(std::make_error_code(std::errc::timed_out)); + } else { + nbytes = std::get<0>(op_res); + + _serverLogger->trace("{} bytes were received", nbytes); + + if constexpr (traits::is_local_seqpack_proto) { + if (!nbytes) { // EOF! + throw std::system_error(std::error_code(asio::error::misc_errors::eof)); + } + } + } + + sbuff.commit(nbytes); + } // here, the input stream buffer still contains remaining bytes. try to handle its + + auto start_ptr = static_cast(sbuff.data().data()); + + auto msg = look_for_whole_msg(std::span(start_ptr, sbuff.size())); + if (msg.empty()) { // still not whole message + _serverLogger->trace( + "It seems a partial command message was received, so waiting for remaining part ..."); + do_read = true; + continue; + } + + + // extract command without stop sequence symbols + // std::string comm; + // std::ranges::copy(msg | std::views::take(msg.size() - BM700::CONTROL_PROTO_STOP_SEQ.size()), + // std::back_inserter(comm)); + + std::string_view comm{msg.begin(), msg.end() - BM700::CONTROL_PROTO_STOP_SEQ.size()}; + + _serverLogger->debug("A command [{}] was received from client (remote endpoint <{}>, thread ID = {})", + comm, r_epn, thr_id); + + auto resp = handleClientCommand(comm); + + // remove received message from the input stream buffer. NOTE: 'msg' is now invalidated!!! + sbuff.consume(msg.size()); + do_read = sbuff.size() == 0; + + + _serverLogger->debug("Send respond [{}] to client (remote endpoint <{}>, thread ID = {})", + std::string_view(resp.begin(), resp.end()), r_epn, thr_id); + + // send server respond to client + snd_buff_seq[0] = {resp.data(), resp.size()}; + + timeout_timer.expires_after(snd_timeout); + if constexpr (traits::is_local_seqpack_proto) { + op_res = + co_await (socket.async_send(snd_buff_seq, 0, asio::redirect_error(asio::use_awaitable, ec)) || + timeout_timer.async_wait(asio::use_awaitable)); + } else { + // nbytes = co_await asio::async_write(socket, snd_buff_seq, asio::use_awaitable); + op_res = co_await ( + asio::async_write(socket, snd_buff_seq, asio::redirect_error(asio::use_awaitable, ec)) || + timeout_timer.async_wait(asio::use_awaitable)); + } + + if (ec) { + throw std::system_error(ec); + } + + if (op_res.index()) { + throw std::system_error(std::make_error_code(std::errc::timed_out)); + } else { + nbytes = std::get<0>(op_res); + _serverLogger->trace("{} bytes were sent", nbytes); + } + + if (nbytes != (resp.size() + BM700::CONTROL_PROTO_STOP_SEQ.size())) { // !!!!!!!!!! + } + } + + } catch (const std::system_error& ex) { + if (ex.code() == std::error_code(asio::error::misc_errors::eof)) { + _serverLogger->info( + "It seems client or server closed the connection (remote endpoint <{}>, thread ID = {})", r_epn, + thr_id); + } else { + _serverLogger->error("An error '{}' occured in client session (remote endpoint <{}>, thread ID = {})", + ex.what(), r_epn, thr_id); + } + } catch (const std::exception& ex) { + _serverLogger->error( + "An unhandled error '{}' occured in client sesssion (remote endpoint <{}>, thread ID = {})", ex.what(), + r_epn, thr_id); + } catch (...) { + _serverLogger->error("An unhandled error occured in client sesssion (remote endpoint <{}>, thread ID = {})", + r_epn, thr_id); + } + + // remove pointer as it is invalidated here (at the exit of the method) + if constexpr (traits::is_serial_proto) { + _serialPorts.erase(&socket); + } else if constexpr (traits::is_tcp_proto) { + _tcpSockets.erase(&socket); + } else if constexpr (traits::is_local_stream_proto) { + _localStreamSockets.erase(&socket); + } else if constexpr (traits::is_local_seqpack_proto) { + _localSeqpackSockets.erase(&socket); + } else { + static_assert(false, "INVALID SOCKET TTYPE!!!"); + } + + _serverLogger->info("Close client session: remote endpoint <{}> (thread ID = {})", r_epn, thr_id); + } +}; + +} // namespace asibfm700 diff --git a/asibfm700/asibfm700_netserver_endpoint.h b/asibfm700/asibfm700_netserver_endpoint.h new file mode 100644 index 0000000..6b9834e --- /dev/null +++ b/asibfm700/asibfm700_netserver_endpoint.h @@ -0,0 +1,507 @@ +#pragma once + +#include +#include +#include +#include +#include +#include +#include + +#include "mcc_traits.h" + +namespace asibfm700 +{ + +namespace utils +{ + +static constexpr bool charSubrangeCompare(const mcc::traits::mcc_char_view auto& what, + const mcc::traits::mcc_char_view auto& where, + bool case_insensitive = false) +{ + if (std::ranges::size(what) == std::ranges::size(where)) { + if (case_insensitive) { + auto f = std::ranges::search(where, + std::views::transform(what, [](const char& ch) { return std::tolower(ch); })); + return !f.empty(); + } else { + auto f = std::ranges::search(where, what); + return !f.empty(); + } + } + + return false; +} + +} // namespace utils + +/* + * Very simple various protocols endpoint parser and holder class + * + * endpoint: proto_mark://host_name:port_num/path + * where "part" is optional for all non-local protocol kinds; + * + * for local kind of protocols the endpoint must be given as: + * local://stream/PATH + * local://seqpacket/PATH + * local://serial/PATH + * where 'stream' and 'seqpacket' "host_name"-field marks the + * stream-type and seqpacket-type UNIX domain sockets protocols; + * 'serial' marks a serial (RS232/485) protocol. + * here, possible "port_num" field is allowed but ignored. + * + * NOTE: "proto_mark" and "host_name" (for local kind) fields are parsed in case-insensitive manner! + * + * EXAMPLES: tcp://192.168.70.130:3131 + * local://serial/dev/ttyS1 + * local://seqpacket/tmp/BM70_SERVER_SOCK + * + * + */ + + +class Asibfm700NetserverEndpoint +{ +public: + static constexpr std::string_view protoHostDelim = "://"; + static constexpr std::string_view hostPortDelim = ":"; + static constexpr std::string_view portPathDelim = "/"; + + enum proto_id_t : uint8_t { + PROTO_ID_LOCAL, + PROTO_ID_SEQLOCAL, + PROTO_ID_SERLOCAL, + PROTO_ID_TCP, + PROTO_ID_TLS, + PROTO_ID_UNKNOWN + }; + + static constexpr std::string_view protoMarkLocal{"local"}; // UNIX domain + static constexpr std::string_view protoMarkTCP{"tcp"}; // TCP + static constexpr std::string_view protoMarkTLS{"tls"}; // TLS + + static constexpr std::array validProtoMarks{protoMarkLocal, protoMarkTCP, protoMarkTLS}; + + + static constexpr std::string_view localProtoTypeStream{"stream"}; // UNIX domain stream + static constexpr std::string_view localProtoTypeSeqpacket{"seqpacket"}; // UNIX domain seqpacket + static constexpr std::string_view localProtoTypeSerial{"serial"}; // serial (RS232/485) + + static constexpr std::array validLocalProtoTypes{localProtoTypeStream, localProtoTypeSeqpacket, + localProtoTypeSerial}; + + + template + Asibfm700NetserverEndpoint(const R& ept) + { + fromRange(ept); + } + + Asibfm700NetserverEndpoint(const Asibfm700NetserverEndpoint& other) + { + copyInst(other); + } + + Asibfm700NetserverEndpoint(Asibfm700NetserverEndpoint&& other) + { + moveInst(std::move(other)); + } + + virtual ~Asibfm700NetserverEndpoint() = default; + + + Asibfm700NetserverEndpoint& operator=(const Asibfm700NetserverEndpoint& other) + { + copyInst(other); + + return *this; + } + + + Asibfm700NetserverEndpoint& operator=(Asibfm700NetserverEndpoint&& other) + { + moveInst(std::move(other)); + + return *this; + } + + template + requires std::ranges::contiguous_range + bool fromRange(const R& ept) + { + _isValid = false; + + // at least 'ws://a' (proto, proto-host delimiter and at least a single character of hostname) + if (std::ranges::size(ept) < 6) { + return _isValid; + } + + if constexpr (std::is_array_v>) { + _endpoint = ept; + } else { + _endpoint.clear(); + std::ranges::copy(ept, std::back_inserter(_endpoint)); + } + + auto found = std::ranges::search(_endpoint, protoHostDelim); + if (found.empty()) { + return _isValid; + } + + + ssize_t idx; + if ((idx = checkProtoMark(std::string_view{_endpoint.begin(), found.begin()})) < 0) { + return _isValid; + } + + _proto = validProtoMarks[idx]; + + _host = std::string_view{found.end(), _endpoint.end()}; + + auto f1 = std::ranges::search(_host, portPathDelim); + // std::string_view port_sv; + if (f1.empty() && isLocal()) { // no path, but it is mandatory for 'local'! + return _isValid; + } else { + _host = std::string_view(_host.begin(), f1.begin()); + + _path = std::string_view(f1.end(), &*_endpoint.end()); + + f1 = std::ranges::search(_host, hostPortDelim); + if (f1.empty() && !isLocal()) { // no port, but it is mandatory for non-local! + return _isValid; + } + + _portView = std::string_view(f1.end(), _host.end()); + if (_portView.size()) { + _host = std::string_view(_host.begin(), f1.begin()); + + if (!isLocal()) { + // convert port string to int + auto end_ptr = _portView.data() + _portView.size(); + + auto [ptr, ec] = std::from_chars(_portView.data(), end_ptr, _port); + if (ec != std::errc() || ptr != end_ptr) { + return _isValid; + } + } else { // ignore for local + _port = -1; + } + } else { + _port = -1; + } + + if (isLocal()) { // check for special values + idx = 0; + if (std::ranges::any_of(validLocalProtoTypes, [&idx, this](const auto& el) { + bool ok = utils::charSubrangeCompare(_host, el, true); + if (!ok) { + ++idx; + } + return ok; + })) { + _host = validLocalProtoTypes[idx]; + } else { + return _isValid; + } + } + } + + _isValid = true; + + return _isValid; + } + + + bool isValid() const + { + return _isValid; + } + + + auto endpoint() const + { + return _endpoint; + } + + template + R proto() const + { + return part(PROTO_PART); + } + + std::string_view proto() const + { + return proto(); + } + + template + R host() const + { + return part(HOST_PART); + } + + std::string_view host() const + { + return host(); + } + + int port() const + { + return _port; + } + + template + R portView() const + { + return part(PORT_PART); + } + + std::string_view portView() const + { + return portView(); + } + + template + R path(RR&& root_path) const + { + if (_path.empty()) { + if constexpr (mcc::traits::mcc_output_char_range) { + R res; + std::ranges::copy(std::forward(root_path), std::back_inserter(res)); + + return res; + } else { // can't add root path!!! + return part(PATH_PART); + } + } + + auto N = std::ranges::distance(root_path.begin(), root_path.end()); + + if (N) { + R res; + std::filesystem::path pt(root_path.begin(), root_path.end()); + + if (isLocal() && _path[0] == '\0') { + std::ranges::copy(std::string_view(" "), std::back_inserter(res)); + pt /= _path.substr(1); + std::ranges::copy(pt.string(), std::back_inserter(res)); + *res.begin() = '\0'; + } else { + pt /= _path; + std::ranges::copy(pt.string(), std::back_inserter(res)); + } + + return res; + } else { + return part(PATH_PART); + } + } + + template + std::string path(RR&& root_path) const + { + return path(std::forward(root_path)); + } + + template + R path() const + { + return part(PATH_PART); + } + + std::string_view path() const + { + return path(); + } + + + bool isLocal() const + { + return proto() == protoMarkLocal; + } + + bool isLocalStream() const + { + return host() == localProtoTypeStream; + } + + bool isLocalSerial() const + { + return host() == localProtoTypeSerial; + } + + bool isLocalSeqpacket() const + { + return host() == localProtoTypeSeqpacket; + } + + + bool isTCP() const + { + return proto() == protoMarkTCP; + } + + bool isTLS() const + { + return proto() == protoMarkTLS; + } + + + // add '\0' char (or replace special-meaning char/char-sequence) to construct UNIX abstract namespace + // endpoint path + template + Asibfm700NetserverEndpoint& makeAbstract(const T& mark = nullptr) + requires(mcc::traits::mcc_input_char_range || std::same_as, char> || + std::is_null_pointer_v>) + { + if (!(isLocalStream() || isLocalSeqpacket())) { // only local proto is valid! + return *this; + } + + if constexpr (std::is_null_pointer_v) { // just insert '\0' + auto it = _endpoint.insert(std::string::const_iterator(_path.begin()), '\0'); + _path = std::string_view(it, _endpoint.end()); + } else if constexpr (std::same_as, char>) { // replace a character (mark) + auto pos = std::distance(_endpoint.cbegin(), std::string::const_iterator(_path.begin())); + if (_endpoint[pos] == mark) { + _endpoint[pos] = '\0'; + } + } else { // replace a character range (mark) + if (std::ranges::equal(_path | std::views::take(std::ranges::size(mark), mark))) { + auto pos = std::distance(_endpoint.cbegin(), std::string::const_iterator(_path.begin())); + _endpoint.replace(pos, std::ranges::size(mark), 1, '\0'); + _path = std::string_view(_endpoint.begin() + pos, _endpoint.end()); + } + } + + return *this; + } + +protected: + std::string _endpoint; + std::string_view _proto, _host, _path, _portView; + int _port; + bool _isValid; + + + virtual ssize_t checkProtoMark(std::string_view proto_mark) + { + ssize_t idx = 0; + + // case-insensitive look-up + bool found = + std::ranges::any_of(Asibfm700NetserverEndpoint::validProtoMarks, [&idx, &proto_mark](const auto& el) { + bool ok = utils::charSubrangeCompare(proto_mark, el, true); + + if (!ok) { + ++idx; + } + + return ok; + }); + + return found ? idx : -1; + } + + enum EndpointPart { PROTO_PART, HOST_PART, PATH_PART, PORT_PART }; + + template + R part(EndpointPart what) const + { + R res; + + // if (!_isValid) { + // return res; + // } + + auto part = _proto; + + switch (what) { + case PROTO_PART: + part = _proto; + break; + case HOST_PART: + part = _host; + break; + case PATH_PART: + part = _path; + break; + case PORT_PART: + part = _portView; + break; + default: + break; + } + + if constexpr (std::ranges::view) { + return {part.begin(), part.end()}; + } else { + std::ranges::copy(part, std::back_inserter(res)); + } + + return res; + } + + void copyInst(const Asibfm700NetserverEndpoint& other) + { + if (&other != this) { + if (other._isValid) { + _isValid = other._isValid; + _endpoint = other._endpoint; + _proto = other._proto; + + std::iterator_traits::difference_type idx; + if (other.isLocal()) { // for 'local' host is one of static class constants + _host = other._host; + } else { + idx = std::distance(other._endpoint.c_str(), other._host.data()); + _host = std::string_view(_endpoint.c_str() + idx, other._host.size()); + } + + idx = std::distance(other._endpoint.c_str(), other._path.data()); + _path = std::string_view(_endpoint.c_str() + idx, other._path.size()); + + idx = std::distance(other._endpoint.c_str(), other._portView.data()); + _portView = std::string_view(_endpoint.c_str() + idx, other._portView.size()); + + _port = other._port; + } else { + _isValid = false; + _endpoint = std::string(); + _proto = std::string_view(); + _host = std::string_view(); + _path = std::string_view(); + _portView = std::string_view(); + _port = -1; + } + } + } + + + void moveInst(Asibfm700NetserverEndpoint&& other) + { + if (&other != this) { + if (other._isValid) { + _isValid = std::move(other._isValid); + _endpoint = std::move(other._endpoint); + _proto = other._proto; + _host = std::move(other._host); + _path = std::move(other._path); + _port = std::move(other._port); + _portView = std::move(other._portView); + } else { + _isValid = false; + _endpoint = std::string(); + _proto = std::string_view(); + _host = std::string_view(); + _path = std::string_view(); + _portView = std::string_view(); + _port = -1; + } + } + } +}; + +} // namespace asibfm700 diff --git a/mcc/mcc_generic_mount.h b/mcc/mcc_generic_mount.h index 1fcc9fc..ce82bf3 100644 --- a/mcc/mcc_generic_mount.h +++ b/mcc/mcc_generic_mount.h @@ -110,13 +110,11 @@ struct MccGenericFsmMountCategory : public std::error_category { inline std::error_code make_error_code(MccGenericMountErrorCode ec) { return std::error_code(static_cast(ec), MccGenericMountCategory::get()); - // return std::error_code(static_cast(ec), std::generic_category()); } inline std::error_code make_error_code(MccGenericFsmMountErrorCode ec) { return std::error_code(static_cast(ec), MccGenericFsmMountCategory::get()); - // return std::error_code(static_cast(ec), std::generic_category()); } @@ -126,14 +124,12 @@ template class MccGenericMount : public HardwareT, public TelemetryT, public PZoneContT, public SlewModelT, public TrackModelT, - // public GuidingModelT, public LoggerT { public: @@ -145,7 +141,6 @@ public: using LoggerT::logWarn; - // using typename GuidingModelT::guiding_params_t; using typename SlewModelT::slewing_params_t; using typename TrackModelT::tracking_params_t; @@ -191,14 +186,14 @@ public: error_t initMount() { - logInfo("Start mount initialization ..."); + logInfo("Start generic mount initialization ..."); auto hw_err = this->hardwareInit(); if (hw_err) { return mcc_deduce_error_code(hw_err, MccGenericMountErrorCode::ERROR_HW_STOP); } - logInfo("Mount initialization was performed"); + logInfo("Generic mount initialization was performed"); return MccGenericMountErrorCode::ERROR_OK; } diff --git a/mcc/mcc_netserver.h b/mcc/mcc_netserver.h new file mode 100644 index 0000000..78920e5 --- /dev/null +++ b/mcc/mcc_netserver.h @@ -0,0 +1,765 @@ +#pragma once + +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include + +#if __has_include() // POSIX +#define FORK_EXISTS 1 +#include +#include +#endif + + + +#include "control_proto.h" +#include "mcc_generics.h" +#include "mcc_netserver_endpoint.h" +#include "mcc_traits.h" + +namespace mcc +{ + + +namespace traits +{ + +template +concept mcc_endpoint_c = std::derived_from || std::derived_from || + std::derived_from || + std::derived_from; + +template +static constexpr bool is_serial_proto = std::derived_from; + +template +static constexpr bool is_tcp_proto = + std::derived_from || std::derived_from; + +template +static constexpr bool is_local_stream_proto = std::derived_from || + std::derived_from; + +template +static constexpr bool is_local_seqpack_proto = std::derived_from || + std::derived_from; + + +} // namespace traits + + +template +class MccNetworkServer : public LoggerT +{ +public: + using LoggerT::logDebug; + using LoggerT::logError; + using LoggerT::logInfo; + using LoggerT::logWarn; + + static constexpr std::chrono::duration DEFAULT_RCV_TIMEOUT = std::chrono::hours(12); + static constexpr std::chrono::duration DEFAULT_SND_TIMEOUT = std::chrono::milliseconds(2000); + + MccNetworkServer(asio::io_context& ctx, LoggerT logger = MccNullLogger{}) + : _asioContext(ctx), _stopSignal(ctx), _restartSignal(ctx) + { + std::stringstream st; + st << std::this_thread::get_id(); + + logInfo("Create mount server instance (thread ID = {})", st.str()); + } + + ~MccNetworkServer() + { + std::stringstream st; + st << std::this_thread::get_id(); + + logInfo("Delete mount server instance (thread ID = {}) ...", st.str()); + + stopListening(); + disconnectClients(); + } + + + template + asio::awaitable listen(std::derived_from auto endpoint, CtorArgTs&&... ctor_args) + { + if (!endpoint.isValid()) { + logError("Cannot start listening! Invalid endpoint string representation ('{}')!", endpoint.endpoint()); + co_return; + } + + // add root path to endpoint one + std::filesystem::path pt("/"); + + + if (endpoint.isLocalSerial()) { + pt += endpoint.path(); + + asio::serial_port s_port(_asioContext); + + std::error_code ec; + + if constexpr (sizeof...(CtorArgTs)) { // options + setSerialOpts(s_port, std::forward(ctor_args)...); + } + + s_port.open(pt.string(), ec); + if (ec) { + logError("Cannot open serial device '{}' (Error = '{}')!", pt.string(), ec.message()); + co_return; + } + + // asio::co_spawn(_asioContext, listen(std::move(s_port)), asio::detached); + co_await listen(std::move(s_port)); + } else if (endpoint.isLocal()) { + // create abstract namespace socket endpoint if its path starts from '@' symbol + endpoint.makeAbstract('@'); + + // if (endpoint.path()[0] == '\0') { // abstract namespace + // std::string p; + // std::ranges::copy(endpoint.path(), std::back_inserter(p)); + // p.insert(p.begin() + 1, '/'); // insert after '\0' symbol + // pt = p; + // } else { + // pt += endpoint.path(); + // } + + if (endpoint.isLocalStream()) { + co_await listen(asio::local::stream_protocol::endpoint(endpoint.path(pt.string()))); + } else if (endpoint.isLocalSeqpacket()) { + co_await listen(asio::local::seq_packet_protocol::endpoint(endpoint.path(pt.string()))); + } else { + co_return; // it must not be!!!! + } + } else if (endpoint.isTCP()) { + // resolve hostname + try { + asio::ip::tcp::resolver res(_asioContext); + + auto r_result = co_await res.async_resolve(endpoint.host(), endpoint.portView(), asio::use_awaitable); + + logInfo("Resolve hostname <{}> to {} IP-addresses", endpoint.host(), r_result.size()); + + + bool exit_flag = false; + asio::ip::tcp::acceptor acc(_asioContext); + + for (auto const& epn : r_result) { + try { + // std::stringstream st; + + // logDebug("Create connection acceptor for endpoint <{}> ...", + // epn.address().to_string()); + acc = asio::ip::tcp::acceptor(_asioContext, epn); + // st << acc.local_endpoint(); + exit_flag = true; + break; + } catch (const std::system_error& err) { + logError("An error occuring while creating connection acceptor (ec = {})", err.what()); + continue; + } + } + if (!exit_flag) { + logError("Cannot start listening on any resolved endpoints!"); + co_return; + } + + _tcpAcceptors.emplace_back(&acc); + + logInfo("Start listening at <{}> endpoint ...", acc.local_endpoint().address().to_string()); + + // start accepting connections + for (;;) { + auto sock = co_await acc.async_accept(asio::use_awaitable); + // start new client session + asio::co_spawn(_asioContext, startSession(std::move(sock)), asio::detached); + } + + } catch (const std::system_error& err) { + logError("An error occured while trying to start accepting connections! ec = '{}'", err.what()); + } + } + } + + template + asio::awaitable listen(EpnT endpoint) + { + using epn_t = std::decay_t; + + std::error_code ec; + + if constexpr (traits::is_serial_proto) { + // first, check if port is open + if (!endpoint.is_open()) { + if (ec) { + // ?????????? + logError("Serial port was not open! Do not start waiting for commands!"); + } + } else { + asio::co_spawn(_asioContext, startSession(std::move(endpoint)), asio::detached); + } + + } else if constexpr (traits::is_tcp_proto || traits::is_local_stream_proto || + traits::is_local_seqpack_proto) { + try { + std::stringstream st; + + st << endpoint; + logDebug("Create connection acceptor for endpoint <{}> ...", st.str()); + auto acc = typename epn_t::protocol_type::acceptor(_asioContext, endpoint); + + st.str(""); + st << acc.local_endpoint(); + logInfo("Start listening at <{}> endpoint ...", st.str()); + + + if constexpr (traits::is_tcp_proto) { + _tcpAcceptors.emplace_back(&acc); + } else if constexpr (traits::is_local_stream_proto) { + _localStreamAcceptors.emplace_back(&acc); + } else if constexpr (traits::is_local_seqpack_proto) { + _localSeqpackAcceptors.emplace_back(&acc); + } else { + static_assert(false, "INVALID ENDPOINT!!!"); + } + + // start accepting connections + for (;;) { + auto sock = co_await acc.async_accept(asio::use_awaitable); + // start new client session + asio::co_spawn(_asioContext, startSession(std::move(sock)), asio::detached); + } + + + } catch (const std::system_error& err) { + logError("An error occured while trying to start accepting connections! ec = '{}'", err.what()); + } + } else { + static_assert(false, "INVALID ENDPOINT!!!"); + } + + co_return; + } + + + // close listening on all endpoints + void stopListening() + { + std::error_code ec; + + logInfo("Close all listening endpoints ..."); + + auto num = + _serialPorts.size() + _tcpAcceptors.size() + _localStreamAcceptors.size() + _localSeqpackAcceptors.size(); + if (!num) { + logInfo("There are no listening ports/sockets!"); + return; + } + + auto close_func = [this](auto& acc_ptrs, std::string_view desc) { + size_t N = 0, M = 0; + std::error_code ec; + + if (acc_ptrs.size()) { + logInfo("Close {} acceptors ...", desc); + + for (auto& acc : acc_ptrs) { + acc->close(ec); + if (ec) { + logError("Cannot close {} acceptor! ec = '{}'", desc, ec.message()); + } else { + ++M; + } + ++N; + } + + logDebug("{} from {} {} acceptors were closed!", M, N, desc); + + // pointers are invalidated here, so clear its container + acc_ptrs.clear(); + } + }; + + close_func(_tcpAcceptors, "TCP socket"); + close_func(_localStreamAcceptors, "local stream socket"); + close_func(_localSeqpackAcceptors, "local seqpack socket"); + + + logInfo("The all server listening endpoints were closed!"); + } + + void disconnectClients() + { + auto disconn_func = [this](std::ranges::input_range auto& ptrs) { + std::error_code ec; + for (auto& ptr : ptrs) { + // ptr->cancel(ec); + // if (ec) { + // logWarn("socket_base::cancel: an error occured (ec = {})", ec.message()); + // } + + ptr->shutdown(asio::socket_base::shutdown_both, ec); + if (ec) { + logWarn("socket_base::shutdown: an error occured (ec = {})", ec.message()); + } + + ptr->close(ec); + if (ec) { + logWarn("socket_base::close: an error occured (ec = {})", ec.message()); + } + } + }; + + logInfo("Close all client connections ..."); + + if (_serialPorts.empty() && _localStreamSockets.empty() && _localSeqpackSockets.empty() && + _tcpSockets.empty()) { + logInfo("There were no active client connections! Skip!"); + } + + if (_serialPorts.size()) { + std::lock_guard lock_g(_serialPortsMutex); + + std::error_code ec; + logInfo("Close serial port clients ({} in total) ...", _serialPorts.size()); + for (auto& ptr : _serialPorts) { + ptr->cancel(ec); + if (ec) { + logWarn("serial_port::cancel: an error occured (ec = {})", ec.message()); + } + ptr->close(ec); + if (ec) { + logWarn("serial_port::close: an error occured (ec = {})", ec.message()); + } + } + } + + if (_localStreamSockets.size()) { + std::lock_guard lock_g(_localStreamSocketsMutex); + + logInfo("Close local stream socket-type clients ({} in total) ...", _localStreamSockets.size()); + disconn_func(_localStreamSockets); + } + + if (_localSeqpackSockets.size()) { + std::lock_guard lock_g(_localSeqpackSocketsMutex); + + logInfo("Close local seqpack socket-type clients ({} in total) ...", _localSeqpackSockets.size()); + disconn_func(_localSeqpackSockets); + } + + if (_tcpSockets.size()) { + std::lock_guard lock_g(_tcpSocketsMutex); + + logInfo("Close TCP socket-type clients ({} in total) ...", _tcpSockets.size()); + disconn_func(_tcpSockets); + } + + logInfo("Client connection were closed!"); + } + + void daemonize() + { +#ifdef FORK_EXISTS + logInfo("Daemonize the server ..."); + + _asioContext.notify_fork(asio::execution_context::fork_prepare); + + auto tmp_path = std::filesystem::temp_directory_path(); + if (tmp_path.empty()) { + tmp_path = std::filesystem::current_path().root_path(); + } + + + if (pid_t pid = fork()) { + if (pid > 0) { + exit(0); + } else { + // throw std::system_error(errno, std::generic_category(), "CANNOT FORK 1-STAGE"); + logError("CANNOT FORK 1-STAGE! The server was not daemonized!"); + return; + } + } + + if (setsid() == -1) { + // throw std::system_error(errno, std::generic_category(), "CANNOT FORK SETSID"); + logError("CANNOT FORK SETSID! The server was not daemonized!"); + return; + } + + logInfo("Try to set the daemon current path to '{}' ...", tmp_path.string()); + + std::error_code ec{}; + + std::filesystem::current_path(tmp_path, ec); + if (!ec) { + logWarn("Cannot change current path to '{}'! Ignore!", tmp_path.string()); + } + + umask(0); + + if (pid_t pid = fork()) { + if (pid > 0) { + exit(0); + } else { + // throw std::system_error(errno, std::generic_category(), "CANNOT FORK 2-STAGE"); + logError("CANNOT FORK 2-STAGE! The server was not daemonized!"); + return; + } + } + + // stdin, stdout, stderr + close(0); + close(1); + close(2); + + _asioContext.notify_fork(asio::io_context::fork_child); + + logInfo("The server was daemonized successfully!"); + +#else + logWarn("Host platform is not POSIX one, so cannot daemonize the server!"); +#endif + } + + template , std::ranges::range RRT = std::vector> + void setupSignals(const RST& stop_sig_num = {SIGINT, SIGTERM}, const RRT& restart_sig_num = {SIGUSR1}) + requires(std::convertible_to, int> && + std::convertible_to, int>) + { + for (const int sig : stop_sig_num) { + _stopSignal.add(sig); + } + + _stopSignal.async_wait([this](std::error_code, int signo) { + logInfo("Stop signal was received (signo = {})", signo); + + stopListening(); + disconnectClients(); + + _asioContext.stop(); + }); + + for (const int sig : restart_sig_num) { + _restartSignal.add(sig); + } + + _restartSignal.async_wait([this](std::error_code, int signo) { + logInfo("Restart signal was received (signo = {})", signo); + restart(); + }); + } + + void restart() + { + disconnectClients(); + + _restartSignal.async_wait([this](std::error_code, int signo) { + logInfo("Restart signal was received (signo = {})", signo); + restart(); + }); + } + +private: + asio::io_context& _asioContext; + + asio::signal_set _stopSignal, _restartSignal; + + std::set _serialPorts; + // std::vector _serialPorts; + + std::vector _tcpAcceptors; + std::vector _localStreamAcceptors; + std::vector _localSeqpackAcceptors; + + std::set _tcpSockets; + std::set _localStreamSockets; + std::set _localSeqpackSockets; + // std::vector _tcpSockets; + // std::vector _localStreamSockets; + // std::vector _localSeqpackSockets; + + std::mutex _serialPortsMutex, _tcpSocketsMutex, _localStreamSocketsMutex, _localSeqpackSocketsMutex; + + // helpers + template + void setSerialOpts(asio::serial_port& s_port, OptT&& opt, OptTs&&... opts) + { + std::error_code ec; + + s_port.set_option(opt, ec); + if (ec) { + std::string_view opt_name; + + if constexpr (std::same_as) { + opt_name = "baud rate"; + } else if constexpr (std::same_as) { + opt_name = "parity"; + } else if constexpr (std::same_as) { + opt_name = "flow control"; + } else if constexpr (std::same_as) { + opt_name = "stop bits"; + } else if constexpr (std::same_as) { + opt_name = "char size"; + } + + logError("Cannot set serial port '{}' option! Just skip!", opt_name); + } + + if constexpr (sizeof...(OptTs)) { + setSerialOpts(s_port, std::forward(opts)...); + } + } + + + std::vector handleClientCommand(std::string_view command) + { + std::vector resp{BM700::CONTROL_PROTO_STR_RESP_ACK.begin(), BM700::CONTROL_PROTO_STR_RESP_ACK.end()}; + + return resp; + } + + + template + asio::awaitable startSession(auto socket, + const RCVT& rcv_timeout = DEFAULT_RCV_TIMEOUT, + const SNDT& snd_timeout = DEFAULT_SND_TIMEOUT) + { + using namespace asio::experimental::awaitable_operators; + + using sock_t = std::decay_t; + + + auto look_for_whole_msg = [](auto const& bytes) { + auto found = std::ranges::search(bytes, BM700::CONTROL_PROTO_STOP_SEQ); + return found.empty() ? std::span(bytes.begin(), bytes.begin()) : std::span(bytes.begin(), found.end()); + }; + + + asio::streambuf sbuff; + size_t nbytes; + std::stringstream st; + std::string r_epn; + + st << std::this_thread::get_id(); + std::string thr_id = st.str(); + st.str(""); + + if constexpr (traits::is_serial_proto) { + st << "serial port: " << socket.native_handle(); + } else { // network sockets + st << socket.remote_endpoint(); + } + + r_epn = st.str(); + if (r_epn.empty()) { // UNIX domain sockets + r_epn = "local"; + } + + logInfo("Start client session: remote endpoint <{}> (session thread ID = {})", r_epn, thr_id); + + try { + if constexpr (!traits::is_serial_proto) { + _serverLogger->trace("Set socket option KEEP_ALIVE to TRUE"); + socket.set_option(asio::socket_base::keep_alive(true)); + } + + if constexpr (traits::is_serial_proto) { + std::lock_guard lock_g(_serialPortsMutex); + _serialPorts.insert(&socket); + } else if constexpr (traits::is_tcp_proto) { + std::lock_guard lock_g(_tcpSocketsMutex); + // _tcpSockets.emplace_back(&socket); + _tcpSockets.insert(&socket); + } else if constexpr (traits::is_local_stream_proto) { + std::lock_guard lock_g(_localStreamSocketsMutex); + // _localStreamSockets.emplace_back(&socket); + _localStreamSockets.insert(&socket); + } else if constexpr (traits::is_local_seqpack_proto) { + std::lock_guard lock_g(_localSeqpackSocketsMutex); + // _localSeqpackSockets.emplace_back(&socket); + _localSeqpackSockets.insert(&socket); + } else { + static_assert(false, "INVALID SOCKET TTYPE!!!"); + } + + + // send buffer sequence + // initiate the second element by "stop-sequence" symbols + std::vector snd_buff_seq{ + {}, {BM700::CONTROL_PROTO_STOP_SEQ.data(), BM700::CONTROL_PROTO_STOP_SEQ.size()}}; + + asio::steady_timer timeout_timer(_asioContext); + std::variant op_res; + + std::error_code ec; + + bool do_read = true; + + // main client request -- server respond cycle + for (;;) { + // receive message + + if (do_read) { + _serverLogger->trace("Start socket/port reading operation with timeout {} ...", rcv_timeout); + + if constexpr (traits::is_serial_proto) { + nbytes = 1024; + } else { + nbytes = socket.available(); + } + + auto buff = sbuff.prepare(nbytes ? nbytes : 1); + + // timeout_timer.expires_after(std::chrono::seconds(5)); + timeout_timer.expires_after(rcv_timeout); + + if constexpr (traits::is_local_seqpack_proto) { + asio::socket_base::message_flags oflags; + + op_res = co_await ( + socket.async_receive(buff, oflags, asio::redirect_error(asio::use_awaitable, ec)) || + timeout_timer.async_wait(asio::use_awaitable)); + + } else { + op_res = co_await (asio::async_read(socket, buff, asio::transfer_at_least(1), + asio::redirect_error(asio::use_awaitable, ec)) || + timeout_timer.async_wait(asio::use_awaitable)); + } + + if (ec) { + throw std::system_error(ec); + } + + if (op_res.index()) { + throw std::system_error(std::make_error_code(std::errc::timed_out)); + } else { + nbytes = std::get<0>(op_res); + + _serverLogger->trace("{} bytes were received", nbytes); + + if constexpr (traits::is_local_seqpack_proto) { + if (!nbytes) { // EOF! + throw std::system_error(std::error_code(asio::error::misc_errors::eof)); + } + } + } + + sbuff.commit(nbytes); + } // here, the input stream buffer still contains remaining bytes. try to handle its + + auto start_ptr = static_cast(sbuff.data().data()); + + auto msg = look_for_whole_msg(std::span(start_ptr, sbuff.size())); + if (msg.empty()) { // still not whole message + _serverLogger->trace( + "It seems a partial command message was received, so waiting for remaining part ..."); + do_read = true; + continue; + } + + + // extract command without stop sequence symbols + // std::string comm; + // std::ranges::copy(msg | std::views::take(msg.size() - BM700::CONTROL_PROTO_STOP_SEQ.size()), + // std::back_inserter(comm)); + + std::string_view comm{msg.begin(), msg.end() - BM700::CONTROL_PROTO_STOP_SEQ.size()}; + + logDebug("A command [{}] was received from client (remote endpoint <{}>, thread ID = {})", comm, r_epn, + thr_id); + + auto resp = handleClientCommand(comm); + + // remove received message from the input stream buffer. NOTE: 'msg' is now invalidated!!! + sbuff.consume(msg.size()); + do_read = sbuff.size() == 0; + + + logDebug("Send respond [{}] to client (remote endpoint <{}>, thread ID = {})", + std::string_view(resp.begin(), resp.end()), r_epn, thr_id); + + // send server respond to client + snd_buff_seq[0] = {resp.data(), resp.size()}; + + timeout_timer.expires_after(snd_timeout); + if constexpr (traits::is_local_seqpack_proto) { + op_res = + co_await (socket.async_send(snd_buff_seq, 0, asio::redirect_error(asio::use_awaitable, ec)) || + timeout_timer.async_wait(asio::use_awaitable)); + } else { + // nbytes = co_await asio::async_write(socket, snd_buff_seq, asio::use_awaitable); + op_res = co_await ( + asio::async_write(socket, snd_buff_seq, asio::redirect_error(asio::use_awaitable, ec)) || + timeout_timer.async_wait(asio::use_awaitable)); + } + + if (ec) { + throw std::system_error(ec); + } + + if (op_res.index()) { + throw std::system_error(std::make_error_code(std::errc::timed_out)); + } else { + nbytes = std::get<0>(op_res); + _serverLogger->trace("{} bytes were sent", nbytes); + } + + if (nbytes != (resp.size() + BM700::CONTROL_PROTO_STOP_SEQ.size())) { // !!!!!!!!!! + } + } + + } catch (const std::system_error& ex) { + if (ex.code() == std::error_code(asio::error::misc_errors::eof)) { + logInfo("It seems client or server closed the connection (remote endpoint <{}>, thread ID = {})", r_epn, + thr_id); + } else { + logError("An error '{}' occured in client session (remote endpoint <{}>, thread ID = {})", ex.what(), + r_epn, thr_id); + } + } catch (const std::exception& ex) { + logError("An unhandled error '{}' occured in client sesssion (remote endpoint <{}>, thread ID = {})", + ex.what(), r_epn, thr_id); + } catch (...) { + logError("An unhandled error occured in client sesssion (remote endpoint <{}>, thread ID = {})", r_epn, + thr_id); + } + + // remove pointer as it is invalidated here (at the exit of the method) + if constexpr (traits::is_serial_proto) { + _serialPorts.erase(&socket); + } else if constexpr (traits::is_tcp_proto) { + _tcpSockets.erase(&socket); + } else if constexpr (traits::is_local_stream_proto) { + _localStreamSockets.erase(&socket); + } else if constexpr (traits::is_local_seqpack_proto) { + _localSeqpackSockets.erase(&socket); + } else { + static_assert(false, "INVALID SOCKET TTYPE!!!"); + } + + logInfo("Close client session: remote endpoint <{}> (thread ID = {})", r_epn, thr_id); + } +}; + +} // namespace mcc diff --git a/mcc/mcc_netserver_endpoint.h b/mcc/mcc_netserver_endpoint.h new file mode 100644 index 0000000..5b0fa6c --- /dev/null +++ b/mcc/mcc_netserver_endpoint.h @@ -0,0 +1,512 @@ +#pragma once + +/* MOUNT CONTROL COMPONENTS LIBRARY */ + + + +/* NETWORK SERVER ENDPOINT CLASS IMPLEMENTATION */ + +#include +#include +#include +#include +#include +#include +#include + +#include "mcc_traits.h" + +namespace mcc +{ + +namespace utils +{ + +static constexpr bool mcc_char_subrange_compare(const traits::mcc_char_view auto& what, + const traits::mcc_char_view auto& where, + bool case_insensitive = false) +{ + if (std::ranges::size(what) == std::ranges::size(where)) { + if (case_insensitive) { + auto f = std::ranges::search(where, + std::views::transform(what, [](const char& ch) { return std::tolower(ch); })); + return !f.empty(); + } else { + auto f = std::ranges::search(where, what); + return !f.empty(); + } + } + + return false; +} + +} // namespace utils + +/* + * Very simple various protocols endpoint parser and holder class + * + * endpoint: proto_mark://host_name:port_num/path + * where "part" is optional for all non-local protocol kinds; + * + * for local kind of protocols the endpoint must be given as: + * local://stream/PATH + * local://seqpacket/PATH + * local://serial/PATH + * where 'stream' and 'seqpacket' "host_name"-field marks the + * stream-type and seqpacket-type UNIX domain sockets protocols; + * 'serial' marks a serial (RS232/485) protocol. + * here, possible "port_num" field is allowed but ignored. + * + * NOTE: "proto_mark" and "host_name" (for local kind) fields are parsed in case-insensitive manner! + * + * EXAMPLES: tcp://192.168.70.130:3131 + * local://serial/dev/ttyS1 + * local://seqpacket/tmp/BM70_SERVER_SOCK + * + * + */ + + +class MccServerEndpoint +{ +public: + static constexpr std::string_view protoHostDelim = "://"; + static constexpr std::string_view hostPortDelim = ":"; + static constexpr std::string_view portPathDelim = "/"; + + enum proto_id_t : uint8_t { + PROTO_ID_LOCAL, + PROTO_ID_SEQLOCAL, + PROTO_ID_SERLOCAL, + PROTO_ID_TCP, + PROTO_ID_TLS, + PROTO_ID_UNKNOWN + }; + + static constexpr std::string_view protoMarkLocal{"local"}; // UNIX domain + static constexpr std::string_view protoMarkTCP{"tcp"}; // TCP + static constexpr std::string_view protoMarkTLS{"tls"}; // TLS + + static constexpr std::array validProtoMarks{protoMarkLocal, protoMarkTCP, protoMarkTLS}; + + + static constexpr std::string_view localProtoTypeStream{"stream"}; // UNIX domain stream + static constexpr std::string_view localProtoTypeSeqpacket{"seqpacket"}; // UNIX domain seqpacket + static constexpr std::string_view localProtoTypeSerial{"serial"}; // serial (RS232/485) + + static constexpr std::array validLocalProtoTypes{localProtoTypeStream, localProtoTypeSeqpacket, + localProtoTypeSerial}; + + + template + MccServerEndpoint(const R& ept) + { + fromRange(ept); + } + + MccServerEndpoint(const MccServerEndpoint& other) + { + copyInst(other); + } + + MccServerEndpoint(MccServerEndpoint&& other) + { + moveInst(std::move(other)); + } + + virtual ~MccServerEndpoint() = default; + + + MccServerEndpoint& operator=(const MccServerEndpoint& other) + { + copyInst(other); + + return *this; + } + + + MccServerEndpoint& operator=(MccServerEndpoint&& other) + { + moveInst(std::move(other)); + + return *this; + } + + template + requires std::ranges::contiguous_range + bool fromRange(const R& ept) + { + _isValid = false; + + // at least 'ws://a' (proto, proto-host delimiter and at least a single character of hostname) + if (std::ranges::size(ept) < 6) { + return _isValid; + } + + if constexpr (std::is_array_v>) { + _endpoint = ept; + } else { + _endpoint.clear(); + std::ranges::copy(ept, std::back_inserter(_endpoint)); + } + + auto found = std::ranges::search(_endpoint, protoHostDelim); + if (found.empty()) { + return _isValid; + } + + + ssize_t idx; + if ((idx = checkProtoMark(std::string_view{_endpoint.begin(), found.begin()})) < 0) { + return _isValid; + } + + _proto = validProtoMarks[idx]; + + _host = std::string_view{found.end(), _endpoint.end()}; + + auto f1 = std::ranges::search(_host, portPathDelim); + // std::string_view port_sv; + if (f1.empty() && isLocal()) { // no path, but it is mandatory for 'local'! + return _isValid; + } else { + _host = std::string_view(_host.begin(), f1.begin()); + + _path = std::string_view(f1.end(), &*_endpoint.end()); + + f1 = std::ranges::search(_host, hostPortDelim); + if (f1.empty() && !isLocal()) { // no port, but it is mandatory for non-local! + return _isValid; + } + + _portView = std::string_view(f1.end(), _host.end()); + if (_portView.size()) { + _host = std::string_view(_host.begin(), f1.begin()); + + if (!isLocal()) { + // convert port string to int + auto end_ptr = _portView.data() + _portView.size(); + + auto [ptr, ec] = std::from_chars(_portView.data(), end_ptr, _port); + if (ec != std::errc() || ptr != end_ptr) { + return _isValid; + } + } else { // ignore for local + _port = -1; + } + } else { + _port = -1; + } + + if (isLocal()) { // check for special values + idx = 0; + if (std::ranges::any_of(validLocalProtoTypes, [&idx, this](const auto& el) { + bool ok = utils::mcc_char_subrange_compare(_host, el, true); + if (!ok) { + ++idx; + } + return ok; + })) { + _host = validLocalProtoTypes[idx]; + } else { + return _isValid; + } + } + } + + _isValid = true; + + return _isValid; + } + + + bool isValid() const + { + return _isValid; + } + + + auto endpoint() const + { + return _endpoint; + } + + template + R proto() const + { + return part(PROTO_PART); + } + + std::string_view proto() const + { + return proto(); + } + + template + R host() const + { + return part(HOST_PART); + } + + std::string_view host() const + { + return host(); + } + + int port() const + { + return _port; + } + + template + R portView() const + { + return part(PORT_PART); + } + + std::string_view portView() const + { + return portView(); + } + + template + R path(RR&& root_path) const + { + if (_path.empty()) { + if constexpr (traits::mcc_output_char_range) { + R res; + std::ranges::copy(std::forward(root_path), std::back_inserter(res)); + + return res; + } else { // can't add root path!!! + return part(PATH_PART); + } + } + + auto N = std::ranges::distance(root_path.begin(), root_path.end()); + + if (N) { + R res; + std::filesystem::path pt(root_path.begin(), root_path.end()); + + if (isLocal() && _path[0] == '\0') { + std::ranges::copy(std::string_view(" "), std::back_inserter(res)); + pt /= _path.substr(1); + std::ranges::copy(pt.string(), std::back_inserter(res)); + *res.begin() = '\0'; + } else { + pt /= _path; + std::ranges::copy(pt.string(), std::back_inserter(res)); + } + + return res; + } else { + return part(PATH_PART); + } + } + + template + std::string path(RR&& root_path) const + { + return path(std::forward(root_path)); + } + + template + R path() const + { + return part(PATH_PART); + } + + std::string_view path() const + { + return path(); + } + + + bool isLocal() const + { + return proto() == protoMarkLocal; + } + + bool isLocalStream() const + { + return host() == localProtoTypeStream; + } + + bool isLocalSerial() const + { + return host() == localProtoTypeSerial; + } + + bool isLocalSeqpacket() const + { + return host() == localProtoTypeSeqpacket; + } + + + bool isTCP() const + { + return proto() == protoMarkTCP; + } + + bool isTLS() const + { + return proto() == protoMarkTLS; + } + + + // add '\0' char (or replace special-meaning char/char-sequence) to construct UNIX abstract namespace + // endpoint path + template + MccServerEndpoint& makeAbstract(const T& mark = nullptr) + requires(traits::mcc_input_char_range || std::same_as, char> || + std::is_null_pointer_v>) + { + if (!(isLocalStream() || isLocalSeqpacket())) { // only local proto is valid! + return *this; + } + + if constexpr (std::is_null_pointer_v) { // just insert '\0' + auto it = _endpoint.insert(std::string::const_iterator(_path.begin()), '\0'); + _path = std::string_view(it, _endpoint.end()); + } else if constexpr (std::same_as, char>) { // replace a character (mark) + auto pos = std::distance(_endpoint.cbegin(), std::string::const_iterator(_path.begin())); + if (_endpoint[pos] == mark) { + _endpoint[pos] = '\0'; + } + } else { // replace a character range (mark) + if (std::ranges::equal(_path | std::views::take(std::ranges::size(mark), mark))) { + auto pos = std::distance(_endpoint.cbegin(), std::string::const_iterator(_path.begin())); + _endpoint.replace(pos, std::ranges::size(mark), 1, '\0'); + _path = std::string_view(_endpoint.begin() + pos, _endpoint.end()); + } + } + + return *this; + } + +protected: + std::string _endpoint; + std::string_view _proto, _host, _path, _portView; + int _port; + bool _isValid; + + + virtual ssize_t checkProtoMark(std::string_view proto_mark) + { + ssize_t idx = 0; + + // case-insensitive look-up + bool found = std::ranges::any_of(MccServerEndpoint::validProtoMarks, [&idx, &proto_mark](const auto& el) { + bool ok = utils::mcc_char_subrange_compare(proto_mark, el, true); + + if (!ok) { + ++idx; + } + + return ok; + }); + + return found ? idx : -1; + } + + enum EndpointPart { PROTO_PART, HOST_PART, PATH_PART, PORT_PART }; + + template + R part(EndpointPart what) const + { + R res; + + // if (!_isValid) { + // return res; + // } + + auto part = _proto; + + switch (what) { + case PROTO_PART: + part = _proto; + break; + case HOST_PART: + part = _host; + break; + case PATH_PART: + part = _path; + break; + case PORT_PART: + part = _portView; + break; + default: + break; + } + + if constexpr (std::ranges::view) { + return {part.begin(), part.end()}; + } else { + std::ranges::copy(part, std::back_inserter(res)); + } + + return res; + } + + void copyInst(const MccServerEndpoint& other) + { + if (&other != this) { + if (other._isValid) { + _isValid = other._isValid; + _endpoint = other._endpoint; + _proto = other._proto; + + std::iterator_traits::difference_type idx; + if (other.isLocal()) { // for 'local' host is one of static class constants + _host = other._host; + } else { + idx = std::distance(other._endpoint.c_str(), other._host.data()); + _host = std::string_view(_endpoint.c_str() + idx, other._host.size()); + } + + idx = std::distance(other._endpoint.c_str(), other._path.data()); + _path = std::string_view(_endpoint.c_str() + idx, other._path.size()); + + idx = std::distance(other._endpoint.c_str(), other._portView.data()); + _portView = std::string_view(_endpoint.c_str() + idx, other._portView.size()); + + _port = other._port; + } else { + _isValid = false; + _endpoint = std::string(); + _proto = std::string_view(); + _host = std::string_view(); + _path = std::string_view(); + _portView = std::string_view(); + _port = -1; + } + } + } + + + void moveInst(MccServerEndpoint&& other) + { + if (&other != this) { + if (other._isValid) { + _isValid = std::move(other._isValid); + _endpoint = std::move(other._endpoint); + _proto = other._proto; + _host = std::move(other._host); + _path = std::move(other._path); + _port = std::move(other._port); + _portView = std::move(other._portView); + } else { + _isValid = false; + _endpoint = std::string(); + _proto = std::string_view(); + _host = std::string_view(); + _path = std::string_view(); + _portView = std::string_view(); + _port = -1; + } + } + } +}; + +} // namespace mcc diff --git a/mcc/mcc_netserver_proto.h b/mcc/mcc_netserver_proto.h new file mode 100644 index 0000000..98c5da7 --- /dev/null +++ b/mcc/mcc_netserver_proto.h @@ -0,0 +1,124 @@ +#pragma once + +#include +#include "mcc_angle.h" + +namespace mcc +{ + +/* + * network communication message format: + * keyword[[key-param-delim]param1[param-param-delim][param2]...] + * e.g. + * "target 12:23:45.56 00:32:21.978\n" + */ + + +/* message */ +static constexpr std::string_view MCC_COMMPROTO_STOP_SEQ = "\n"; +static constexpr std::string_view MCC_COMMPROTO_KEYPARAM_DELIM_DEQ = " "; +static constexpr std::string_view MCC_COMMPROTO_PARAMPARAM_DELIM_DEQ = " "; + + +/* server special response keywords */ + +static constexpr std::string_view MCC_COMMPROTO_SERVER_RESP_ACK = "ACK"; // ACK +static constexpr std::string_view MCC_COMMPROTO_SERVER_RESP_ERROR = "ERROR"; // mount operational error +// pre-defined errors +static constexpr std::string_view MCC_COMMPROTO_SERVER_RESP_ERROR_INVKEY = "INVKEY"; // invalid keyword +static constexpr std::string_view MCC_COMMPROTO_SERVER_RESP_ERROR_INVPAR = "INVPAR"; // invalid parameter + +/* predefined parameters */ + +static constexpr std::string_view MCC_COMMPROTO_COORD_KIND_RADEC_ICRS = "RADEC_ICRS"; +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"; +static constexpr std::string_view MCC_COMMPROTO_COORD_KIND_AZZD = "AZZD"; +static constexpr std::string_view MCC_COMMPROTO_COORD_KIND_AZALT = "AZALT"; +static constexpr std::string_view MCC_COMMPROTO_COORD_KIND_XY = "XY"; + + +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; +} + +/* keywords */ + +// NOTE: THE COORDINATES AND TIME-RELATED QUANTITIES CAN BE EXPRESSED IN THE TWO FORMAT: +// 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' + + +// 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 of returned coordinates +static constexpr std::string_view MCC_COMMPROTO_KEYWORD_COORDPREC_STR = "COORDPREC"; + + +// set/get target coordinates +// "TRAGET 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 mount 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" -> "AC MOUNT 1.2332325 54.23321312 AZALT\n" +// "MOUNT AZAL\n" -> "ERROR INVPAR\n" (invalid parameter of coordinates pair kind) +// "MOUNT\n" -> "AC MOUNT 1.2332325 54.23321312 AZZD\n" for alt-azimuthal mount +// "MOUNT\n" -> "AC 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_TELEMTRY_STR = "TELEMETRY"; + + +} // namespace mcc