diff --git a/CMakeLists.txt b/CMakeLists.txt index ae037d3..9a5ae99 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -17,6 +17,9 @@ endif() add_compile_options(-Wall -Wextra -Wsign-conversion) +# максимальный размер тела запроса 200mb +add_definitions(-DHTTP_MAX_PAYLOAD=200000000) + add_subdirectory(dependencies/control_system) add_executable(terminal-web-server diff --git a/src/main.cpp b/src/main.cpp index 5cb1473..1844b12 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -93,6 +93,7 @@ public: // а эти стили нельзя кешировать в отладочной версии static constexpr const char* STYLE_CSS = "static/style.css"; static constexpr const char* FIELDS_CSS = "static/fields.css"; + static constexpr const char* KB_MP4 = "static/video_2024-11-06_15-49-35.mp4"; ServerResources(const ServerResources&) = delete; @@ -105,6 +106,7 @@ public: sf->registerFile(FAVICON_ICO, mime_types::image_png, true); sf->registerFile(KROKODIL_GIF, mime_types::image_gif, true); sf->registerFile(VUE_JS, mime_types::javascript, true); + sf->registerFile(KB_MP4, mime_types::video_mp4, true); #if USE_DEBUG constexpr bool allowCacheCss = false; @@ -172,6 +174,7 @@ public: s.resources.emplace_back(std::make_unique("/style.css", [this](const auto& req, auto& rep) { boost::ignore_unused(req); sf->serve(STYLE_CSS, rep); })); s.resources.emplace_back(std::make_unique("/fields.css", [this](const auto& req, auto& rep) { boost::ignore_unused(req); sf->serve(FIELDS_CSS, rep); })); s.resources.emplace_back(std::make_unique("/js/vue.js", [this](const auto& req, auto& rep) { boost::ignore_unused(req); sf->serve(VUE_JS, rep); })); + s.resources.emplace_back(std::make_unique("/vid/video_2024-11-06_15-49-35.mp4", [this](const auto& req, auto& rep) { boost::ignore_unused(req); sf->serve(KB_MP4, rep); })); s.resources.emplace_back(std::make_unique("/api/statistics", [](const auto& req, auto& rep) { if (req.method != "GET") { diff --git a/src/server/mime_types.cpp b/src/server/mime_types.cpp index d433547..ec66b3c 100644 --- a/src/server/mime_types.cpp +++ b/src/server/mime_types.cpp @@ -36,6 +36,7 @@ std::string http::server::mime_types::toString(Mime m) { case text_plain: return "text/plain"; case text_html: return "text/html"; case text_css: return "text/css"; + case video_mp4: return "video/mp4"; case json: return "application/json"; case javascript: return "application/javascript"; case blob: diff --git a/src/server/mime_types.hpp b/src/server/mime_types.hpp index 7079d51..74f51c5 100644 --- a/src/server/mime_types.hpp +++ b/src/server/mime_types.hpp @@ -15,6 +15,7 @@ namespace http::server::mime_types { text_plain, // text/plain text_html, // text/html text_css, // text/css + video_mp4, // video/mp4 json, // application/json javascript, // application/javascript blob // application/octet-stream diff --git a/src/server/request_parser.cpp b/src/server/request_parser.cpp index aebc060..637924d 100644 --- a/src/server/request_parser.cpp +++ b/src/server/request_parser.cpp @@ -70,6 +70,12 @@ namespace http::server { RequestParser::result_type RequestParser::consume(Request &req, char input) { switch (state_) { + case expecting_payload: + req.payload.push_back(input); + if (req.payload.size() <= contentLenghtHeader - 1) { + return indeterminate; + } + return good; case method_start: if (!is_char(input) || is_ctl(input) || is_tspecial(input)) { return bad; @@ -82,36 +88,34 @@ namespace http::server { if (input == ' ') { state_ = uri; return indeterminate; - } else if (!is_char(input) || is_ctl(input) || is_tspecial(input)) { - return bad; - } else { - req.method.push_back(input); - return indeterminate; } + if (!is_char(input) || is_ctl(input) || is_tspecial(input)) { + return bad; + } + req.method.push_back(input); + return indeterminate; case uri: if (input == ' ') { state_ = http_version_h; return indeterminate; - } else if (is_ctl(input)) { - return bad; - } else { - req.queryUri.push_back(input); - return indeterminate; } + if (is_ctl(input)) { + return bad; + } + req.queryUri.push_back(input); + return indeterminate; case http_version_h: if (input == 'H') { state_ = http_version_t_1; return indeterminate; - } else { - return bad; } + return bad; case http_version_t_1: if (input == 'T') { state_ = http_version_t_2; return indeterminate; - } else { - return bad; } + return bad; case http_version_t_2: if (input == 'T') { state_ = http_version_p; @@ -210,12 +214,12 @@ namespace http::server { if (input == ':') { state_ = space_before_header_value; return indeterminate; - } else if (!is_char(input) || is_ctl(input) || is_tspecial(input)) { - return bad; - } else { - req.headers.back().name.push_back(input); - return indeterminate; } + if (!is_char(input) || is_ctl(input) || is_tspecial(input)) { + return bad; + } + req.headers.back().name.push_back(input); + return indeterminate; case space_before_header_value: if (input == ' ') { state_ = header_value; @@ -249,15 +253,12 @@ namespace http::server { } contentLenghtHeader = std::stol(content_len); state_ = expecting_payload; + if (contentLenghtHeader > HTTP_MAX_PAYLOAD) { + return bad; + } return indeterminate; } return bad; - case expecting_payload: - req.payload.push_back(input); - if (req.payload.size() <= contentLenghtHeader - 1) { - return indeterminate; - } - return good; default: return bad; diff --git a/src/server/server.cpp b/src/server/server.cpp index a4dcff2..5e5068c 100644 --- a/src/server/server.cpp +++ b/src/server/server.cpp @@ -1,6 +1,7 @@ #include "server.hpp" #include #include +#include namespace http::server { @@ -126,7 +127,12 @@ namespace http::server { if (res->path != req.url->path) { continue; } - res->handle(req, rep); + try { + res->handle(req, rep); + } catch (std::exception& e) { + BOOST_LOG_TRIVIAL(error) << "Server::requestHandler(): what = " << e.what(); + stockReply(internal_server_error, rep); + } return; } diff --git a/src/terminal_api_driver.cpp b/src/terminal_api_driver.cpp index c868054..42911d7 100644 --- a/src/terminal_api_driver.cpp +++ b/src/terminal_api_driver.cpp @@ -1,41 +1,13 @@ #include "terminal_api_driver.h" -#include +#include #include "terminal_api/ControlProtoCInterface.h" #include -#include #include -/* -timer_->timeout().connect([=]{ - double txpwd = 0; - CP_GetGain(sid, "TXPWD", &txpwd); - transmitt_active->set_end(!txpwd); - CP_GetLevelDemod(sid, "modcod_tx", &txpwd); - modcod_tx->setText(std::to_string(static_cast(txpwd) >> 2)); - uint8_t type_pack = static_cast(txpwd); - type_pack = type_pack & 0b00000010; - if(type_pack) - type_pack_lbl->setText("short"); - else - type_pack_lbl->setText("normal"); +#include "../dependencies/control_system/common/protocol_commands.h" - uint8_t is_pilots = static_cast(txpwd); - is_pilots = is_pilots & 0b00000001; - if(is_pilots) - pilot_lbl->setText("pilots"); - else - pilot_lbl->setText("no pilots"); - - CP_GetLevelDemod(sid, "snr_acm", &txpwd); - std::stringstream buf_double; - buf_double << std::fixed << - std::setprecision(2) << txpwd; - snr_acm->setText(buf_double.str()); -}); - - */ api_driver::ApiDriver::ApiDriver() { CP_Login("admin", "pass", &sid, &access); @@ -62,9 +34,29 @@ static bool DriverCP_GetCinC(TSID sid) { return s.is_cinc; } -std::string api_driver::ApiDriver::loadTerminalState() { +void writeDouble(std::ostream& out, double value, int prec = 2) { + if (std::isnan(value) || std::isinf(value)) { + out << "\"nan\""; + } else { + out << std::fixed << std::setprecision(prec) << value; + } +} + +double translateCoordinates(uint8_t deg, uint8_t min) { + return static_cast(deg) + static_cast(min) / 60; +} + +std::tuple translateCoordinates(double abs) { + auto deg = static_cast(abs); + double min_double = (abs - deg) * 60; + auto min = static_cast(min_double); + return std::make_tuple(deg, min); +} + + +std::string api_driver::ApiDriver::loadTerminalState() const { std::stringstream result; - result << "{\"initState\":" << buildEscapedString(this->deviceInitState); + result << "{\n\"initState\":" << buildEscapedString(this->deviceInitState); modulator_state modulator{}; CP_GetModulatorState(sid, modulator); @@ -80,9 +72,9 @@ std::string api_driver::ApiDriver::loadTerminalState() { result << ",\"isCinC\":" << boolAsStr(isCinC); // формируем структуру для TX - result << ",\"tx.state\":" << boolAsStr(modulator.is_tx_on); + result << ",\n\"tx.state\":" << boolAsStr(modulator.is_tx_on); result << ",\"tx.modcod\":" << modulator.modcod; - result << ",\"tx.snr\":" << std::fixed << std::setprecision(2) << modulator.snr_remote; + result << ",\"tx.snr\":"; writeDouble(result, modulator.snr_remote); if (modulator.is_short) { result << R"(,"tx.frameSize":"short")"; @@ -96,18 +88,18 @@ std::string api_driver::ApiDriver::loadTerminalState() { result << R"(,"tx.pilots":"no pilots")"; } - result << ",\"tx.speedOnTxKbit\":" << std::fixed << std::setprecision(2) << static_cast(modulator.speed_in_bytes_tx) / 128.0; - result << ",\"tx.speedOnIifKbit\":" << std::fixed << std::setprecision(2) << (static_cast(modulator.speed_in_bytes_tx_iface) / 128.0); + result << ",\"tx.speedOnTxKbit\":"; writeDouble(result, static_cast(modulator.speed_in_bytes_tx) / 128.0); + result << ",\"tx.speedOnIifKbit\":"; writeDouble(result, (static_cast(modulator.speed_in_bytes_tx_iface) / 128.0)); // формируем структуру для RX - result << ",\"rx.state\":" << boolAsStr(demodulator.locks.sym_sync_lock && demodulator.locks.freq_lock && demodulator.locks.afc_lock && demodulator.locks.pkt_sync); + result << ",\n\"rx.state\":" << boolAsStr(demodulator.locks.sym_sync_lock && demodulator.locks.freq_lock && demodulator.locks.afc_lock && demodulator.locks.pkt_sync); result << ",\"rx.sym_sync_lock\":" << boolAsStr(demodulator.locks.sym_sync_lock); result << ",\"rx.freq_search_lock\":" << boolAsStr(demodulator.locks.freq_lock); result << ",\"rx.afc_lock\":" << boolAsStr(demodulator.locks.afc_lock); result << ",\"rx.pkt_sync\":" << boolAsStr(demodulator.locks.pkt_sync); - result << ",\"rx.snr\":" << std::fixed << std::setprecision(2) << demodulator.snr; - result << ",\"rx.rssi\":" << std::fixed << std::setprecision(2) << demodulator.rssi; + result << ",\"rx.snr\":"; writeDouble(result, demodulator.snr); + result << ",\"rx.rssi\":"; writeDouble(result, demodulator.rssi); result << ",\"rx.modcod\":" << demodulator.modcod; if (demodulator.is_short) { @@ -122,13 +114,13 @@ std::string api_driver::ApiDriver::loadTerminalState() { result << R"(,"rx.pilots":"no pilots")"; } - result << ",\"rx.symError\":" << std::fixed << std::setprecision(2) << demodulator.sym_err; - result << ",\"rx.freqErr\":" << std::fixed << std::setprecision(2) << demodulator.crs_freq_err; - result << ",\"rx.freqErrAcc\":" << std::fixed << std::setprecision(2) << demodulator.fine_freq_err; - result << ",\"rx.inputSignalLevel\":" << std::fixed << std::setprecision(2) << demodulator.if_overload; - result << ",\"rx.pllError\":" << std::fixed << std::setprecision(2) << demodulator.afc_err; - result << ",\"rx.speedOnRxKbit\":" << std::fixed << std::setprecision(2) << (static_cast(demodulator.speed_in_bytes_rx) / 128.0); - result << ",\"rx.speedOnIifKbit\":" << std::fixed << std::setprecision(2) << (static_cast(demodulator.speed_in_bytes_rx_iface) / 128.0); + result << ",\n\"rx.symError\":"; writeDouble(result, demodulator.sym_err); + result << ",\"rx.freqErr\":"; writeDouble(result, demodulator.crs_freq_err); + result << ",\"rx.freqErrAcc\":"; writeDouble(result, demodulator.fine_freq_err); + result << ",\"rx.inputSignalLevel\":"; writeDouble(result, demodulator.if_overload); + result << ",\"rx.pllError\":"; writeDouble(result, demodulator.afc_err); + result << ",\"rx.speedOnRxKbit\":"; writeDouble(result, static_cast(demodulator.speed_in_bytes_rx) / 128.0); + result << ",\"rx.speedOnIifKbit\":"; writeDouble(result, static_cast(demodulator.speed_in_bytes_rx_iface) / 128.0); result << ",\"rx.packetsOk\":" << demodulator.packet_ok_cnt; result << ",\"rx.packetsBad\":" << demodulator.packet_bad_cnt; result << ",\"rx.packetsDummy\":" << demodulator.dummy_cnt; @@ -148,7 +140,7 @@ std::string api_driver::ApiDriver::loadTerminalState() { result << R"(,"cinc.correlator":null)"; } - result << ",\"cinc.occ\":" << std::fixed << std::setprecision(3) << state_cinc.ratio_signal_signal; + result << ",\n\"cinc.occ\":"; writeDouble(result, state_cinc.ratio_signal_signal, 3); result << ",\"cinc.correlatorFails\":" << state_cinc.cnt_bad_lock; result << ",\"cinc.freqErr\":" << state_cinc.freq_error_offset; result << ",\"cinc.freqErrAcc\":" << state_cinc.freq_fine_estimate; @@ -158,9 +150,9 @@ std::string api_driver::ApiDriver::loadTerminalState() { } // структура температур девайса - result << ",\"device.adrv\":" << std::fixed << std::setprecision(3) << device.adrv_temp; - result << ",\"device.fpga\":" << std::fixed << std::setprecision(3) << device.pl_temp; - result << ",\"device.zync\":" << std::fixed << std::setprecision(3) << device.zynq_temp; + result << ",\n\"device.adrv\":"; writeDouble(result, device.adrv_temp, 1); + result << ",\"device.fpga\":"; writeDouble(result, device.pl_temp, 1); + result << ",\"device.zync\":"; writeDouble(result, device.zynq_temp, 1); result << "}"; @@ -173,54 +165,100 @@ void api_driver::ApiDriver::resetPacketStatistics() const { CP_GetDmaDebug(sid, "reset_cnt_rx", &tmp); } -std::string api_driver::ApiDriver::loadSettings() { - // modulator_settings modSettings{}; - // CP_GetModulatorSettings(sid, modSettings); - // - // modSettings.baudrate = 2000000; - // modSettings.central_freq_in_kGz = 1340000.24; - // modSettings.rollof = 0.25; - // modSettings.tx_is_on = true; - // modSettings.is_test_data = false; - // modSettings.is_save_current_state = true; - // modSettings.is_cinc = false; - // modSettings.is_carrier = true; - // CP_SetModulatorSettings(sid, modSettings); - // demodulator_settings demodulator_sett; - // demodulator_sett.baudrate = 1340000; - // demodulator_sett.central_freq_in_kGz = 2400000.34; - // demodulator_sett.is_rvt_iq = true; - // demodulator_sett.is_aru_on = true; - // demodulator_sett.gain = 13; - // demodulator_sett.rollof = 0.15; - // if(CP_SetDemodulatorSettings(sid, demodulator_sett)== OK) - // { - // std::cout << "OK SET DEMODULATOR SETTINGS" << std::endl; - // } - // demodulator_settings demodulator_settings_; - // if(CP_GetDemodulatorSettings(sid,demodulator_settings_) == OK) - // { - // std::cout << "OK GET DEMODULATOR SETTINGS" << std::endl; - // std::cout << demodulator_settings_.baudrate << std::endl; - // std::cout << demodulator_settings_.gain<< std::endl; - // std::cout << demodulator_settings_.rollof << std::endl; - // std::cout << demodulator_settings_.is_aru_on << std::endl; - // std::cout << demodulator_settings_.is_rvt_iq << std::endl; - // std::cout << demodulator_settings_.central_freq_in_kGz << std::endl; - // } - // modulator_settings modulator_settings_; - // if(CP_GetModulatorSettings(sid,modulator_settings_)== OK) - // { - // std::cout << "OK GET MODULATOR SETTINGS" << std::endl; - // std::cout << modulator_settings_.baudrate << std::endl; - // std::cout << modulator_settings_.attenuation << std::endl; - // std::cout << modulator_settings_.central_freq_in_kGz << std::endl; - // std::cout << modulator_settings_.is_carrier << std::endl; - // std::cout << modulator_settings_.is_cinc << std::endl; - // std::cout << modulator_settings_.rollof << std::endl; - // } +std::string api_driver::ApiDriver::loadSettings() const { + /* +param: { + cinc: { + cinc.mode: null, // 'positional' | 'delay' + cinc.searchBandwidth: 0, // полоса поиска в кГц + cinc.position.station.latitude: 0, + cinc.position.station.longitude: 0, + cinc.position.satelliteLongitude: 0, + cinc.delayMin: 0, + cinc.delayMax: 0 + }, + buc: { + buc.refClk10M: false, // подача опоры 10MHz + buc.powering: 0 // 0, 24, 48 + }, + lnb: { + lnb.refClk10M: false, // подача опоры 10MHz + lnb.powering: 0 // 0, 13, 18, 24 + }, + serviceSettings: { + serviceSettings.refClk10M: false, // подача опоры 10MHz + serviceSettings.startDelay: 0, // задержка включения передачи + serviceSettings.autoStart: false + }, +}, + */ + constexpr auto* UNKNOWN = "\"?\""; - return "{}"; + modulator_settings modSettings{}; + CP_GetModulatorSettings(sid, modSettings); + uint32_t modulatorModcod; + CP_GetModulatorParams(sid, "modcod", &modulatorModcod); + + demodulator_settings demodSettings{}; + CP_GetDemodulatorSettings(sid, demodSettings); + ACM_parameters_serv_ acmSettings{}; + // CP_GetAcmParams(sid, &acmSettings); + DPDI_parmeters dpdiSettings{}; + // CP_GetDpdiParams(sid, &dpdiSettings); + + std::stringstream result; + result << "{\n\"general.iscInC\":" << boolAsStr(modSettings.is_cinc); + result << ",\"general.txEn\":" << boolAsStr(modSettings.tx_is_on); + result << ",\"general.modulatorMode\":" << (modSettings.is_carrier ? "\"normal\"" : "\"test\""); + result << ",\"general.autoStartTx\":" << boolAsStr(modSettings.is_save_current_state); + result << ",\"general.isTestInputData\":" << boolAsStr(modSettings.is_test_data); + + result << ",\n\"tx.attenuation\":"; writeDouble(result, modSettings.attenuation); + result << ",\"tx.rolloff\":"; writeDouble(result, modSettings.rollof); + result << ",\"tx.cymRate\":" << modSettings.baudrate; + result << ",\"tx.centerFreq\":"; writeDouble(result, modSettings.central_freq_in_kGz, 3); + + result << ",\n\"dvbs2.isAcm\":" << boolAsStr(acmSettings.enable); + result << ",\"dvbs2.frameSize\":" << ((modulatorModcod & 2) ? "\"short\"" : "\"normal\""); + // result << ",\"dvbs2.pilots\":" << "null"; + result << ",\"dvbs2.ccm_modcod\":" << (modulatorModcod >> 4); + result << ",\"dvbs2.acm_maxModcod\":" << (acmSettings.max_modcod >> 2); + result << ",\"dvbs2.acm_minModcod\":" << (acmSettings.min_modcod >> 2); + result << ",\"dvbs2.snrReserve\":" << UNKNOWN; + result << ",\"dvbs2.servicePacketPeriod\":" << UNKNOWN; + + result << ",\n\"acm.en\":" << boolAsStr(acmSettings.enable_auto_atten); + result << ",\"acm.maxAttenuation\":"; writeDouble(result, acmSettings.max_attenuation); + result << ",\"acm.minAttenuation\":"; writeDouble(result, acmSettings.min_attenuation); + result << ",\"acm.requiredSnr\":" << UNKNOWN; + + result << ",\n\"rx.gainMode\":" << (demodSettings.is_aru_on ? "\"auto\"" : "\"manual\""); + result << ",\"rx.manualGain\":"; writeDouble(result, demodSettings.gain); + result << ",\"rx.spectrumInversion\":" << boolAsStr(demodSettings.is_rvt_iq); + result << ",\"rx.rolloff\":"; writeDouble(result, demodSettings.rollof); + result << ",\"rx.cymRate\":" << demodSettings.baudrate; + result << ",\"rx.centerFreq\":"; writeDouble(result, demodSettings.central_freq_in_kGz); + + result << ",\n\"cinc.mode\":" << (dpdiSettings.is_delay_window ? "\"delay\"" : "\"positional\""); + result << ",\"cinc.searchBandwidth\":" << dpdiSettings.freq_offset; // полоса поиска в кГц + result << ",\"cinc.position.station.latitude\":"; writeDouble(result, translateCoordinates(dpdiSettings.latitude_station_grad, dpdiSettings.latitude_station_minute), 6); + result << ",\"cinc.position.station.longitude\":"; writeDouble(result, translateCoordinates(dpdiSettings.longitude_station_grad, dpdiSettings.longitude_station_minute), 6); + result << ",\"cinc.position.satelliteLongitude\":"; writeDouble(result, translateCoordinates(dpdiSettings.longitude_sattelite_grad, dpdiSettings.longitude_sattelite_minute), 6); + result << ",\"cinc.delayMin\":" << dpdiSettings.min_delay; + result << ",\"cinc.delayMax\":" << dpdiSettings.max_delay; + + result << ",\n\"buc.refClk10M\":" << UNKNOWN; + result << ",\"buc.powering\":" << UNKNOWN; + + result << ",\n\"lnb.refClk10M\":" << UNKNOWN; + result << ",\"lnb.powering\":" << UNKNOWN; + + result << ",\n\"serviceSettings.refClk10M\":" << UNKNOWN; + result << ",\"serviceSettings.startDelay\":" << UNKNOWN; + result << ",\"serviceSettings.autoStart\":" << UNKNOWN; + + result << "}"; + return result.str(); } api_driver::ApiDriver::~ApiDriver() = default; diff --git a/src/terminal_api_driver.h b/src/terminal_api_driver.h index 7f67b6d..43bdea7 100644 --- a/src/terminal_api_driver.h +++ b/src/terminal_api_driver.h @@ -17,14 +17,14 @@ namespace api_driver { * Запросить общее состояние терминала * @return {"txState":false,"rxState":false,"rx.sym_sync_lock":false,"rx.freq_search_lock":false,"rx.afc_lock":false,"rx.pkt_sync":false} */ - std::string loadTerminalState(); + std::string loadTerminalState() const; /** * Сбросить статистику пакетов */ void resetPacketStatistics() const; - std::string loadSettings(); + std::string loadSettings() const; ~ApiDriver(); diff --git a/static/main.html b/static/main.html index e454fd9..fe8eb5d 100644 --- a/static/main.html +++ b/static/main.html @@ -21,6 +21,7 @@ RSCM-101 Мониторинг Настройки + QoS Администрирование Выход @@ -89,7 +90,7 @@ - +
Температура ADRV{{ stat_device.adrv }} °C
Температура ZYNC{{ stat_device.zync }} °C
Температура ZYNC ULTRASUCK{{ stat_device.zync }} °C
Температура FPGA{{ stat_device.fpga }} °C
@@ -97,18 +98,17 @@

Настройки приема/передачи

-
+ -
-
-
+ +

Настройки передатчика

- - -
+
+

Параметры передачи

- - - -
+ +
+

Режим работы DVB-S2

- + + + + + + + + + - - - -
+
+

Настройки авто-регулировки мощности

- - -
+
+

Настройка приемника

- - +
+ + + +

Настройки режима CinC

+
+ + +

Настройки позиционирования

+ + + + +

Задержка до спутника

+ + +
+ + +

Настройки питания и опорного генератора

+
+
+

Настройки BUC

+ + +
+ +
+

Настройки LNB

+ + +
+ +
+

Сервисные настройки

+ + +
-
-

Настройки режима CinC

-

CinC пока нельзя настроить, но скоро разработчик это поправит)

-
-
-

Настройки питания и опорного генератора

-

Эти настройки пока недоступны, но скоро разработчик это поправит)

-
+ +
+
+

+ Эти настройки пока недоступны, но скоро разработчик это поправит. А пока купи разработчику банку пива) +

+ +

@@ -345,7 +434,7 @@