diff --git a/run_cast.sh b/run_cast.sh index e8f6f2de..72bed22e 100755 --- a/run_cast.sh +++ b/run_cast.sh @@ -13,13 +13,31 @@ in_tcp="tcpcli://localhost:${tcp_port}#${receiver_format}" #in_ext_tcp is mainly for dev purpose to receive a raw stream from another base in_ext_tcp="tcpcli://${ext_tcp_source}:${ext_tcp_port}#${receiver_format}" -out_caster_A="-msg ${rtcm_msg_a} -out ntrips://:${svr_pwd_a}@${svr_addr_a}:${svr_port_a}/${mnt_name_a}#rtcm3 -p ${position}" -#add receiver options if it exists -[[ ! -z "${ntrip_a_receiver_options}" ]] && out_caster_A=""${out_caster_A}" -opt "${ntrip_a_receiver_options}"" - -out_caster_B="-msg ${rtcm_msg_b} -out ntrips://:${svr_pwd_b}@${svr_addr_b}:${svr_port_b}/${mnt_name_b}#rtcm3 -p ${position}" -#add receiver options if it exists -[[ ! -z "${ntrip_b_receiver_options}" ]] && out_caster_B=""${out_caster_B}" -opt "${ntrip_b_receiver_options}"" +build_out_caster() { + local suffix="${1^^}" + local suffix_lower="${suffix,,}" + local msg_var="rtcm_msg_${suffix_lower}" + local pwd_var="svr_pwd_${suffix_lower}" + local addr_var="svr_addr_${suffix_lower}" + local port_var="svr_port_${suffix_lower}" + local mount_var="mnt_name_${suffix_lower}" + local options_var="ntrip_${suffix_lower}_receiver_options" + local out_caster="-msg ${!msg_var} -out ntrips://:${!pwd_var}@${!addr_var}:${!port_var}/${!mount_var}#rtcm3 -p ${position}" + + [[ -n "${!options_var}" ]] && out_caster="${out_caster} -opt \"${!options_var}\"" + printf '%s' "${out_caster}" +} + +run_out_caster() { + local input_var="${1}" + local suffix="${2^^}" + local out_caster + out_caster="$(build_out_caster "${suffix}")" + ${cast} -in ${!input_var} ${out_caster} -i "${receiver_info}" -a "${antenna_info}" -t ${level} -fl ${logdir}/str2str_ntrip_${suffix}.log +} + +out_caster_A="$(build_out_caster A)" +out_caster_B="$(build_out_caster B)" array_pos=(${position}) if [[ ${local_ntripc_user} == '' ]] && [[ ${local_ntripc_pwd} == '' ]] @@ -63,12 +81,16 @@ mkdir -p ${logdir} ;; out_caster_A) - #echo ${cast} -in ${!1} -out $out_caster - ${cast} -in ${!1} ${out_caster_A} -i "${receiver_info}" -a "${antenna_info}" -t ${level} -fl ${logdir}/str2str_ntrip_A.log + run_out_caster "$1" "A" ;; out_caster_B) - ${cast} -in ${!1} ${out_caster_B} -i "${receiver_info}" -a "${antenna_info}" -t ${level} -fl ${logdir}/str2str_ntrip_B.log + run_out_caster "$1" "B" + ;; + + out_caster) + [[ -z "${3}" ]] && { echo "Missing NTRIP caster suffix"; exit 1; } + run_out_caster "$1" "$3" ;; out_local_caster) diff --git a/tools/install.sh b/tools/install.sh index 8886ab68..e5bddb81 100755 --- a/tools/install.sh +++ b/tools/install.sh @@ -537,14 +537,12 @@ configure_gnss(){ source <( grep -v '^#' "${rtkbase_path}"/settings.conf | grep '=' ) systemctl is-active --quiet str2str_tcp.service && systemctl stop str2str_tcp.service #cleaning receiver options - sudo -u "${RTKBASE_USER}" sed -i s/^ntrip_a_receiver_options=.*/ntrip_a_receiver_options=/ "${rtkbase_path}"/settings.conf && \ - sudo -u "${RTKBASE_USER}" sed -i s/^ntrip_b_receiver_options=.*/ntrip_b_receiver_options=/ "${rtkbase_path}"/settings.conf && \ - sudo -u "${RTKBASE_USER}" sed -i s/^local_ntripc_receiver_options=.*/local_ntripc_receiver_options=/ "${rtkbase_path}"/settings.conf && \ - sudo -u "${RTKBASE_USER}" sed -i s/^rtcm_receiver_options=.*/rtcm_receiver_options=/ "${rtkbase_path}"/settings.conf && \ - sudo -u "${RTKBASE_USER}" sed -i s/^rtcm_client_receiver_options=.*/rtcm_client_receiver_options=/ "${rtkbase_path}"/settings.conf && \ - sudo -u "${RTKBASE_USER}" sed -i s/^rtcm_udp_svr_receiver_options=.*/rtcm_udp_svr_receiver_options=/ "${rtkbase_path}"/settings.conf && \ - sudo -u "${RTKBASE_USER}" sed -i s/^rtcm_udp_client_receiver_options=.*/rtcm_udp_client_receiver_options=/ "${rtkbase_path}"/settings.conf && \ - sudo -u "${RTKBASE_USER}" sed -i s/^rtcm_serial_receiver_options=.*/rtcm_serial_receiver_options=/ "${rtkbase_path}"/settings.conf + for option_name in $(grep -Eo '^ntrip_[a-z0-9]+_receiver_options' "${rtkbase_path}"/settings.conf); do + sudo -u "${RTKBASE_USER}" sed -i "s/^${option_name}=.*/${option_name}=/" "${rtkbase_path}"/settings.conf || return 1 + done + for option_name in local_ntripc_receiver_options rtcm_receiver_options rtcm_client_receiver_options rtcm_udp_svr_receiver_options rtcm_udp_client_receiver_options rtcm_serial_receiver_options; do + sudo -u "${RTKBASE_USER}" sed -i "s/^${option_name}=.*/${option_name}=/" "${rtkbase_path}"/settings.conf || return 1 + done #if the receiver is a U-Blox F9P, launch the set_zed-f9p.sh. This script will reset the F9P and configure it with the corrects settings for rtkbase if [[ $(python3 "${rtkbase_path}"/tools/ubxtool -f /dev/"${com_port}" -s ${com_port_settings%%:*} -p MON-VER) =~ 'ZED-F9P' ]] @@ -560,9 +558,8 @@ configure_gnss(){ sudo -u "${RTKBASE_USER}" sed -i s/^com_port_settings=.*/com_port_settings=\'115200:8:n:1\'/ "${rtkbase_path}"/settings.conf && \ sudo -u "${RTKBASE_USER}" sed -i s/^receiver=.*/receiver=\'U-blox_ZED-F9P\'/ "${rtkbase_path}"/settings.conf && \ sudo -u "${RTKBASE_USER}" sed -i s/^receiver_format=.*/receiver_format=\'ubx\'/ "${rtkbase_path}"/settings.conf && \ - #add option -TADJ=1 on rtcm/ntrip_a/ntrip_b/serial outputs - sudo -u "${RTKBASE_USER}" sed -i s/^ntrip_a_receiver_options=.*/ntrip_a_receiver_options=\'-TADJ=1\'/ "${rtkbase_path}"/settings.conf && \ - sudo -u "${RTKBASE_USER}" sed -i s/^ntrip_b_receiver_options=.*/ntrip_b_receiver_options=\'-TADJ=1\'/ "${rtkbase_path}"/settings.conf && \ + #add option -TADJ=1 on RTK outputs + for option_name in $(grep -Eo '^ntrip_[a-z0-9]+_receiver_options' "${rtkbase_path}"/settings.conf); do sudo -u "${RTKBASE_USER}" sed -i "s/^${option_name}=.*/${option_name}=\'-TADJ=1\'/" "${rtkbase_path}"/settings.conf || exit 1; done && \ sudo -u "${RTKBASE_USER}" sed -i s/^local_ntripc_receiver_options=.*/local_ntripc_receiver_options=\'-TADJ=1\'/ "${rtkbase_path}"/settings.conf && \ sudo -u "${RTKBASE_USER}" sed -i s/^rtcm_receiver_options=.*/rtcm_receiver_options=\'-TADJ=1\'/ "${rtkbase_path}"/settings.conf && \ sudo -u "${RTKBASE_USER}" sed -i s/^rtcm_client_receiver_options=.*/rtcm_client_receiver_options=\'-TADJ=1\'/ "${rtkbase_path}"/settings.conf && \ diff --git a/tools/rtkbase_update.sh b/tools/rtkbase_update.sh index caec42ad..0aec75b7 100755 --- a/tools/rtkbase_update.sh +++ b/tools/rtkbase_update.sh @@ -27,6 +27,7 @@ checking=$6 str2str_active=$(systemctl is-active str2str_tcp) str2str_ntrip_A_active=$(systemctl is-active str2str_ntrip_A) str2str_ntrip_B_active=$(systemctl is-active str2str_ntrip_B) +mapfile -t str2str_dynamic_ntrip_active_units < <(systemctl list-units 'str2str_ntrip@*.service' --plain --all --no-legend 2>/dev/null | awk '$3=="active" {print $1}') str2str_local_caster=$(systemctl is-active str2str_local_ntrip_caster) str2str_rtcm=$(systemctl is-active str2str_rtcm_svr) str2str_serial=$(systemctl is-active str2str_rtcm_serial) @@ -171,6 +172,7 @@ upd_2.4.2() { # restart previously running services [ $str2str_ntrip_A_active = 'active' ] && systemctl start str2str_ntrip_A [ $str2str_ntrip_B_active = 'active' ] && systemctl start str2str_ntrip_B + for unit_name in "${str2str_dynamic_ntrip_active_units[@]}"; do systemctl start "${unit_name}"; done [ $str2str_local_caster = 'active' ] && systemctl start str2str_local_ntrip_caster [ $str2str_rtcm = 'active' ] && systemctl start str2str_rtcm_svr [ $str2str_serial = 'active' ] && systemctl start str2str_rtcm_serial @@ -320,6 +322,7 @@ chown -R ${standard_user}:${standard_user} ${destination_directory} # restart needed with all update to propagate the release number in the rtcm stream [ $str2str_ntrip_A_active = 'active' ] && systemctl restart str2str_ntrip_A [ $str2str_ntrip_B_active = 'active' ] && systemctl restart str2str_ntrip_B + for unit_name in "${str2str_dynamic_ntrip_active_units[@]}"; do systemctl restart "${unit_name}"; done [ $str2str_local_caster = 'active' ] && systemctl restart str2str_local_ntrip_caster [ $str2str_rtcm = 'active' ] && systemctl restart str2str_rtcm_svr [ $str2str_serial = 'active' ] && systemctl restart str2str_rtcm_serial diff --git a/tools/uninstall.sh b/tools/uninstall.sh index 37d86d40..e463f6b1 100755 --- a/tools/uninstall.sh +++ b/tools/uninstall.sh @@ -6,6 +6,7 @@ BASEDIR=$(dirname "$0") for service_name in str2str_tcp.service \ str2str_ntrip_A.service \ str2str_ntrip_B.service \ + str2str_ntrip@.service \ str2str_local_ntrip_caster \ str2str_rtcm_svr.service \ str2str_rtcm_client.service \ @@ -29,6 +30,16 @@ do systemctl reset-failed done +for service_name in $(systemctl list-units 'str2str_ntrip@*.service' --plain --all --no-legend 2>/dev/null | awk '{print $1}'); do + echo 'Deleting ' "${service_name}" + systemctl stop "${service_name}" + systemctl disable "${service_name}" + rm /etc/systemd/system/"${service_name}" + rm /usr/lib/systemd/system/"${service_name}" + systemctl daemon-reload + systemctl reset-failed +done + # removing rtklib binaries echo 'Deleting RTKLib binaries' rm /usr/bin/str2str diff --git a/unit/str2str_ntrip@.service b/unit/str2str_ntrip@.service new file mode 100644 index 00000000..214bbb06 --- /dev/null +++ b/unit/str2str_ntrip@.service @@ -0,0 +1,23 @@ +[Unit] +Description=RTKBase Ntrip %i +#After=network-online.target +#Wants=network-online.target +Requires=str2str_tcp.service +After=str2str_tcp.service + +[Service] +Type=simple +SyslogIdentifier=str2str_ntrip_%i +User={user} +ExecStart={script_path}/run_cast.sh in_tcp out_caster %i +Restart=on-failure +RestartSec=30 +#Limiting log to 1 msg per minute +LogRateLimitIntervalSec=1 minute +LogRateLimitBurst=1 +ProtectHome=read-only +ProtectSystem=strict +ReadWritePaths={script_path} + +[Install] +WantedBy=multi-user.target diff --git a/web_app/RTKBaseConfigManager.py b/web_app/RTKBaseConfigManager.py index cf1c7290..fdbdc072 100644 --- a/web_app/RTKBaseConfigManager.py +++ b/web_app/RTKBaseConfigManager.py @@ -1,12 +1,14 @@ import os from configparser import ConfigParser from secrets import token_urlsafe +import string class RTKBaseConfigManager: """ A class to easily access the settings from RTKBase settings.conf """ NON_QUOTED_KEYS = ("basedir", "web_authentification", "new_web_password", "web_password_hash", "flask_secret_key", "archive_name", "user") + NTRIP_TEMPLATE_SECTION = "ntrip_A" def __init__(self, default_settings_path, user_settings_path): """ @@ -119,22 +121,109 @@ def get_ntrip_A_settings(self): Get a subset of the settings from the ntrip A section in an ordered object and remove the single quotes. """ - ordered_ntrip = [{"source_section" : "ntrip_A"}] - for key in ("svr_addr_A", "svr_port_A", "svr_pwd_A", "mnt_name_A", "rtcm_msg_A", "ntrip_A_receiver_options"): - ordered_ntrip.append({key : self.config.get('ntrip_A', key).strip("'")}) - return ordered_ntrip + return self.get_legacy_ntrip_settings("ntrip_A") def get_ntrip_B_settings(self): """ Get a subset of the settings from the ntrip B section in an ordered object and remove the single quotes. """ - #TODO need refactoring with get_ntrip_A_settings - ordered_ntrip = [{"source_section" : "ntrip_B"}] - for key in ("svr_addr_B", "svr_port_B", "svr_pwd_B", "mnt_name_B", "rtcm_msg_B", "ntrip_B_receiver_options"): - ordered_ntrip.append({key : self.config.get('ntrip_B', key).strip("'")}) + return self.get_legacy_ntrip_settings("ntrip_B") + + def is_ntrip_section(self, section): + return section.startswith("ntrip_") and section != "local_ntrip_caster" + + def get_ntrip_suffix(self, section): + return section.split("_", 1)[1].upper() + + def get_ntrip_sections(self): + return sorted( + [section for section in self.config.sections() if self.is_ntrip_section(section)], + key=lambda section: self.get_ntrip_suffix(section), + ) + + def _get_ntrip_field_names(self, suffix): + suffix = suffix.upper() + suffix_lower = suffix.lower() + return { + "svr_addr": f"svr_addr_{suffix}", + "svr_port": f"svr_port_{suffix}", + "svr_pwd": f"svr_pwd_{suffix}", + "mnt_name": f"mnt_name_{suffix}", + "rtcm_msg": f"rtcm_msg_{suffix}", + "receiver_options": f"ntrip_{suffix}_receiver_options", + }, suffix_lower + + def get_legacy_ntrip_settings(self, section): + suffix = self.get_ntrip_suffix(section) + field_names, _ = self._get_ntrip_field_names(suffix) + ordered_ntrip = [{"source_section" : section}] + for key in field_names.values(): + ordered_ntrip.append({key : self.config.get(section, key).strip("'")}) return ordered_ntrip + def get_ntrip_settings(self, section): + suffix = self.get_ntrip_suffix(section) + field_names, _ = self._get_ntrip_field_names(suffix) + return { + "source_section": section, + "service_name": section, + "service_label": f"Ntrip {suffix} service", + "switch_id": f"{section}-switch", + "suffix": suffix, + "svr_addr": {"name": field_names["svr_addr"], "value": self.config.get(section, field_names["svr_addr"]).strip("'")}, + "svr_port": {"name": field_names["svr_port"], "value": self.config.get(section, field_names["svr_port"]).strip("'")}, + "svr_pwd": {"name": field_names["svr_pwd"], "value": self.config.get(section, field_names["svr_pwd"]).strip("'")}, + "mnt_name": {"name": field_names["mnt_name"], "value": self.config.get(section, field_names["mnt_name"]).strip("'")}, + "rtcm_msg": {"name": field_names["rtcm_msg"], "value": self.config.get(section, field_names["rtcm_msg"]).strip("'")}, + "receiver_options": { + "name": field_names["receiver_options"], + "value": self.config.get(section, field_names["receiver_options"]).strip("'"), + }, + } + + def get_all_ntrip_settings(self): + return [self.get_ntrip_settings(section) for section in self.get_ntrip_sections()] + + def get_first_ntrip_mount_name(self): + sections = self.get_ntrip_sections() + if not sections: + return "RTKBase" + field_names, _ = self._get_ntrip_field_names(self.get_ntrip_suffix(sections[0])) + return self.config.get(sections[0], field_names["mnt_name"]).strip("'") + + def _load_default_ntrip_template(self): + defaults = ConfigParser(interpolation=None) + defaults.read(self.default_settings_path) + if defaults.has_section(self.NTRIP_TEMPLATE_SECTION): + return defaults[self.NTRIP_TEMPLATE_SECTION] + return self.config[self.NTRIP_TEMPLATE_SECTION] + + def _next_ntrip_suffix(self): + existing_suffixes = {self.get_ntrip_suffix(section) for section in self.get_ntrip_sections()} + for suffix in string.ascii_uppercase: + if suffix not in existing_suffixes: + return suffix + raise ValueError("No available NTRIP caster suffix left") + + def add_ntrip_settings(self): + suffix = self._next_ntrip_suffix() + suffix_lower = suffix.lower() + section = f"ntrip_{suffix}" + template = self._load_default_ntrip_template() + self.config.add_section(section) + self.config[section][f"svr_addr_{suffix_lower}"] = template.get("svr_addr_a", "'caster.centipede.fr'") + self.config[section][f"svr_port_{suffix_lower}"] = template.get("svr_port_a", "'2101'") + self.config[section][f"svr_pwd_{suffix_lower}"] = template.get("svr_pwd_a", "''") + self.config[section][f"mnt_name_{suffix_lower}"] = template.get("mnt_name_a", "'Your_mount_name'") + self.config[section][f"rtcm_msg_{suffix_lower}"] = template.get( + "rtcm_msg_a", + "'1004,1005(10),1006,1008(10),1012,1019,1020,1033(10),1042,1045,1046,1077,1087,1097,1107,1127,1230'", + ) + self.config[section][f"ntrip_{suffix_lower}_receiver_options"] = template.get("ntrip_a_receiver_options", "''") + self.write_file() + return section + def get_local_ntripc_settings(self): """ Get a subset of the settings from the local ntrip section in an ordered object @@ -212,8 +301,7 @@ def get_ordered_settings(self): """ ordered_settings = {} ordered_settings['main'] = self.get_main_settings() - ordered_settings['ntrip_A'] = self.get_ntrip_A_settings() - ordered_settings['ntrip_B'] = self.get_ntrip_B_settings() + ordered_settings['ntrip'] = self.get_all_ntrip_settings() ordered_settings['local_ntripc'] = self.get_local_ntripc_settings() ordered_settings['file'] = self.get_file_settings() ordered_settings['rtcm_svr'] = self.get_rtcm_svr_settings() diff --git a/web_app/server.py b/web_app/server.py index e36bbc49..04402139 100755 --- a/web_app/server.py +++ b/web_app/server.py @@ -99,18 +99,45 @@ log_path=app.config["DOWNLOAD_FOLDER"], ) -services_list = [{"service_unit" : "str2str_tcp.service", "name" : "main"}, - {"service_unit" : "str2str_ntrip_A.service", "name" : "ntrip_A"}, - {"service_unit" : "str2str_ntrip_B.service", "name" : "ntrip_B"}, - {"service_unit" : "str2str_local_ntrip_caster.service", "name" : "local_ntrip_caster"}, - {"service_unit" : "str2str_rtcm_svr.service", "name" : "rtcm_svr"}, - {'service_unit' : 'str2str_rtcm_serial.service', "name" : "rtcm_serial"}, - {"service_unit" : "str2str_file.service", "name" : "file"}, - {'service_unit' : 'rtkbase_archive.timer', "name" : "archive_timer"}, - {'service_unit' : 'rtkbase_archive.service', "name" : "archive_service"}, - {'service_unit' : 'rtkbase_raw2nmea.service', "name" : "raw2nmea"}, - {'service_unit' : 'rtkbase_gnss_web_proxy.service', "name": "RTKBase Reverse Proxy for Gnss receiver Web Server"} - ] +def get_ntrip_service_unit(section_name): + suffix = rtkbaseconfig.get_ntrip_suffix(section_name) + if suffix in ("A", "B"): + return f"str2str_ntrip_{suffix}.service" + return f"str2str_ntrip@{suffix}.service" + + +def build_services_list(): + services = [{"service_unit" : "str2str_tcp.service", "name" : "main"}] + services.extend( + {"service_unit": get_ntrip_service_unit(section_name), "name": section_name} + for section_name in rtkbaseconfig.get_ntrip_sections() + ) + services.extend( + [ + {"service_unit" : "str2str_local_ntrip_caster.service", "name" : "local_ntrip_caster"}, + {"service_unit" : "str2str_rtcm_svr.service", "name" : "rtcm_svr"}, + {'service_unit' : 'str2str_rtcm_serial.service', "name" : "rtcm_serial"}, + {"service_unit" : "str2str_file.service", "name" : "file"}, + {'service_unit' : 'rtkbase_archive.timer', "name" : "archive_timer"}, + {'service_unit' : 'rtkbase_archive.service', "name" : "archive_service"}, + {'service_unit' : 'rtkbase_raw2nmea.service', "name" : "raw2nmea"}, + {'service_unit' : 'rtkbase_gnss_web_proxy.service', "name": "RTKBase Reverse Proxy for Gnss receiver Web Server"}, + ] + ) + return services + + +services_list = build_services_list() + + +def refresh_services_list(load_units_now=False): + global services_list + + previous_units = {service["name"]: service.get("unit") for service in services_list} + services_list = build_services_list() + if load_units_now: + services_list = load_units(services_list, existing_units=previous_units) + return services_list #Delay before rtkrcv will stop if no user is on status.html page rtkcv_standby_delay = 600 @@ -413,7 +440,7 @@ def inject_global_infos(): Insert various informations as global variables for Flask/Jinja """ g.version = rtkbaseconfig.get("general", "version") - g.station_name = rtkbaseconfig.get_ntrip_A_settings()[4]['mnt_name_A'] + g.station_name = rtkbaseconfig.get_first_ntrip_mount_name() g.sbc_model = get_sbc_model() @login.user_loader @@ -456,8 +483,7 @@ def settings_page(): #TODO use dict and not list main_settings = rtkbaseconfig.get_main_settings() main_settings.append(gnss_rcv_url.geturl()) - ntrip_A_settings = rtkbaseconfig.get_ntrip_A_settings() - ntrip_B_settings = rtkbaseconfig.get_ntrip_B_settings() + ntrip_settings = rtkbaseconfig.get_all_ntrip_settings() local_ntripc_settings = rtkbaseconfig.get_local_ntripc_settings() rtcm_svr_settings = rtkbaseconfig.get_rtcm_svr_settings() rtcm_client_settings = rtkbaseconfig.get_rtcm_client_settings() @@ -467,8 +493,7 @@ def settings_page(): file_settings = rtkbaseconfig.get_file_settings() return render_template("settings.html", main_settings = main_settings, - ntrip_A_settings = ntrip_A_settings, - ntrip_B_settings = ntrip_B_settings, + ntrip_settings = ntrip_settings, local_ntripc_settings = local_ntripc_settings, rtcm_svr_settings = rtcm_svr_settings, rtcm_client_settings = rtcm_client_settings, @@ -722,7 +747,7 @@ def reset_settings(): @app.route("/logs/download/settings") @login_required def backup_settings(): - settings_file_name = str("RTKBase_{}_{}_{}.conf".format(rtkbaseconfig.get("general", "version"), rtkbaseconfig.get("ntrip_A", "mnt_name_a").strip("'"), time.strftime("%Y-%m-%d_%HH%M"))) + settings_file_name = str("RTKBase_{}_{}_{}.conf".format(rtkbaseconfig.get("general", "version"), rtkbaseconfig.get_first_ntrip_mount_name(), time.strftime("%Y-%m-%d_%HH%M"))) #return send_file(os.path.join(rtkbase_path, "settings.conf"), as_attachment=True, download_name=settings_file_name) return send_from_directory(rtkbase_path, "settings.conf", as_attachment=True, download_name=settings_file_name) @@ -830,7 +855,7 @@ def turnOffWiFi(): #### Systemd Services functions #### -def load_units(services): +def load_units(services, existing_units=None): """ load unit service before getting status :param services: A list of systemd services (dict) containing a service_unit key:value @@ -841,8 +866,12 @@ def load_units(services): return will be [{"service_unit" : "str2str_tcp.service", "unit" : a pystemd object}] """ + existing_units = existing_units or {} for service in services: - service["unit"] = ServiceController(service["service_unit"]) + if existing_units.get(service["name"]) is not None: + service["unit"] = existing_units[service["name"]] + elif service.get("unit") is None: + service["unit"] = ServiceController(service["service_unit"]) return services def update_std_user(services): @@ -984,11 +1013,9 @@ def update_settings(json_msg): #Restart service if needed if source_section == "main": - restartServices(("main", "ntrip_A", "ntrip_B", "local_ntrip_caster", "rtcm_svr", "rtcm_client", "rtcm_udp_svr", "rtcm_udp_client", "file", "rtcm_serial", "raw2nmea")) - elif source_section == "ntrip_A": - restartServices(("ntrip_A",)) - elif source_section == "ntrip_B": - restartServices(("ntrip_B",)) + restartServices(tuple(["main", *rtkbaseconfig.get_ntrip_sections(), "local_ntrip_caster", "rtcm_svr", "rtcm_client", "rtcm_udp_svr", "rtcm_udp_client", "file", "rtcm_serial", "raw2nmea"])) + elif rtkbaseconfig.is_ntrip_section(source_section): + restartServices((source_section,)) elif source_section == "local_ntrip_caster": restartServices(("local_ntrip_caster",)) elif source_section == "rtcm_svr": @@ -1004,6 +1031,17 @@ def update_settings(json_msg): elif source_section == "local_storage": restartServices(("file",)) + +@socketio.on("add ntrip service", namespace="/test") +def add_ntrip_service(): + try: + new_section = rtkbaseconfig.add_ntrip_settings() + refresh_services_list(load_units_now=True) + socketio.emit("ntrip service added", json.dumps({"section": new_section}), namespace="/test") + getServicesStatus() + except Exception as e: + socketio.emit("ntrip service add failed", json.dumps({"error": str(e)}), namespace="/test") + def arg_parse(): parser = argparse.ArgumentParser( description="RTKBase Web server", diff --git a/web_app/static/settings.js b/web_app/static/settings.js index 5b64baae..1c1260cc 100644 --- a/web_app/static/settings.js +++ b/web_app/static/settings.js @@ -76,10 +76,10 @@ $(document).ready(function () { }); - // View/hide password buttons for: Ntrip A, Ntrip B and Local caster + // View/hide password buttons for dynamic NTRIP casters, local caster and RTCM client document.querySelectorAll(".input-group-append").forEach(function(e) { var name = e.querySelector("button").id.replace("_button", ""); - if (!["svr_pwd_A", "svr_pwd_B", "local_ntripc_pwd", "rtcm_client_pwd"].includes(name)) + if (!/^svr_pwd_[A-Z0-9]+$/.test(name) && !["local_ntripc_pwd", "rtcm_client_pwd"].includes(name)) return; var button = $("#" + name + "_button"); @@ -106,215 +106,66 @@ $(document).ready(function () { }); }); - // ####################### HANDLE RTKBASE SERVICES ####################### - - socket.on("services status", function(msg) { - // gestion des services - var servicesStatus = JSON.parse(msg); - //console.log("service status: " + servicesStatus); - - // ################ MAiN service Switch ###################### - //console.log("REFRESHING service switch"); - var mainSwitch = $('#main-switch'); - // set the switch to on/off depending of the service status - if (servicesStatus[0].active === true) { - //document.querySelector("#main-switch").bootstrapToggle('on'); - mainSwitch.bootstrapToggle('on', true); - } else { - //document.querySelector("#main-switch").bootstrapToggle('off'); - mainSwitch.bootstrapToggle('off', true); - } - //console.log(servicesStatus[0]); - if (servicesStatus[0].btn_color) { - mainSwitch.bootstrapToggle('setOnStyle', servicesStatus[0].btn_color); - } - if (servicesStatus[0].btn_off_color) { - mainSwitch.bootstrapToggle('setOffStyle', servicesStatus[0].btn_off_color); - } - - // event for switching on/off service on user mouse click - //TODO When the switch changes its position, this event seems attached before - //the switch finish its transition, then fire another event. - $( "#main-switch" ).one("change", function(e) { - var switchStatus = $(this).prop('checked'); - //console.log(" e : " + e); - console.log("Main SwitchStatus : " + switchStatus); - socket.emit("services switch", {"name" : "main", "active" : switchStatus}); - }) - - // #################### NTRIP A service Switch ######################### - var ntrip_A_Switch = $('#ntrip_A-switch'); - // set the switch to on/off depending of the service status - if (servicesStatus[1].active === true) { - //document.querySelector("#main-switch").bootstrapToggle('on'); - ntrip_A_Switch.bootstrapToggle('on', true); - } else { - //document.querySelector("#main-switch").bootstrapToggle('off'); - ntrip_A_Switch.bootstrapToggle('off', true); - } - //console.log(servicesStatus[1]); - if (servicesStatus[1].btn_color) { - ntrip_A_Switch.bootstrapToggle('setOnStyle', servicesStatus[1].btn_color); - } - if (servicesStatus[1].btn_off_color) { - ntrip_A_Switch.bootstrapToggle('setOffStyle', servicesStatus[1].btn_off_color); - } - - // event for switching on/off service on user mouse click - //TODO When the switch changes its position, this event seems attached before - //the switch finish its transition, then fire another event. - $( "#ntrip_A-switch" ).one("change", function(e) { - var switchStatus = $(this).prop('checked'); - //console.log(" e : " + e); - //console.log("Ntrip SwitchStatus : " + switchStatus); - socket.emit("services switch", {"name" : "ntrip_A", "active" : switchStatus}); - }) - - // #################### NTRIP B service Switch ######################### - var ntrip_B_Switch = $('#ntrip_B-switch'); - // set the switch to on/off depending of the service status - if (servicesStatus[2].active === true) { - //document.querySelector("#main-switch").bootstrapToggle('on'); - ntrip_B_Switch.bootstrapToggle('on', true); - } else { - //document.querySelector("#main-switch").bootstrapToggle('off'); - ntrip_B_Switch.bootstrapToggle('off', true); - } - //console.log(servicesStatus[2]); - if (servicesStatus[2].btn_color) { - ntrip_B_Switch.bootstrapToggle('setOnStyle', servicesStatus[2].btn_color); - } - if (servicesStatus[2].btn_off_color) { - ntrip_B_Switch.bootstrapToggle('setOffStyle', servicesStatus[2].btn_off_color); - } - - // event for switching on/off service on user mouse click - //TODO When the switch changes its position, this event seems attached before - //the switch finish its transition, then fire another event. - $( "#ntrip_B-switch" ).one("change", function(e) { - var switchStatus = $(this).prop('checked'); - //console.log(" e : " + e); - //console.log("Ntrip SwitchStatus : " + switchStatus); - socket.emit("services switch", {"name" : "ntrip_B", "active" : switchStatus}); - }) - - // ################ Local NTRIP Caster service Switch ##################### - - var ntripcSwitch = $('#ntripc-switch'); - // set the switch to on/off depending of the service status - if (servicesStatus[3].active === true) { - //document.querySelector("#main-switch").bootstrapToggle('on'); - ntripcSwitch.bootstrapToggle('on', true); - } else { - //document.querySelector("#main-switch").bootstrapToggle('off'); - ntripcSwitch.bootstrapToggle('off', true); - } - - //console.log(servicesStatus[2]); - if (servicesStatus[3].btn_color) { - ntripcSwitch.bootstrapToggle('setOnStyle', servicesStatus[3].btn_color); - } - if (servicesStatus[3].btn_off_color) { - ntripcSwitch.bootstrapToggle('setOffStyle', servicesStatus[3].btn_off_color); - } - - // event for switching on/off service on user mouse click - //TODO When the switch changes its position, this event seems attached before - //the switch finish its transition, then fire another event. - $( "#ntripc-switch" ).one("change", function(e) { - var switchStatus = $(this).prop('checked'); - //console.log(" e : " + e); - //console.log("Ntrip Caster SwitchStatus : " + switchStatus); - socket.emit("services switch", {"name" : "local_ntrip_caster", "active" : switchStatus}); - }) - - // #################### RTCM TCP server service Switch ######################### - - var rtcmSvrSwitch = $('#rtcm_svr-switch'); - // set the switch to on/off depending of the service status - if (servicesStatus[4].active === true) { - //document.querySelector("#main-switch").bootstrapToggle('on'); - rtcmSvrSwitch.bootstrapToggle('on', true); - } else { - //document.querySelector("#main-switch").bootstrapToggle('off'); - rtcmSvrSwitch.bootstrapToggle('off', true); - } - //console.log(servicesStatus[3]); - if (servicesStatus[4].btn_color) { - rtcmSvrSwitch.bootstrapToggle('setOnStyle', servicesStatus[4].btn_color); - } - if (servicesStatus[4].btn_off_color) { - rtcmSvrSwitch.bootstrapToggle('setOffStyle', servicesStatus[4].btn_off_color); + function updateServiceToggle(toggle, serviceStatus, serviceName) { + if (toggle.length === 0 || !serviceStatus) { + return; } - - // event for switching on/off service on user mouse click - //TODO When the switch changes its position, this event seems attached before - //the switch finish its transition, then fire another event. - $( "#rtcm_svr-switch" ).one("change", function(e) { - var switchStatus = $(this).prop('checked'); - //console.log(" e : " + e); - //console.log("RTCM Server SwitchStatus : " + switchStatus); - socket.emit("services switch", {"name" : "rtcm_svr", "active" : switchStatus}); - }) - // #################### Serial RTCM service Switch ######################### - - var rtcmSerialSwitch = $('#rtcm_serial-switch'); - // set the switch to on/off depending of the service status - if (servicesStatus[5].active === true) { - //document.querySelector("#main-switch").bootstrapToggle('on'); - rtcmSerialSwitch.bootstrapToggle('on', true); + if (serviceStatus.active === true) { + toggle.bootstrapToggle('on', true); } else { - //document.querySelector("#main-switch").bootstrapToggle('off'); - rtcmSerialSwitch.bootstrapToggle('off', true); + toggle.bootstrapToggle('off', true); } - //console.log(servicesStatus[4]); - if (servicesStatus[5].btn_color) { - rtcmSerialSwitch.bootstrapToggle('setOnStyle', servicesStatus[5].btn_color); + if (serviceStatus.btn_color) { + toggle.bootstrapToggle('setOnStyle', serviceStatus.btn_color); } - if (servicesStatus[5].btn_off_color) { - rtcmSerialSwitch.bootstrapToggle('setOffStyle', servicesStatus[5].btn_off_color); + if (serviceStatus.btn_off_color) { + toggle.bootstrapToggle('setOffStyle', serviceStatus.btn_off_color); } - - // event for switching on/off service on user mouse click - //TODO When the switch changes its position, this event seems attached before - //the switch finish its transition, then fire another event. - $( "#rtcm_serial-switch" ).one("change", function(e) { - var switchStatus = $(this).prop('checked'); - //console.log(" e : " + e); - //console.log("Serial RTCM SwitchStatus : " + switchStatus); - socket.emit("services switch", {"name" : "rtcm_serial", "active" : switchStatus}); - }) - - // #################### LOG service Switch ######################### - var fileSwitch = $('#file-switch'); - // set the switch to on/off depending of the service status - if (servicesStatus[6].active === true) { - //document.querySelector("#main-switch").bootstrapToggle('on'); - fileSwitch.bootstrapToggle('on', true); - } else { - //document.querySelector("#main-switch").bootstrapToggle('off'); - fileSwitch.bootstrapToggle('off', true); - } - //console.log(servicesStatus[5]); - if (servicesStatus[6].btn_color) { - fileSwitch.bootstrapToggle('setOnStyle', servicesStatus[6].btn_color); - } - if (servicesStatus[6].btn_off_color) { - fileSwitch.bootstrapToggle('setOffStyle', servicesStatus[6].btn_off_color); - } - - // event for switching on/off service on user mouse click - //TODO When the switch changes its position, this event seems attached before - //the switch finish its transition, then fire another event. - $( "#file-switch" ).one("change", function(e) { + toggle.off("change.serviceToggle").one("change.serviceToggle", function() { var switchStatus = $(this).prop('checked'); - //console.log(" e : " + e); - //console.log("File SwitchStatus : " + switchStatus); - socket.emit("services switch", {"name" : "file", "active" : switchStatus}); - }) - }) + socket.emit("services switch", {"name" : serviceName, "active" : switchStatus}); + }); + } + + // ####################### HANDLE RTKBASE SERVICES ####################### + + socket.on("services status", function(msg) { + var servicesStatus = JSON.parse(msg); + var servicesByName = {}; + servicesStatus.forEach(function(service) { + servicesByName[service.name] = service; + }); + + updateServiceToggle($('#main-switch'), servicesByName.main, "main"); + $('.ntrip-service-toggle').each(function() { + var toggle = $(this); + var serviceName = toggle.data('service-name'); + updateServiceToggle(toggle, servicesByName[serviceName], serviceName); + }); + updateServiceToggle($('#ntripc-switch'), servicesByName.local_ntrip_caster, "local_ntrip_caster"); + updateServiceToggle($('#rtcm_svr-switch'), servicesByName.rtcm_svr, "rtcm_svr"); + updateServiceToggle($('#rtcm_serial-switch'), servicesByName.rtcm_serial, "rtcm_serial"); + updateServiceToggle($('#file-switch'), servicesByName.file, "file"); + }); + + $('#add-ntrip-service').on("click", function () { + var addButton = $(this); + addButton.prop("disabled", true); + addButton.html(' Adding...'); + socket.emit("add ntrip service"); + }); + + socket.on("ntrip service added", function() { + location.href = document.URL.replace(/#$/, ''); + }); + + socket.on("ntrip service add failed", function(msg) { + var response = JSON.parse(msg); + window.alert(response.error); + $('#add-ntrip-service').prop("disabled", false).text("Add Ntrip caster"); + }); socket.on("system time corrected", function(msg) { $('.warning_footer h1').text("Reach time synced with GPS!"); diff --git a/web_app/templates/settings.html b/web_app/templates/settings.html index 9690796d..72f193d8 100644 --- a/web_app/templates/settings.html +++ b/web_app/templates/settings.html @@ -117,86 +117,18 @@

