diff --git a/CHANGELOG.md b/CHANGELOG.md index 50918a5a..04fab550 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,7 @@ - Septentrio Mosaic-X5 detection and configuration - Reverse proxy server with Rtkbase authentication, for Mosaic-X5 web interface - Added description below form input. #381 + - rtcm tcp client service added (useful for the Rtkdirect service). #376 ### Changed ### Deprecated ### Removed diff --git a/images/internal.png b/images/internal.png index 13bf6825..43e63ddf 100644 Binary files a/images/internal.png and b/images/internal.png differ diff --git a/images/internal_synoptic.odp b/images/internal_synoptic.odp index d7dbf568..8c7010aa 100644 Binary files a/images/internal_synoptic.odp and b/images/internal_synoptic.odp differ diff --git a/run_cast.sh b/run_cast.sh index 9b0834a4..036e3799 100755 --- a/run_cast.sh +++ b/run_cast.sh @@ -40,6 +40,14 @@ out_rtcm_svr="tcpsvr://:${rtcm_svr_port}#rtcm3 -msg ${rtcm_svr_msg} -p ${positio #add receiver options if it exists [[ ! -z "${rtcm_receiver_options}" ]] && out_rtcm_svr=""${out_rtcm_svr}" -opt "${rtcm_receiver_options}"" +out_rtcm_client="tcpcli://${rtcm_client_addr}:${rtcm_client_port}#rtcm3 -msg ${rtcm_client_msg} -p ${position}" +#add receiver options if it exists +[[ ! -z "${rtcm_client_receiver_options}" ]] && out_rtcm_client=""${out_rtcm_client}" -opt "${rtcm_receiver_client_options}"" + +out_rtcm_udp_svr="udpsvr://:${rtcm_udp_svr_port}#rtcm3 -msg ${rtcm_udp_svr_msg} -p ${position}" +#add receiver options if it exists +[[ ! -z "${rtcm_udp_svr_receiver_options}" ]] && out_rtcm_udp_svr=""${out_rtcm_udp_svr}" -opt "${rtcm_udp_svr_receiver_options}"" + out_rtcm_serial="serial://${out_com_port}:${out_com_port_settings}#rtcm3 -msg ${rtcm_serial_msg} -p ${position}" #add receiver options if it exists [[ ! -z "${rtcm_serial_receiver_options}" ]] && out_rtcm_serial=""${out_rtcm_serial}" -opt "${rtcm_serial_receiver_options}"" @@ -71,7 +79,17 @@ mkdir -p ${logdir} #echo ${cast} -in ${!1} -out $out_rtcm_svr ${cast} -in ${!1} -out ${out_rtcm_svr} -i "${receiver_info}" -a "${antenna_info}" -t ${level} -fl ${logdir}/str2str_rtcm_svr.log & ;; - + + out_rtcm_client) + #echo ${cast} -in ${!1} -out $out_rtcm_client + ${cast} -in ${!1} -out ${out_rtcm_client} -i "${receiver_info}" -a "${antenna_info}" -t ${level} -fl ${logdir}/str2str_rtcm_client.log & + ;; + + out_rtcm_udp_svr) + #echo ${cast} -in ${!1} -out $out_rtcm_udp_svr + ${cast} -in ${!1} -out ${out_rtcm_udp_svr} -i "${receiver_info}" -a "${antenna_info}" -t ${level} -fl ${logdir}/str2str_rtcm_udp_svr.log & + ;; + out_rtcm_serial) #echo ${cast} -in ${!1} -out $out_rtcm_serial ${cast} -in ${!1} -out ${out_rtcm_serial} -i "${receiver_info}" -a "${antenna_info}" -t ${level} -fl ${logdir}/str2str_rtcm_serial.log & diff --git a/settings.conf.default b/settings.conf.default index e69ba60c..98027dd4 100644 --- a/settings.conf.default +++ b/settings.conf.default @@ -133,6 +133,42 @@ rtcm_svr_msg='1004,1005(10),1006,1008(10),1012,1019,1020,1033(10),1042,1045,1046 #Receiver dependent options rtcm_receiver_options='' +[rtcm_client] + +# RTCM client options + +#port for rtcm client +rtcm_client_addr='' +rtcm_client_port='80' +#messages for rtcm client use +rtcm_client_msg='1004,1005(10),1006,1008(10),1012,1019,1020,1033(10),1042,1045,1046,1077,1087,1097,1107,1127,1230' +#Receiver dependent options +rtcm_client_receiver_options='' + +[rtcm_udp_svr] + +#RTCM UDP Server options + +#port for rtcm UDP use +rtcm_udp_svr_port='' +#messages for rtcm udp use +rtcm_udp_svr_msg='1004,1005(10),1006,1008(10),1012,1019,1020,1033(10),1042,1045,1046,1077,1087,1097,1107,1127,1230' +#Receiver dependent options +rtcm_udp_svr_receiver_options='' + +[rtcm_udp_client] + +#RTCM UDP Client options + +#address for rtcm UDP +rtcm_udp_client_addr='' +#port for rtcm UDP use +rtcm_udp_client_port='' +#messages for rtcm udp use +rtcm_udp_client_msg='1004,1005(10),1006,1008(10),1012,1019,1020,1033(10),1042,1045,1046,1077,1087,1097,1107,1127,1230' +#Receiver dependent options +rtcm_udp_client_receiver_options='' + [rtcm_serial] # Serial output RTCM options diff --git a/tools/install.sh b/tools/install.sh index 1dd00c04..43be5442 100755 --- a/tools/install.sh +++ b/tools/install.sh @@ -520,6 +520,9 @@ configure_gnss(){ sudo -u "${RTKBASE_USER}" sed -i s/^ntrip_b_receiver_options=.*/ntrip_b_receiver_options=\'-TADJ=1\'/ "${rtkbase_path}"/settings.conf && \ 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 && \ + sudo -u "${RTKBASE_USER}" sed -i s/^rtcm_udp_svr_receiver_options=.*/rtcm_udp_svr_receiver_options=\'-TADJ=1\'/ "${rtkbase_path}"/settings.conf && \ + sudo -u "${RTKBASE_USER}" sed -i s/^rtcm_udp_client_receiver_options=.*/rtcm_udp_client_receiver_options=\'-TADJ=1\'/ "${rtkbase_path}"/settings.conf && \ sudo -u "${RTKBASE_USER}" sed -i s/^rtcm_serial_receiver_options=.*/rtcm_serial_receiver_options=\'-TADJ=1\'/ "${rtkbase_path}"/settings.conf && \ #remove SBAS Rtcm message (1107) as it is disabled in the F9P configuration. sudo -u "${RTKBASE_USER}" sed -i -r '/^rtcm_/s/1107(\([0-9]+\))?,//' "${rtkbase_path}"/settings.conf && \ diff --git a/tools/uninstall.sh b/tools/uninstall.sh index dde38774..5a8f8ad1 100755 --- a/tools/uninstall.sh +++ b/tools/uninstall.sh @@ -8,6 +8,9 @@ for service_name in str2str_tcp.service \ str2str_ntrip_B.service \ str2str_local_ntrip_caster \ str2str_rtcm_svr.service \ + str2str_rtcm_client.service \ + str2str_rtcm_udp_svr.service \ + str2str_rtcm_udp_client.service \ str2str_rtcm_serial.service \ str2str_file.service \ rtkbase_web \ diff --git a/unit/disabled/str2str_rtcm_udp_client.service b/unit/disabled/str2str_rtcm_udp_client.service new file mode 100644 index 00000000..d4a0c4c2 --- /dev/null +++ b/unit/disabled/str2str_rtcm_udp_client.service @@ -0,0 +1,21 @@ +[Unit] +Description=RTKBase RTCM UDP Client +#After=network-online.target +#Wants=network-online.target +Requires=str2str_tcp.service + +[Service] +Type=forking +User={user} +ExecStart={script_path}/run_cast.sh in_tcp out_rtcm_udp_client +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/unit/disabled/str2str_rtcm_udp_svr.service b/unit/disabled/str2str_rtcm_udp_svr.service new file mode 100644 index 00000000..e7ce44d8 --- /dev/null +++ b/unit/disabled/str2str_rtcm_udp_svr.service @@ -0,0 +1,21 @@ +[Unit] +Description=RTKBase RTCM UDP Server +#After=network-online.target +#Wants=network-online.target +Requires=str2str_tcp.service + +[Service] +Type=forking +User={user} +ExecStart={script_path}/run_cast.sh in_tcp out_rtcm_udp_svr +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/unit/str2str_rtcm_client.service b/unit/str2str_rtcm_client.service new file mode 100644 index 00000000..f7101fc6 --- /dev/null +++ b/unit/str2str_rtcm_client.service @@ -0,0 +1,21 @@ +[Unit] +Description=RTKBase RTCM Client +#After=network-online.target +#Wants=network-online.target +Requires=str2str_tcp.service + +[Service] +Type=forking +User={user} +ExecStart={script_path}/run_cast.sh in_tcp out_rtcm_client +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 bd3822e3..2036957c 100644 --- a/web_app/RTKBaseConfigManager.py +++ b/web_app/RTKBaseConfigManager.py @@ -165,6 +165,37 @@ def get_rtcm_svr_settings(self): ordered_rtcm_svr.append({key : self.config.get('rtcm_svr', key).strip("'")}) return ordered_rtcm_svr + def get_rtcm_client_settings(self): + """ + Get a subset of the settings from the file section in an ordered object + and remove the single quotes. + """ + ordered_rtcm_client = [{"source_section" : "rtcm_client"}] + for key in ("rtcm_client_addr", "rtcm_client_port", "rtcm_client_msg", "rtcm_client_receiver_options"): + ordered_rtcm_client.append({key : self.config.get('rtcm_client', key).strip("'")}) + print("ORDERED RTCM CLIENT", ordered_rtcm_client) + return ordered_rtcm_client + + def get_rtcm_udp_svr_settings(self): + """ + Get a subset of the settings from the file section in an ordered object + and remove the single quotes. + """ + ordered_rtcm_udp_svr = [{"source_section" : "rtcm_udp_svr"}] + for key in ("rtcm_udp_svr_port", "rtcm_udp_svr_msg", "rtcm_udp_svr_receiver_options"): + ordered_rtcm_udp_svr.append({key : self.config.get('rtcm_udp_svr', key).strip("'")}) + return ordered_rtcm_udp_svr + + def get_rtcm_udp_client_settings(self): + """ + Get a subset of the settings from the file section in an ordered object + and remove the single quotes. + """ + ordered_rtcm_udp_client = [{"source_section" : "rtcm_udp_client"}] + for key in ("rtcm_udp_client_addr", "rtcm_udp_client_port", "rtcm_udp_client_msg", "rtcm_udp_client_receiver_options"): + ordered_rtcm_udp_client.append({key : self.config.get('rtcm_udp_client', key).strip("'")}) + return ordered_rtcm_udp_client + def get_rtcm_serial_settings(self): """ Get a subset of the settings from the file section in an ordered object @@ -187,6 +218,9 @@ def get_ordered_settings(self): 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() + ordered_settings['rtcm_client'] = self.get_rtcm_client_settings() + ordered_settings['rtcm_udp_svr'] = self.get_rtcm_udp_svr_settings() + ordered_settings['rtcm_udp_client'] = self.get_rtcm_udp_client_settings() ordered_settings['rtcm_serial'] = self.get_rtcm_serial_settings() return ordered_settings diff --git a/web_app/server.py b/web_app/server.py index 2cfad4ee..1fcdfea4 100755 --- a/web_app/server.py +++ b/web_app/server.py @@ -105,6 +105,9 @@ {"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_client.service", "name" : "rtcm_client"}, + {"service_unit" : "str2str_rtcm_udp_svr.service", "name" : "rtcm_udp_svr"}, + {'service_unit' : 'str2str_rtcm_udp_client.service', "name" : "rtcm_udp_client"}, {'service_unit' : 'str2str_rtcm_serial.service', "name" : "rtcm_serial"}, {"service_unit" : "str2str_file.service", "name" : "file"}, {'service_unit' : 'rtkbase_archive.timer', "name" : "archive_timer"}, @@ -405,17 +408,23 @@ def settings_page(): ntrip_A_settings = rtkbaseconfig.get_ntrip_A_settings() ntrip_B_settings = rtkbaseconfig.get_ntrip_B_settings() local_ntripc_settings = rtkbaseconfig.get_local_ntripc_settings() - file_settings = rtkbaseconfig.get_file_settings() rtcm_svr_settings = rtkbaseconfig.get_rtcm_svr_settings() + rtcm_client_settings = rtkbaseconfig.get_rtcm_client_settings() + rtcm_udp_svr_settings = rtkbaseconfig.get_rtcm_udp_svr_settings() + rtcm_udp_client_settings = rtkbaseconfig.get_rtcm_udp_client_settings() rtcm_serial_settings = rtkbaseconfig.get_rtcm_serial_settings() + 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, local_ntripc_settings = local_ntripc_settings, - file_settings = file_settings, rtcm_svr_settings = rtcm_svr_settings, + rtcm_client_settings = rtcm_client_settings, + rtcm_udp_svr_settings = rtcm_udp_svr_settings, + rtcm_udp_client_settings = rtcm_udp_client_settings, rtcm_serial_settings = rtcm_serial_settings, + file_settings = file_settings, os_infos = distro.info(),) @app.route('/logs') @@ -897,7 +906,7 @@ def update_settings(json_msg): #Restart service if needed if source_section == "main": - restartServices(("main", "ntrip_A", "ntrip_B", "local_ntrip_caster", "rtcm_svr", "file", "rtcm_serial")) + restartServices(("main", "ntrip_A", "ntrip_B", "local_ntrip_caster", "rtcm_svr", "rtcm_client", "rtcm_udp_svr", "rtcm_udp_client", "file", "rtcm_serial")) elif source_section == "ntrip_A": restartServices(("ntrip_A",)) elif source_section == "ntrip_B": @@ -906,6 +915,12 @@ def update_settings(json_msg): restartServices(("local_ntrip_caster",)) elif source_section == "rtcm_svr": restartServices(("rtcm_svr",)) + elif source_section == "rtcm_client": + restartServices(("rtcm_client",)) + elif source_section == "rtcm_udp_svr": + restartServices(("rtcm_udp_svr",)) + elif source_section == "rtcm_udp_client": + restartServices(("rtcm_udp_client",)) elif source_section == "rtcm_serial": restartServices(("rtcm_serial",)) elif source_section == "local_storage": @@ -927,7 +942,7 @@ def update_settings(json_msg): #check if we run RTKBase for the first time after an update #and restart some services to let them send the new release number. if rtkbaseconfig.get("general", "updated", fallback="False").lower() == "true": - restartServices(["ntrip_A", "ntrip_B", "local_ntrip_caster", "rtcm_svr", "rtcm_serial"]) + restartServices(["ntrip_A", "ntrip_B", "local_ntrip_caster", "rtcm_svr", "rtcm_client", "rtcm_udp_svr", "rtcm_udp_client", "rtcm_serial"]) rtkbaseconfig.remove_option("general", "updated") rtkbaseconfig.write_file() #Start a "manager" thread diff --git a/web_app/static/settings.js b/web_app/static/settings.js index dc59f1c4..c33b7e9e 100644 --- a/web_app/static/settings.js +++ b/web_app/static/settings.js @@ -228,7 +228,7 @@ $(document).ready(function () { socket.emit("services switch", {"name" : "local_ntrip_caster", "active" : switchStatus}); }) - // #################### RTCM server service Switch ######################### + // #################### RTCM TCP server service Switch ######################### var rtcmSvrSwitch = $('#rtcm_svr-switch'); // set the switch to on/off depending of the service status @@ -257,11 +257,99 @@ $(document).ready(function () { socket.emit("services switch", {"name" : "rtcm_svr", "active" : switchStatus}); }) + // #################### RTCM TCP client service Switch ######################### + + var rtcmClientSwitch = $('#rtcm_client-switch'); + // set the switch to on/off depending of the service status + if (servicesStatus[5].active === true) { + //document.querySelector("#main-switch").bootstrapToggle('on'); + rtcmClientSwitch.bootstrapToggle('on', true); + } else { + //document.querySelector("#main-switch").bootstrapToggle('off'); + rtcmClientSwitch.bootstrapToggle('off', true); + } + //console.log(servicesStatus[3]); + if (servicesStatus[5].btn_color) { + rtcmClientSwitch.bootstrapToggle('setOnStyle', servicesStatus[5].btn_color); + } + if (servicesStatus[5].btn_off_color) { + rtcmClientSwitch.bootstrapToggle('setOffStyle', servicesStatus[5].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_client-switch" ).one("change", function(e) { + var switchStatus = $(this).prop('checked'); + //console.log(" e : " + e); + //console.log("RTCM Client SwitchStatus : " + switchStatus); + socket.emit("services switch", {"name" : "rtcm_client", "active" : switchStatus}); + }) + + // #################### RTCM UDP server service Switch ######################### + + var rtcmUdpSvrSwitch = $('#rtcm_udp_svr-switch'); + // set the switch to on/off depending of the service status + if (servicesStatus[6].active === true) { + //document.querySelector("#main-switch").bootstrapToggle('on'); + rtcmUdpSvrSwitch.bootstrapToggle('on', true); + } else { + //document.querySelector("#main-switch").bootstrapToggle('off'); + rtcmUdpSvrSwitch.bootstrapToggle('off', true); + } + //console.log(servicesStatus[3]); + if (servicesStatus[6].btn_color) { + rtcmUdpSvrSwitch.bootstrapToggle('setOnStyle', servicesStatus[6].btn_color); + } + if (servicesStatus[6].btn_off_color) { + rtcmUdpSvrSwitch.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. + $( "#rtcm_udp_svr-switch" ).one("change", function(e) { + var switchStatus = $(this).prop('checked'); + //console.log(" e : " + e); + //console.log("RTCM UDP Server SwitchStatus : " + switchStatus); + socket.emit("services switch", {"name" : "rtcm_udp_svr", "active" : switchStatus}); + }) + + // #################### RTCM UDP client service Switch ######################### + + var rtcmUdpClientSwitch = $('#rtcm_udp_client-switch'); + // set the switch to on/off depending of the service status + if (servicesStatus[6].active === true) { + //document.querySelector("#main-switch").bootstrapToggle('on'); + rtcmUdpClientSwitch.bootstrapToggle('on', true); + } else { + //document.querySelector("#main-switch").bootstrapToggle('off'); + rtcmUdpClientSwitch.bootstrapToggle('off', true); + } + //console.log(servicesStatus[3]); + if (servicesStatus[6].btn_color) { + rtcmUdpClientSwitch.bootstrapToggle('setOnStyle', servicesStatus[6].btn_color); + } + if (servicesStatus[6].btn_off_color) { + rtcmUdpClientSwitch.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. + $( "#rtcm_udp_client-switch" ).one("change", function(e) { + var switchStatus = $(this).prop('checked'); + //console.log(" e : " + e); + //console.log("RTCM UDP Client SwitchStatus : " + switchStatus); + socket.emit("services switch", {"name" : "rtcm_udp_client", "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) { + if (servicesStatus[8].active === true) { //document.querySelector("#main-switch").bootstrapToggle('on'); rtcmSerialSwitch.bootstrapToggle('on', true); } else { @@ -269,11 +357,11 @@ $(document).ready(function () { rtcmSerialSwitch.bootstrapToggle('off', true); } //console.log(servicesStatus[4]); - if (servicesStatus[5].btn_color) { - rtcmSerialSwitch.bootstrapToggle('setOnStyle', servicesStatus[5].btn_color); + if (servicesStatus[8].btn_color) { + rtcmSerialSwitch.bootstrapToggle('setOnStyle', servicesStatus[8].btn_color); } - if (servicesStatus[5].btn_off_color) { - rtcmSerialSwitch.bootstrapToggle('setOffStyle', servicesStatus[5].btn_off_color); + if (servicesStatus[8].btn_off_color) { + rtcmSerialSwitch.bootstrapToggle('setOffStyle', servicesStatus[8].btn_off_color); } // event for switching on/off service on user mouse click @@ -290,7 +378,7 @@ $(document).ready(function () { var fileSwitch = $('#file-switch'); // set the switch to on/off depending of the service status - if (servicesStatus[6].active === true) { + if (servicesStatus[9].active === true) { //document.querySelector("#main-switch").bootstrapToggle('on'); fileSwitch.bootstrapToggle('on', true); } else { @@ -298,11 +386,11 @@ $(document).ready(function () { fileSwitch.bootstrapToggle('off', true); } //console.log(servicesStatus[5]); - if (servicesStatus[6].btn_color) { - fileSwitch.bootstrapToggle('setOnStyle', servicesStatus[6].btn_color); + if (servicesStatus[9].btn_color) { + fileSwitch.bootstrapToggle('setOnStyle', servicesStatus[9].btn_color); } - if (servicesStatus[6].btn_off_color) { - fileSwitch.bootstrapToggle('setOffStyle', servicesStatus[6].btn_off_color); + if (servicesStatus[9].btn_off_color) { + fileSwitch.bootstrapToggle('setOffStyle', servicesStatus[9].btn_off_color); } // event for switching on/off service on user mouse click diff --git a/web_app/templates/settings.html b/web_app/templates/settings.html index de01cebc..70737d06 100644 --- a/web_app/templates/settings.html +++ b/web_app/templates/settings.html @@ -329,7 +329,7 @@

Services:

- +
@@ -371,6 +371,149 @@

Services:

+ +
+
+ + + + + + + + + +
+
+
+
+ +
+ +
Rtcm client address
+
+
+
+ +
+ +
Rtcm client port
+
+
+
+ +
+ +
Rtcm client messages list
+
+
+
+ +
+ +
Receiver dependent options
+
+
+ +
+ +
+
+ + + + + +