Services:

- -
+
- - - - - - - - - +
-
-
- -
- - Caster url address -
-
-
- -
- - Caster port -
-
-
- -
-
- - - - -
- Caster password -
-
-
- -
- - Mount name -
-
-
- -
- - Rtcm messages list: msg(interval in seconds),msg(interval in seconds),... -
-
-
- -
- - Receiver dependent options, e.g. -TADJ=1 for U-Blox F9P -
-
- -
- -
-
- -
+ {% for ntrip in ntrip_settings %} +
- - - + + + @@ -205,28 +137,28 @@

Services:

-
+
- +
- - Caster url address + + Caster url address
- +
- - Caster port + + Caster port
- +
- + -
- Caster password + Caster password
- +
- - Mount name + + Mount name
- +
- - Rtcm messages list: msg(interval in seconds),msg(interval in seconds),... + + Rtcm messages list: msg(interval in seconds),msg(interval in seconds),...
- +
- - Receiver dependent options, e.g. -TADJ=1 for U-Blox F9P + + Receiver dependent options, e.g. -TADJ=1 for U-Blox F9P
-
+
+ {% endfor %}
@@ -802,4 +735,4 @@ -{% endblock %} \ No newline at end of file +{% endblock %} diff --git a/web_app/test_config_manager.py b/web_app/test_config_manager.py new file mode 100644 index 00000000..fc562cdd --- /dev/null +++ b/web_app/test_config_manager.py @@ -0,0 +1,46 @@ +from __future__ import annotations + +import shutil +import tempfile +import unittest +from pathlib import Path + +import sys + + +WEB_APP_DIR = Path(__file__).resolve().parent +if str(WEB_APP_DIR) not in sys.path: + sys.path.insert(0, str(WEB_APP_DIR)) + +from RTKBaseConfigManager import RTKBaseConfigManager + + +class RTKBaseConfigManagerTests(unittest.TestCase): + def setUp(self): + self.tempdir = tempfile.TemporaryDirectory() + self.default_settings = WEB_APP_DIR.parent / "settings.conf.default" + self.user_settings = Path(self.tempdir.name) / "settings.conf" + shutil.copy(self.default_settings, self.user_settings) + self.config = RTKBaseConfigManager(str(self.default_settings), str(self.user_settings)) + + def tearDown(self): + self.tempdir.cleanup() + + def test_get_all_ntrip_settings_lists_existing_sections(self): + ntrip_sections = [section["source_section"] for section in self.config.get_all_ntrip_settings()] + self.assertEqual(["ntrip_A", "ntrip_B"], ntrip_sections) + + def test_add_ntrip_settings_creates_next_available_section(self): + new_section = self.config.add_ntrip_settings() + + self.assertEqual("ntrip_C", new_section) + self.assertIn("ntrip_C", self.config.sections()) + + ntrip_settings = self.config.get_ntrip_settings("ntrip_C") + self.assertEqual("svr_addr_C", ntrip_settings["svr_addr"]["name"]) + self.assertEqual("caster.centipede.fr", ntrip_settings["svr_addr"]["value"]) + self.assertEqual("2101", ntrip_settings["svr_port"]["value"]) + + +if __name__ == "__main__": + unittest.main()