From 8f26224a9d1008bae02aeffe3b1c6ab9e44dc6ea Mon Sep 17 00:00:00 2001 From: Stefal Date: Fri, 29 Mar 2024 18:01:05 +0100 Subject: [PATCH 01/24] udev rules for mosaic-x5 gnss receiver --- tools/udev_rules/91-gnss.rules | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/tools/udev_rules/91-gnss.rules b/tools/udev_rules/91-gnss.rules index c9baa220..87d11a69 100644 --- a/tools/udev_rules/91-gnss.rules +++ b/tools/udev_rules/91-gnss.rules @@ -1 +1,7 @@ +#U-Blox F9P SUBSYSTEM=="tty", ATTRS{idVendor}=="1546", ATTRS{idProduct}=="01a9", SYMLINK+="ttyGNSS" + +#Septentrio +SUBSYSTEM=="tty", ATTRS{idVendor}=="152a", ATTRS{idProduct}=="85c0", ENV{USB_TYPE}="152a:85c0" +ENV{USB_TYPE}=="152a:85c0", SUBSYSTEM=="tty", ATTRS{bInterfaceNumber}=="02", GROUP="dialout", SYMLINK+="ttyGNSS" +ENV{USB_TYPE}=="152a:85c0", SUBSYSTEM=="tty", ATTRS{bInterfaceNumber}=="04", MODE="0660", GROUP="dialout", SYMLINK+="ttyGNSS_CTRL" From 3bcc1e3cf303dadebb0fe9cfda3998bb5e08f012 Mon Sep 17 00:00:00 2001 From: Stefal Date: Fri, 29 Mar 2024 18:59:12 +0100 Subject: [PATCH 02/24] Add python module to send command to mosaic X5 Add config example for mosaic X5 --- receiver_cfg/Septentrio_Mosaic-X5.cfg | 15 +++ tools/septentrio/septentrio_cmd.py | 142 ++++++++++++++++++++++++++ tools/septentrio/serial_comm.py | 54 ++++++++++ 3 files changed, 211 insertions(+) create mode 100644 receiver_cfg/Septentrio_Mosaic-X5.cfg create mode 100644 tools/septentrio/septentrio_cmd.py create mode 100644 tools/septentrio/serial_comm.py diff --git a/receiver_cfg/Septentrio_Mosaic-X5.cfg b/receiver_cfg/Septentrio_Mosaic-X5.cfg new file mode 100644 index 00000000..fb56a79a --- /dev/null +++ b/receiver_cfg/Septentrio_Mosaic-X5.cfg @@ -0,0 +1,15 @@ +setSBFOutput, Stream1, USB1 +setSBFOutput, Stream1, , , sec1 +setSBFOutput, Stream1, , MeasEpoch+MeasExtra+EndOfMeas +setSBFOutput, Stream1, , GPSRawCA+GPSRawL2C+GPSRawL5 +setSBFOutput, Stream1, , GLORawCA +setSBFOutput, Stream1, , GALRawFNAV+GALRawINAV+GALRawCNAV +setSBFOutput, Stream1, , BDSRaw+BDSRawB1C+BDSRawB2a+BDSRawB2b +setSBFOutput, Stream1, , QZSRawL1CA+QZSRawL2C+QZSRawL5 +setSBFOutput, Stream1, , NAVICRaw +setSBFOutput, Stream1, , GEORawL1+GEORawL5 +setSignalTracking, GPSL1CA+GPSL1PY+GPSL2PY+GPSL2C+GPSL5+GLOL1CA+GLOL2P+GLOL2CA+GLOL3+GALL1BC+GALE6BC+GALE5a+GALE5b+GALE5+GEOL1+GEOL5+BDSB1I+BDSB2I+BDSB3I+BDSB1C+BDSB2a+BDSB2b+QZSL1CA+QZSL2C+QZSL5+QZSL1CB+NAVICL5 +setSBFOutput, Stream1, , PVTGeodetic+ChannelStatus+ReceiverStatus+SatVisibility+ReceiverTime +setPVTMode, Static +setUSBInternetAccess, on +#END \ No newline at end of file diff --git a/tools/septentrio/septentrio_cmd.py b/tools/septentrio/septentrio_cmd.py new file mode 100644 index 00000000..76ede347 --- /dev/null +++ b/tools/septentrio/septentrio_cmd.py @@ -0,0 +1,142 @@ +#! /usr/bin/env python3 +from . serial_comm import SerialComm +from enum import Enum +from logging import getLogger +import xml.etree.ElementTree as ET +#Code inspired by https://github.com/jonamat/sim-modem +#TODO add __enter__ and __exit__ method to be able to use with Modem('/dev/tty..') as modem: do... + +class SeptGnss: + """Class for sending command to Septentrio Gnss receivers""" + + def __init__( + self, + address, + baudrate=115200, + timeout=2, + cmd_delay=0.1, + debug=False, + ): + self.comm = SerialComm( + address=address, + baudrate=baudrate, + timeout=timeout, + cmd_delay=cmd_delay, + connection_descriptor='USB2>', + ) + self.debug = debug + self.connect() + + def connect(self) -> None: + self.comm.send('exeEchoMessage, COM1, "A:HELLO", none') + read = self.comm.read_raw(1000) + try: + if b"A:HELLO" in read: + self.comm.connection_descriptor = read.decode().split()[-1] + else: + raise Exception + if self.debug: + print("GNSS receiver connected, debug mode enabled") + print("Connection descriptor: {}".format(self.comm.connection_descriptor)) + except Exception: + print("GNSS receiver did not respond correctly") + if self.debug: + print(read) + self.close() + + def close(self) -> None: + self.comm.close() + + # --------------------------------- Common methods --------------------------------- # + + def get_receiver_model(self) -> str: + read = self.send_read_until('lstInternalFile', 'Identification') + model = self.__parse_rcv_info(read, 'hwplatform', 'product') + return model + + def get_receiver_firmware(self) -> str: + read = self.send_read_until('lstInternalFile', 'Identification') + firmware = self.__parse_rcv_info(read, 'firmware', 'version') + return firmware + + def get_receiver_ip(self) -> str: + read = self.send_read_until('lstInternalFile', 'IPParameters') + ip_addr = self.__parse_rcv_info(read, 'inet', 'addr') + return ip_addr + + def __parse_rcv_info(self, pseudo_xml, element_tag, info) -> str: + ''' + This methode will try to parse the xml file received + when we send the lstInternalFile command + ''' + pseudo_xml = [line for line in pseudo_xml[2:-1] if not (line.startswith('$') or line == '---->')] + e = ET.ElementTree(ET.fromstring(''.join(pseudo_xml))) + res_info = None + for element in e.iter(): + #print(element.tag, element.attrib, element.text) + res_info = element.get(info) if element_tag in element.tag and element.get(info) else res_info + return res_info + + def get_port_applications(self, port) -> str: + read = self.send_read_until('getRegisteredApplications', port) + return read[-2].split(',')[-1].replace('"','').strip() + + def set_port_applications(self, port, applications_name) -> None: + read = self.send_read_until('exeRegisteredApplications', port, applications_name) + + def set_factory_default(self) -> None: + ''' + Reset receiver settings to factory defaults and restart it + Connection will be closed + ''' + if self.debug: + print("Sending: 'exeResetReceiver, Soft, Config'") + self.comm.send('exeResetReceiver, Soft, Config') + read = self.comm.read_until('STOP>') + if self.debug: + print("Receiving: {}".format(read)) + if read[-1] != 'STOP>' or read[0].startswith('$R?'): + raise Exception("Command failed!\nSent: 'exeResetReceiver, Soft, Config'\nReceived: {}".format(read)) + self.close() + print("Connection closed") + + def send_config_file(self, file) -> None: + ''' + Send user commands from a txt file, line by line + ''' + with open(file, 'r') as f: + for line in f: + if line.strip() != '' and not line.startswith('#'): + cmd,*args = line.split(',') + print(cmd, args) + self.send_read_until(cmd + ', ' + ', '.join(args)) + + def set_config_permanent(self) -> None: + ''' + Save current settings to boot config + ''' + read = self.send_read_until('exeCopyConfigFile', 'Current', 'Boot') + + # ----------------------------------- OTHERS --------------------------------- # + + def send_read_lines(self, cmd, *args) -> list: + if self.debug: + print("Sending: {}{}{}".format(cmd, ', ' if args else '', ', '.join(args))) + self.comm.send("{}{}{}".format(cmd, ', ' if args else '', ', '.join(args))) + read = self.comm.read_lines() + if self.debug: + print("Receiving: {}".format(read)) + if read[-1] != self.comm.connection_descriptor or read[0].startswith('$R?'): + raise Exception("Command failed!\nSent: {}\nReceived: {}".format((cmd + ', ' + ', '.join(args)), read)) + return read + + def send_read_until(self, cmd, *args) -> list: + if self.debug: + print("Sending: {}{}{}".format(cmd, ', ' if args else '', ', '.join(args))) + self.comm.send("{}{}{}".format(cmd, ', ' if args else '', ', '.join(args))) + read = self.comm.read_until() + if self.debug: + print("Receiving: {}".format(read)) + if read[-1] != self.comm.connection_descriptor or read[0].startswith('$R?'): + raise Exception("Command failed!\nSent: {}\nReceived: {}".format((cmd + ', ' + ', '.join(args)), read)) + return read diff --git a/tools/septentrio/serial_comm.py b/tools/septentrio/serial_comm.py new file mode 100644 index 00000000..dc694734 --- /dev/null +++ b/tools/septentrio/serial_comm.py @@ -0,0 +1,54 @@ +#! /usr/bin/env python3 +import serial +import time + + +class SerialComm: + def __init__( + self, + address, + baudrate=115200, + timeout=5, + write_timeout=5, + cmd_delay=0.1, + connection_descriptor='USB2>', + on_error=None, + byte_encoding="ISO-8859-1", + ): + self.cmd_delay = cmd_delay + self.connection_descriptor = connection_descriptor + self.on_error = on_error + self.byte_encoding = byte_encoding + self.device_serial = serial.Serial( + port=address, + baudrate=baudrate, + timeout=timeout, + write_timeout=write_timeout + ) + + def send(self, cmd) -> str: + self.device_serial.write(cmd.encode(self.byte_encoding) + b"\r") + time.sleep(self.cmd_delay) + + def send_raw(self, cmd): + self.device_serial.write(cmd) + time.sleep(self.cmd_delay) + + def read_lines(self) -> list: + read = self.device_serial.readlines() + for i, line in enumerate(read): + read[i] = line.decode(self.byte_encoding).strip() + return read + + def read_until(self, expect = None) -> list: + expect = self.connection_descriptor if expect == None else expect + read = self.device_serial.read_until(expected=bytes(expect, encoding=self.byte_encoding)) + read = read.decode(self.byte_encoding).strip().splitlines() + read = [ val for val in read if val != ''] + return read + + def read_raw(self, size: int): + return self.device_serial.read(size) + + def close(self): + self.device_serial.close() \ No newline at end of file From 1cef36d1e4f9e8dd675d0ce78b3d4239a0dace73 Mon Sep 17 00:00:00 2001 From: Stefal Date: Sat, 30 Mar 2024 19:05:49 +0000 Subject: [PATCH 03/24] Detect Septentrio receiver --- tools/install.sh | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/tools/install.sh b/tools/install.sh index 11412e39..7e0517fb 100755 --- a/tools/install.sh +++ b/tools/install.sh @@ -401,15 +401,18 @@ detect_gnss() { if [[ "$devname" == "bus/"* ]]; then continue; fi eval "$(udevadm info -q property --export -p "${syspath}")" if [[ -z "$ID_SERIAL" ]]; then continue; fi - if [[ "$ID_SERIAL" =~ (u-blox|skytraq) ]] + if [[ "$ID_SERIAL" =~ (u-blox|skytraq|Septentrio) ]] then detected_gnss[0]=$devname detected_gnss[1]=$ID_SERIAL #echo '/dev/'"${detected_gnss[0]}" ' - ' "${detected_gnss[1]}" + # If /dev/ttyGNSS is a symlink of the detected serial port, we've found the gnss receiver, break the loop. + # This test is useful with gnss receiver offering several serial ports (like mosaic X5). The Udev rule should symlink the right one with ttyGNSS + [[ '/dev/ttyGNSS' -ef '/dev/'"${detected_gnss[0]}" ]] && break fi done if [[ ${#detected_gnss[*]} -ne 2 ]]; then - vendor_and_product_ids=$(lsusb | grep -i "u-blox" | grep -Eo "[0-9A-Za-z]+:[0-9A-Za-z]+") + vendor_and_product_ids=$(lsusb | grep -i "u-blox\|Septentrio" | grep -Eo "[0-9A-Za-z]+:[0-9A-Za-z]+") if [[ -z "$vendor_and_product_ids" ]]; then echo 'NO USB GNSS RECEIVER DETECTED' echo 'YOU CAN REDETECT IT FROM THE WEB UI' @@ -422,10 +425,10 @@ detect_gnss() { fi fi # detection on uart port - echo '################################' - echo 'UART GNSS RECEIVER DETECTION' - echo '################################' if [[ ${#detected_gnss[*]} -ne 2 ]]; then + echo '################################' + echo 'UART GNSS RECEIVER DETECTION' + echo '################################' systemctl is-active --quiet str2str_tcp.service && sudo systemctl stop str2str_tcp.service && echo 'Stopping str2str_tcp service' for port in ttyS1 serial0 ttyS2 ttyS3 ttyS0; do for port_speed in 115200 57600 38400 19200 9600; do From ae42e1688c758d6db5b6b23380349bbff7755779 Mon Sep 17 00:00:00 2001 From: Stefal Date: Sat, 30 Mar 2024 19:47:51 +0000 Subject: [PATCH 04/24] Add context manager for SeptGnss class --- tools/septentrio/__init__.py | 0 tools/septentrio/septentrio_cmd.py | 9 ++++++++- 2 files changed, 8 insertions(+), 1 deletion(-) create mode 100644 tools/septentrio/__init__.py diff --git a/tools/septentrio/__init__.py b/tools/septentrio/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/tools/septentrio/septentrio_cmd.py b/tools/septentrio/septentrio_cmd.py index 76ede347..f528d501 100644 --- a/tools/septentrio/septentrio_cmd.py +++ b/tools/septentrio/septentrio_cmd.py @@ -4,7 +4,7 @@ from logging import getLogger import xml.etree.ElementTree as ET #Code inspired by https://github.com/jonamat/sim-modem -#TODO add __enter__ and __exit__ method to be able to use with Modem('/dev/tty..') as modem: do... +#TODO add __enter__ and __exit__ method to be able to use with SeptGnss('/dev/tty..') as gnss: do... class SeptGnss: """Class for sending command to Septentrio Gnss receivers""" @@ -46,6 +46,13 @@ def connect(self) -> None: def close(self) -> None: self.comm.close() + + def __enter__(self): + return self + + def __exit__(self, exception_type, exception_value, exception_traceback): + self.close() + print("closing") # --------------------------------- Common methods --------------------------------- # From 1ba17ffe153de6ef8bb037a5c545b7e02b00c52a Mon Sep 17 00:00:00 2001 From: Stefal Date: Sun, 31 Mar 2024 18:24:33 +0000 Subject: [PATCH 05/24] Fix config file. Without the '+', all previous settings one the same command, are lost --- receiver_cfg/Septentrio_Mosaic-X5.cfg | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/receiver_cfg/Septentrio_Mosaic-X5.cfg b/receiver_cfg/Septentrio_Mosaic-X5.cfg index fb56a79a..f579c3e8 100644 --- a/receiver_cfg/Septentrio_Mosaic-X5.cfg +++ b/receiver_cfg/Septentrio_Mosaic-X5.cfg @@ -1,15 +1,16 @@ +# Config file for using a Septentrio Mosaic X5 with RTKBase setSBFOutput, Stream1, USB1 setSBFOutput, Stream1, , , sec1 setSBFOutput, Stream1, , MeasEpoch+MeasExtra+EndOfMeas -setSBFOutput, Stream1, , GPSRawCA+GPSRawL2C+GPSRawL5 -setSBFOutput, Stream1, , GLORawCA -setSBFOutput, Stream1, , GALRawFNAV+GALRawINAV+GALRawCNAV -setSBFOutput, Stream1, , BDSRaw+BDSRawB1C+BDSRawB2a+BDSRawB2b -setSBFOutput, Stream1, , QZSRawL1CA+QZSRawL2C+QZSRawL5 -setSBFOutput, Stream1, , NAVICRaw -setSBFOutput, Stream1, , GEORawL1+GEORawL5 +setSBFOutput, Stream1, , +GPSRawCA+GPSRawL2C+GPSRawL5 +setSBFOutput, Stream1, , +GLORawCA +setSBFOutput, Stream1, , +GALRawFNAV+GALRawINAV+GALRawCNAV +setSBFOutput, Stream1, , +BDSRaw+BDSRawB1C+BDSRawB2a+BDSRawB2b +setSBFOutput, Stream1, , +QZSRawL1CA+QZSRawL2C+QZSRawL5 +setSBFOutput, Stream1, , +NAVICRaw +setSBFOutput, Stream1, , +GEORawL1+GEORawL5 setSignalTracking, GPSL1CA+GPSL1PY+GPSL2PY+GPSL2C+GPSL5+GLOL1CA+GLOL2P+GLOL2CA+GLOL3+GALL1BC+GALE6BC+GALE5a+GALE5b+GALE5+GEOL1+GEOL5+BDSB1I+BDSB2I+BDSB3I+BDSB1C+BDSB2a+BDSB2b+QZSL1CA+QZSL2C+QZSL5+QZSL1CB+NAVICL5 -setSBFOutput, Stream1, , PVTGeodetic+ChannelStatus+ReceiverStatus+SatVisibility+ReceiverTime +setSBFOutput, Stream1, , +PVTGeodetic+ChannelStatus+ReceiverStatus+SatVisibility+ReceiverTime setPVTMode, Static setUSBInternetAccess, on #END \ No newline at end of file From 07c73251a0eb698dbe651aeaa5c2482b62409e48 Mon Sep 17 00:00:00 2001 From: Stefal Date: Sun, 31 Mar 2024 18:34:46 +0000 Subject: [PATCH 06/24] Add sept_tool.py command line tool to configure the mosaic-X5 --- tools/sept_tool.py | 42 ++++++++++++++++++++++++++++++ tools/septentrio/septentrio_cmd.py | 10 ++++--- 2 files changed, 48 insertions(+), 4 deletions(-) create mode 100644 tools/sept_tool.py diff --git a/tools/sept_tool.py b/tools/sept_tool.py new file mode 100644 index 00000000..cf47fd00 --- /dev/null +++ b/tools/sept_tool.py @@ -0,0 +1,42 @@ +#! /usr/bin/env python3 + +import argparse +from septentrio.septentrio_cmd import * +from enum import Enum +from operator import methodcaller + +class CmdMapping(Enum): + """Mapping human command to septentrio methods""" + + #get_model = methodcaller('get_receiver_model') + get_model = 'get_receiver_model' + get_firmware = 'get_receiver_firmware' + get_ip = 'get_receiver_ip' + reset = 'set_factory_default' + test = 'test' + send_config_file = 'send_config_file' + +def arg_parse(): + """ Parse the command line you use to launch the script """ + + parser= argparse.ArgumentParser(prog='Septentrio tool', description="A tool to send comment to a Septentrio GNSS receiver") + parser.add_argument("-p", "--port", help="Port to connect to", type=str) + parser.add_argument("-b", "--baudrate", help="port baudrate", default=115200, type=int) + parser.add_argument("-c", "--command", nargs='+', help="Command to send to the gnss receiver", type=str) + parser.add_argument("-s", "--store", action='store_true', help="Store settings as permanent", default=False) + parser.add_argument("--version", action="version", version="%(prog)s 0.1") + args = parser.parse_args() + #print(args) + return args + +if __name__ == '__main__': + args = arg_parse() + #print(args) + command = args.command[0] + with SeptGnss(args.port, baudrate=args.baudrate, timeout=2, debug=False) as gnss: + res = methodcaller(CmdMapping[command].value, *args.command[1:])(gnss) + if type(res) is str: + print(res) + if args.store: + gnss.set_config_permanent() + #methodcaller(args.command[0])(gnss) \ No newline at end of file diff --git a/tools/septentrio/septentrio_cmd.py b/tools/septentrio/septentrio_cmd.py index f528d501..11f6707d 100644 --- a/tools/septentrio/septentrio_cmd.py +++ b/tools/septentrio/septentrio_cmd.py @@ -3,8 +3,8 @@ from enum import Enum from logging import getLogger import xml.etree.ElementTree as ET +import time #Code inspired by https://github.com/jonamat/sim-modem -#TODO add __enter__ and __exit__ method to be able to use with SeptGnss('/dev/tty..') as gnss: do... class SeptGnss: """Class for sending command to Septentrio Gnss receivers""" @@ -52,7 +52,6 @@ def __enter__(self): def __exit__(self, exception_type, exception_value, exception_traceback): self.close() - print("closing") # --------------------------------- Common methods --------------------------------- # @@ -107,16 +106,19 @@ def set_factory_default(self) -> None: self.close() print("Connection closed") - def send_config_file(self, file) -> None: + def send_config_file(self, file, perm=False) -> None: ''' Send user commands from a txt file, line by line + Set perm to True if you want to set these settings permanent ''' with open(file, 'r') as f: for line in f: if line.strip() != '' and not line.startswith('#'): cmd,*args = line.split(',') - print(cmd, args) + #print(cmd, args) self.send_read_until(cmd + ', ' + ', '.join(args)) + if perm: + self.set_config_permanent() def set_config_permanent(self) -> None: ''' From c8a1f0778bf5e6fd9c27bb0d4fb11114a6265259 Mon Sep 17 00:00:00 2001 From: Stefal Date: Sun, 31 Mar 2024 18:35:21 +0000 Subject: [PATCH 07/24] add mosaic-X5 configuration --- tools/install.sh | 19 +++++++++++++++++-- 1 file changed, 17 insertions(+), 2 deletions(-) diff --git a/tools/install.sh b/tools/install.sh index 7e0517fb..20d657f8 100755 --- a/tools/install.sh +++ b/tools/install.sh @@ -498,8 +498,7 @@ configure_gnss(){ then source <( grep '=' "${rtkbase_path}"/settings.conf ) systemctl is-active --quiet str2str_tcp.service && sudo systemctl stop str2str_tcp.service - #if the receiver is a U-Blox, launch the set_zed-f9p.sh. This script will reset the F9P and configure it with the corrects settings for rtkbase - #!!!!!!!!! CHECK THIS ON A REAL raspberry/orange Pi !!!!!!!!!!! + #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' ]] then #get F9P firmware release @@ -517,8 +516,24 @@ configure_gnss(){ 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_serial_receiver_options=.*/rtcm_serial_receiver_options=\'-TADJ=1\'/ "${rtkbase_path}"/settings.conf && \ + return $? + elif [[ $(python3 "${rtkbase_path}"/tools/sept_tool.py --port /dev/ttyGNSS_CTRL --baudrate ${com_port_settings%%:*} --command get_model) =~ 'mosaic-X5' ]] + then + #get mosaic-X5 firmware release + firmware=$(python3 "${rtkbase_path}"/tools/sept_tool.py --port /dev/ttyGNSS_CTRL --baudrate ${com_port_settings%%:*} --command get_firmware) + sudo -u "${RTKBASE_USER}" sed -i s/^receiver_firmware=.*/receiver_firmware=\'${firmware}\'/ "${rtkbase_path}"/settings.conf + #configure the mosaic-X5 for RTKBase + echo 'Resetting the mosaic-X5 settings....' + python3 "${rtkbase_path}"/tools/sept_tool.py --port /dev/ttyGNSS_CTRL --baudrate ${com_port_settings%%:*} --command reset + sleep 20 + echo 'Sending settings....' + python3 "${rtkbase_path}"/tools/sept_tool.py --port /dev/ttyGNSS_CTRL --baudrate ${com_port_settings%%:*} --command send_config_file "${rtkbase_path}"/receiver_cfg/Septentrio_Mosaic-X5.cfg --store + 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=\'Septentrio_Mosaic-X5\'/ "${rtkbase_path}"/settings.conf && \ + sudo -u "${RTKBASE_USER}" sed -i s/^receiver_format=.*/receiver_format=\'sbf\'/ "${rtkbase_path}"/settings.conf && \ return $? + else echo 'No Gnss receiver has been set. We can'\''t configure' return 1 From 7dad102a5046e39324de3d4ae15c8b95b3d525ff Mon Sep 17 00:00:00 2001 From: Stefal Date: Wed, 3 Apr 2024 19:44:37 +0000 Subject: [PATCH 08/24] enable pps --- receiver_cfg/Septentrio_Mosaic-X5.cfg | 1 + 1 file changed, 1 insertion(+) diff --git a/receiver_cfg/Septentrio_Mosaic-X5.cfg b/receiver_cfg/Septentrio_Mosaic-X5.cfg index f579c3e8..e7bd148a 100644 --- a/receiver_cfg/Septentrio_Mosaic-X5.cfg +++ b/receiver_cfg/Septentrio_Mosaic-X5.cfg @@ -12,5 +12,6 @@ setSBFOutput, Stream1, , +GEORawL1+GEORawL5 setSignalTracking, GPSL1CA+GPSL1PY+GPSL2PY+GPSL2C+GPSL5+GLOL1CA+GLOL2P+GLOL2CA+GLOL3+GALL1BC+GALE6BC+GALE5a+GALE5b+GALE5+GEOL1+GEOL5+BDSB1I+BDSB2I+BDSB3I+BDSB1C+BDSB2a+BDSB2b+QZSL1CA+QZSL2C+QZSL5+QZSL1CB+NAVICL5 setSBFOutput, Stream1, , +PVTGeodetic+ChannelStatus+ReceiverStatus+SatVisibility+ReceiverTime setPVTMode, Static +setPPSParameters, sec1, Low2High, , UTC setUSBInternetAccess, on #END \ No newline at end of file From 0544d23a9d20fb6cc73123f6a256f4bca4c4e173 Mon Sep 17 00:00:00 2001 From: Stefal Date: Wed, 3 Apr 2024 19:51:30 +0000 Subject: [PATCH 09/24] check authorized connection descriptor --- tools/septentrio/septentrio_cmd.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/tools/septentrio/septentrio_cmd.py b/tools/septentrio/septentrio_cmd.py index 11f6707d..4fbb104a 100644 --- a/tools/septentrio/septentrio_cmd.py +++ b/tools/septentrio/septentrio_cmd.py @@ -31,7 +31,10 @@ def connect(self) -> None: self.comm.send('exeEchoMessage, COM1, "A:HELLO", none') read = self.comm.read_raw(1000) try: - if b"A:HELLO" in read: + check_hello = b"A:HELLO" in read + res_descriptor = read.decode().split()[-1] + check_descriptor = 'COM' in res_descriptor or 'USB' in res_descriptor or 'IP1' in res_descriptor + if check_hello and check_descriptor: self.comm.connection_descriptor = read.decode().split()[-1] else: raise Exception From 342ae04e41ca03a68b048c4201e24fd5ad08d228 Mon Sep 17 00:00:00 2001 From: Stefal Date: Thu, 4 Apr 2024 22:13:04 +0200 Subject: [PATCH 10/24] update --- run_cast.sh | 4 ++-- tools/install.sh | 5 ++--- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/run_cast.sh b/run_cast.sh index afdfe2c7..06fa2873 100755 --- a/run_cast.sh +++ b/run_cast.sh @@ -1,11 +1,11 @@ -#!/bin/bash +#!/bin/bash -xv # # run_cast.sh: script to run NTRIP caster by STR2STR # You can read the RTKLIB manual for more str2str informations: # https://github.com/tomojitakasu/RTKLIB BASEDIR=$(dirname "$0") -source <( grep '=' ${BASEDIR}/settings.conf ) #import settings +source <( grep -v '^#' "${rtkbase_path}"/settings.conf | grep '=' ) #import settings receiver_info="RTKBase ${receiver},${version} ${receiver_firmware}" in_serial="serial://${com_port}:${com_port_settings}#${receiver_format}" diff --git a/tools/install.sh b/tools/install.sh index 20d657f8..0d7c75d6 100755 --- a/tools/install.sh +++ b/tools/install.sh @@ -190,7 +190,6 @@ install_rtklib() { cp "${rtkbase_path}"'/tools/bin/rtklib_b34i/'"${arch_package}"/convbin /usr/local/bin/ else echo 'No binary available for ' "${computer_model}" ' - ' "${arch_package}" '. We will build it from source' - echo 'exit test' ; exit _compil_rtklib fi } @@ -496,7 +495,7 @@ configure_gnss(){ echo '################################' if [ -d "${rtkbase_path}" ] then - source <( grep '=' "${rtkbase_path}"/settings.conf ) + source <( grep -v '^#' "${rtkbase_path}"/settings.conf | grep '=' ) systemctl is-active --quiet str2str_tcp.service && sudo systemctl stop str2str_tcp.service #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' ]] @@ -521,7 +520,7 @@ configure_gnss(){ elif [[ $(python3 "${rtkbase_path}"/tools/sept_tool.py --port /dev/ttyGNSS_CTRL --baudrate ${com_port_settings%%:*} --command get_model) =~ 'mosaic-X5' ]] then #get mosaic-X5 firmware release - firmware=$(python3 "${rtkbase_path}"/tools/sept_tool.py --port /dev/ttyGNSS_CTRL --baudrate ${com_port_settings%%:*} --command get_firmware) + firmware="$(python3 "${rtkbase_path}"/tools/sept_tool.py --port /dev/ttyGNSS_CTRL --baudrate ${com_port_settings%%:*} --command get_firmware)" sudo -u "${RTKBASE_USER}" sed -i s/^receiver_firmware=.*/receiver_firmware=\'${firmware}\'/ "${rtkbase_path}"/settings.conf #configure the mosaic-X5 for RTKBase echo 'Resetting the mosaic-X5 settings....' From a9f81ab81d9d63e0b22017b801e87d9504f49708 Mon Sep 17 00:00:00 2001 From: Stefal Date: Fri, 5 Apr 2024 17:19:41 +0200 Subject: [PATCH 11/24] adding nftables dependencie --- tools/install.sh | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/tools/install.sh b/tools/install.sh index 0d7c75d6..ad34a215 100755 --- a/tools/install.sh +++ b/tools/install.sh @@ -104,7 +104,7 @@ install_dependencies() { echo 'INSTALLING DEPENDENCIES' echo '################################' apt-get "${APT_TIMEOUT}" update -y || exit 1 - apt-get "${APT_TIMEOUT}" install -y git build-essential pps-tools python3-pip python3-venv python3-dev python3-setuptools python3-wheel python3-serial libsystemd-dev bc dos2unix socat zip unzip pkg-config psmisc proj-bin || exit 1 + apt-get "${APT_TIMEOUT}" install -y git build-essential pps-tools python3-pip python3-venv python3-dev python3-setuptools python3-wheel python3-serial libsystemd-dev bc dos2unix socat zip unzip pkg-config psmisc proj-bin nftables || exit 1 apt-get install -y libxml2-dev libxslt-dev || exit 1 # needed for lxml (for pystemd) #apt-get "${APT_TIMEOUT}" upgrade -y } @@ -502,6 +502,7 @@ configure_gnss(){ then #get F9P firmware release firmware=$(python3 "${rtkbase_path}"/tools/ubxtool -f /dev/"${com_port}" -s ${com_port_settings%%:*} -p MON-VER | grep 'FWVER' | awk '{print $NF}') + echo 'F9P Firmware: ' "${firmware}" sudo -u "${RTKBASE_USER}" sed -i s/^receiver_firmware=.*/receiver_firmware=\'${firmware}\'/ "${rtkbase_path}"/settings.conf #configure the F9P for RTKBase "${rtkbase_path}"/tools/set_zed-f9p.sh /dev/${com_port} ${com_port_settings%%:*} "${rtkbase_path}"/receiver_cfg/U-Blox_ZED-F9P_rtkbase.cfg && \ @@ -520,12 +521,14 @@ configure_gnss(){ elif [[ $(python3 "${rtkbase_path}"/tools/sept_tool.py --port /dev/ttyGNSS_CTRL --baudrate ${com_port_settings%%:*} --command get_model) =~ 'mosaic-X5' ]] then #get mosaic-X5 firmware release - firmware="$(python3 "${rtkbase_path}"/tools/sept_tool.py --port /dev/ttyGNSS_CTRL --baudrate ${com_port_settings%%:*} --command get_firmware)" + firmware="$(python3 "${rtkbase_path}"/tools/sept_tool.py --port /dev/ttyGNSS_CTRL --baudrate ${com_port_settings%%:*} --command get_firmware || '?' + echo 'Mosaic-X5 Firmware: ' "${firmware}" sudo -u "${RTKBASE_USER}" sed -i s/^receiver_firmware=.*/receiver_firmware=\'${firmware}\'/ "${rtkbase_path}"/settings.conf #configure the mosaic-X5 for RTKBase echo 'Resetting the mosaic-X5 settings....' python3 "${rtkbase_path}"/tools/sept_tool.py --port /dev/ttyGNSS_CTRL --baudrate ${com_port_settings%%:*} --command reset - sleep 20 + echo 'Waiting 30s for mosaic-X5 reboot' + sleep 30 echo 'Sending settings....' python3 "${rtkbase_path}"/tools/sept_tool.py --port /dev/ttyGNSS_CTRL --baudrate ${com_port_settings%%:*} --command send_config_file "${rtkbase_path}"/receiver_cfg/Septentrio_Mosaic-X5.cfg --store sudo -u "${RTKBASE_USER}" sed -i s/^com_port_settings=.*/com_port_settings=\'115200:8:n:1\'/ "${rtkbase_path}"/settings.conf && \ From 4f7be307da61b772b0ec4870c13f83188a02c6f8 Mon Sep 17 00:00:00 2001 From: Stefal Date: Wed, 10 Apr 2024 20:56:37 +0200 Subject: [PATCH 12/24] Higher timeout needed for the first connexion with the Mosaic --- tools/sept_tool.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tools/sept_tool.py b/tools/sept_tool.py index cf47fd00..a8adf5ea 100644 --- a/tools/sept_tool.py +++ b/tools/sept_tool.py @@ -33,7 +33,7 @@ def arg_parse(): args = arg_parse() #print(args) command = args.command[0] - with SeptGnss(args.port, baudrate=args.baudrate, timeout=2, debug=False) as gnss: + with SeptGnss(args.port, baudrate=args.baudrate, timeout=30, debug=True) as gnss: res = methodcaller(CmdMapping[command].value, *args.command[1:])(gnss) if type(res) is str: print(res) From ff35a86cb301150aa68d181006156631cf19814f Mon Sep 17 00:00:00 2001 From: Stefal Date: Wed, 10 Apr 2024 22:41:12 +0200 Subject: [PATCH 13/24] rtkbase_path is not available. Revert to $BASEDIR --- run_cast.sh | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/run_cast.sh b/run_cast.sh index 06fa2873..26079b74 100755 --- a/run_cast.sh +++ b/run_cast.sh @@ -4,8 +4,8 @@ # You can read the RTKLIB manual for more str2str informations: # https://github.com/tomojitakasu/RTKLIB -BASEDIR=$(dirname "$0") -source <( grep -v '^#' "${rtkbase_path}"/settings.conf | grep '=' ) #import settings +BASEDIR="$(dirname "$0")" +source <( grep -v '^#' "${BASEDIR}"/settings.conf | grep '=' ) #import settings receiver_info="RTKBase ${receiver},${version} ${receiver_firmware}" in_serial="serial://${com_port}:${com_port_settings}#${receiver_format}" From 4ba984dbcc4dd832c2b874f8313c36bb9913b2aa Mon Sep 17 00:00:00 2001 From: Stefal Date: Sun, 19 May 2024 20:15:10 +0200 Subject: [PATCH 14/24] Adding Mosaic reverse proxy, to open the integrated mosaic web server with rtkbase authentication --- settings.conf.default | 4 + web_app/RTKBaseConfigManager.py | 2 +- web_app/mosaic_rproxy_server.py | 150 +++++++++++++++++++++++++++++ web_app/server.py | 7 +- web_app/templates/proxy_base.html | 35 +++++++ web_app/templates/proxy_login.html | 7 ++ web_app/templates/settings.html | 6 +- 7 files changed, 208 insertions(+), 3 deletions(-) create mode 100644 web_app/mosaic_rproxy_server.py create mode 100644 web_app/templates/proxy_base.html create mode 100644 web_app/templates/proxy_login.html diff --git a/settings.conf.default b/settings.conf.default index cfe9f01c..e69ba60c 100644 --- a/settings.conf.default +++ b/settings.conf.default @@ -50,6 +50,10 @@ tcp_port='5015' ext_tcp_source='' #ext_tcp_port is the port used for ext_tcp_source ext_tcp_port='' +#ip address of the integrated web service (ie on Mosaic X5) +gnss_rcv_web_ip=192.168.3.1 +#port number for the Flask proxy app used to display the Gnss receiver web service +gnss_rcv_web_proxy_port=9090 [local_storage] diff --git a/web_app/RTKBaseConfigManager.py b/web_app/RTKBaseConfigManager.py index f365c973..bd3822e3 100644 --- a/web_app/RTKBaseConfigManager.py +++ b/web_app/RTKBaseConfigManager.py @@ -110,7 +110,7 @@ def get_main_settings(self): and remove the single quotes. """ ordered_main = [{"source_section" : "main"}] - for key in ("position", "com_port", "com_port_settings", "receiver", "receiver_firmware", "receiver_format", "antenna_info", "tcp_port"): + for key in ("position", "com_port", "com_port_settings", "receiver", "receiver_firmware", "receiver_format", "antenna_info", "tcp_port", "gnss_rcv_web_ip", "gnss_rcv_web_proxy_port"): ordered_main.append({key : self.config.get('main', key).strip("'")}) return ordered_main diff --git a/web_app/mosaic_rproxy_server.py b/web_app/mosaic_rproxy_server.py new file mode 100644 index 00000000..ddd24a64 --- /dev/null +++ b/web_app/mosaic_rproxy_server.py @@ -0,0 +1,150 @@ +#!/usr/bin/python + +# author: Stéphane Péneau +# source: https://github.com/Stefal/rtkbase + +# Reverse proxy server to acces the Mosaic integrated Web Server + +import eventlet +eventlet.monkey_patch() + +import os +import requests + +from RTKBaseConfigManager import RTKBaseConfigManager +from dotenv import load_dotenv # pip package python-dotenv + +from flask_bootstrap import Bootstrap4 +from flask import Flask, render_template, session, request, flash, url_for, Response +from flask import send_file, send_from_directory, redirect, abort +from flask import g +from flask_wtf import FlaskForm +from wtforms import PasswordField, BooleanField, SubmitField +from flask_login import LoginManager, login_user, logout_user, login_required, current_user, UserMixin +from wtforms.validators import ValidationError, DataRequired, EqualTo +from flask_socketio import SocketIO, emit, disconnect +import urllib + +from werkzeug.security import generate_password_hash +from werkzeug.security import check_password_hash +from werkzeug.utils import safe_join +import urllib + +app = Flask(__name__) +app.debug = True +app.config["SECRET_KEY"] = "secret!" +app.config["LOGIN_DISABLED"] = False + +rtkbase_path = os.path.abspath(os.path.join(os.path.dirname(__file__), "../")) + +login=LoginManager(app) +login.login_view = 'login_page' +socketio = SocketIO(app) +bootstrap = Bootstrap4(app) + +#Get settings from settings.conf.default and settings.conf +rtkbaseconfig = RTKBaseConfigManager(os.path.join(rtkbase_path, "settings.conf.default"), os.path.join(rtkbase_path, "settings.conf")) +GNSS_RCV_WEB_URL = str("{}{}".format("http://", rtkbaseconfig.get("main", "gnss_rcv_web_ip"))) + +class User(UserMixin): + """ Class for user authentification """ + def __init__(self, username): + self.id=username + self.password_hash = rtkbaseconfig.get("general", "web_password_hash") + + def check_password(self, password): + return check_password_hash(self.password_hash, password) + +class LoginForm(FlaskForm): + """ Class for the loginform""" + #username = StringField('Username', validators=[DataRequired()]) + password = PasswordField('Please enter the RTKBase password:', validators=[DataRequired()]) + remember_me = BooleanField('Remember Me') + submit = SubmitField('Sign In') + +@app.before_request +def inject_release(): + """ + Insert the RTKBase release number as a global variable for Flask/Jinja + """ + g.version = rtkbaseconfig.get("general", "version") + +@login.user_loader +def load_user(id): + return User(id) + +#proxy code from https://stackoverflow.com/a/36601467 +@app.route('/', defaults={'path': ''}, methods=["GET", "POST"]) # ref. https://medium.com/@zwork101/making-a-flask-proxy-server-online-in-10-lines-of-code-44b8721bca6 +@app.route('/', methods=["GET", "POST"]) # NOTE: better to specify which methods to be accepted. Otherwise, only GET will be accepted. Ref: +@login_required +def redirect_to_API_HOST(path): #NOTE var :path will be unused as all path we need will be read from :request ie from flask import request + res = requests.request( # ref. https://stackoverflow.com/a/36601467/248616 + method = request.method, + url = request.url.replace(request.host_url, f'{GNSS_RCV_WEB_URL}/'), + headers = {k:v for k,v in request.headers if k.lower() != 'host'}, # exclude 'host' header + data = request.get_data(), + cookies = request.cookies, + allow_redirects = False, + ) + """ print("method: ", request.method) + print("request posturl: ", request.url) + print("request host: ", request.host_url) + print("url: ", request.url.replace(request.host_url, f'{GNSS_RCV_WEB_URL}/')) + print("header: ", {k:v for k,v in request.headers if k.lower() != 'host'}) + print("data: ", request.get_data()) + print("cookies: ", request.cookies) + print("host url split", urllib.parse.urlsplit(request.host_url)) + print("host url split2", urllib.parse.urlsplit(request.base_url).hostname) + old = urllib.parse.urlparse(request.host_url) + new = urllib.parse.ParseResult(scheme=old.scheme, netloc="{}:{}".format(old.hostname, 9090), + path=old.path, params=old.params, query=old.query, fragment=old.fragment) + print("new_url: ", new.geturl()) """ + #region exlcude some keys in :res response + excluded_headers = ['content-encoding', 'content-length', 'transfer-encoding', 'connection'] #NOTE we here exclude all "hop-by-hop headers" defined by RFC 2616 section 13.5.1 ref. https://www.rfc-editor.org/rfc/rfc2616#section-13.5.1 + headers = [ + (k,v) for k,v in res.raw.headers.items() + if k.lower() not in excluded_headers + ] + #endregion exlcude some keys in :res response + + response = Response(res.content, res.status_code, headers) + print(response) + return response + + +@app.route('/login', methods=['GET', 'POST']) +def login_page(): + if current_user.is_authenticated: + return redirect(url_for('index')) + + loginform = LoginForm() + if loginform.validate_on_submit(): + user = User('admin') + password = loginform.password.data + if not user.check_password(password): + return abort(401) + + login_user(user, remember=loginform.remember_me.data) + next_page = request.args.get('redirect_to_API_HOST') + if not next_page or urllib.parse.urlsplit(next_page).netloc != '': + next_page = url_for('redirect_to_API_HOST') + + return redirect(next_page) + + return render_template('proxy_login.html', title='Sign In', form=loginform) + +@app.route('/logout') +def logout(): + logout_user() + return redirect(url_for('login_page')) + +if __name__ == "__main__": + try: + #check if authentification is required + if not rtkbaseconfig.get_web_authentification(): + app.config["LOGIN_DISABLED"] = True + app.secret_key = rtkbaseconfig.get_secret_key() + socketio.run(app, host = "::", port = rtkbaseconfig.get("main", "gnss_rcv_web_proxy_port", fallback=9090)) # IPv6 "::" is mapped to IPv4 + + except KeyboardInterrupt: + print("Server interrupted by user!!") diff --git a/web_app/server.py b/web_app/server.py index 0b82d294..2cfad4ee 100755 --- a/web_app/server.py +++ b/web_app/server.py @@ -127,7 +127,7 @@ def check_password(self, password): class LoginForm(FlaskForm): """ Class for the loginform""" #username = StringField('Username', validators=[DataRequired()]) - password = PasswordField('Please enter the password:', validators=[DataRequired()]) + password = PasswordField('Please enter the RTKBase password:', validators=[DataRequired()]) remember_me = BooleanField('Remember Me') submit = SubmitField('Sign In') @@ -396,7 +396,12 @@ def settings_page(): """ The settings page where you can manage the various services, the parameters, update, power... """ + host_url = urllib.parse.urlparse(request.host_url) + gnss_rcv_url = urllib.parse.ParseResult(scheme=host_url.scheme, netloc="{}:{}".format(host_url.hostname, rtkbaseconfig.get("main", "gnss_rcv_web_proxy_port")), + path=host_url.path, params=host_url.params, query=host_url.query, fragment=host_url.fragment) + #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() local_ntripc_settings = rtkbaseconfig.get_local_ntripc_settings() diff --git a/web_app/templates/proxy_base.html b/web_app/templates/proxy_base.html new file mode 100644 index 00000000..96a5c91f --- /dev/null +++ b/web_app/templates/proxy_base.html @@ -0,0 +1,35 @@ +{% from 'bootstrap4/nav.html' import render_nav_item %} + + + + {% block head %} + + + + + {% block styles %} + + + {% endblock %} + + {% endblock %} + + + + +
+ {% block content %}{% endblock %} +
+
+ + + + {% block scripts %} + + + + + + {% endblock %} + + diff --git a/web_app/templates/proxy_login.html b/web_app/templates/proxy_login.html new file mode 100644 index 00000000..1abab9f8 --- /dev/null +++ b/web_app/templates/proxy_login.html @@ -0,0 +1,7 @@ +{% extends 'proxy_base.html' %} +{% block content %} +
+ {% from 'bootstrap4/form.html' import render_form %} + {{ render_form(form) }} +
+{% endblock %} \ No newline at end of file diff --git a/web_app/templates/settings.html b/web_app/templates/settings.html index b6d96dc5..adfd04c7 100644 --- a/web_app/templates/settings.html +++ b/web_app/templates/settings.html @@ -520,7 +520,11 @@

System Settings:

Gnss receiver:
- {{ main_settings[3].receiver }} - {{ main_settings[4].receiver_firmware }} + {{ main_settings[3].receiver }} - {{ main_settings[4].receiver_firmware }} - + {% if 'septentrio' in main_settings[3].receiver.lower() %} + + {% endif %} +
From f8126c5c17ed334b628b78a90e875876ccacfd0a Mon Sep 17 00:00:00 2001 From: Stefal Date: Mon, 20 May 2024 16:38:48 +0000 Subject: [PATCH 15/24] fix firmware error detection --- tools/install.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tools/install.sh b/tools/install.sh index ad34a215..0cfecc69 100755 --- a/tools/install.sh +++ b/tools/install.sh @@ -521,7 +521,7 @@ configure_gnss(){ elif [[ $(python3 "${rtkbase_path}"/tools/sept_tool.py --port /dev/ttyGNSS_CTRL --baudrate ${com_port_settings%%:*} --command get_model) =~ 'mosaic-X5' ]] then #get mosaic-X5 firmware release - firmware="$(python3 "${rtkbase_path}"/tools/sept_tool.py --port /dev/ttyGNSS_CTRL --baudrate ${com_port_settings%%:*} --command get_firmware || '?' + firmware="$(python3 "${rtkbase_path}"/tools/sept_tool.py --port /dev/ttyGNSS_CTRL --baudrate ${com_port_settings%%:*} --command get_firmware)" || firmware='?' echo 'Mosaic-X5 Firmware: ' "${firmware}" sudo -u "${RTKBASE_USER}" sed -i s/^receiver_firmware=.*/receiver_firmware=\'${firmware}\'/ "${rtkbase_path}"/settings.conf #configure the mosaic-X5 for RTKBase From 4813b6da92575a71c9c6b3d7ba5fbc076692e41a Mon Sep 17 00:00:00 2001 From: Stefal Date: Mon, 20 May 2024 18:26:47 +0000 Subject: [PATCH 16/24] disable debug --- tools/sept_tool.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tools/sept_tool.py b/tools/sept_tool.py index a8adf5ea..96394650 100644 --- a/tools/sept_tool.py +++ b/tools/sept_tool.py @@ -33,7 +33,7 @@ def arg_parse(): args = arg_parse() #print(args) command = args.command[0] - with SeptGnss(args.port, baudrate=args.baudrate, timeout=30, debug=True) as gnss: + with SeptGnss(args.port, baudrate=args.baudrate, timeout=30, debug=False) as gnss: res = methodcaller(CmdMapping[command].value, *args.command[1:])(gnss) if type(res) is str: print(res) From bb4f1a1adc9cba3073e5ee1a47b057661f3d7d34 Mon Sep 17 00:00:00 2001 From: Stefal Date: Mon, 20 May 2024 18:34:32 +0000 Subject: [PATCH 17/24] add unit file for reverse proxy --- unit/rtkbase_gnss_web_proxy.service | 16 +++ web_app/gnss_rproxy_server.py | 150 ++++++++++++++++++++++++++++ 2 files changed, 166 insertions(+) create mode 100644 unit/rtkbase_gnss_web_proxy.service create mode 100644 web_app/gnss_rproxy_server.py diff --git a/unit/rtkbase_gnss_web_proxy.service b/unit/rtkbase_gnss_web_proxy.service new file mode 100644 index 00000000..e8964c02 --- /dev/null +++ b/unit/rtkbase_gnss_web_proxy.service @@ -0,0 +1,16 @@ +[Unit] +Description=RTKBase Reverse Proxy for Gnss receiver Web Server +#After=network-online.target +#Wants=network-online.target + +[Service] +User={user} +ExecStart={python_path} {script_path}/web_app/gnss_rproxy_server.py +Restart=on-failure +RestartSec=30 +ProtectHome=read-only +ProtectSystem=strict +ReadWritePaths={script_path} /usr/local/bin + +[Install] +WantedBy=multi-user.target diff --git a/web_app/gnss_rproxy_server.py b/web_app/gnss_rproxy_server.py new file mode 100644 index 00000000..83a0a34c --- /dev/null +++ b/web_app/gnss_rproxy_server.py @@ -0,0 +1,150 @@ +#!/usr/bin/python + +# author: Stéphane Péneau +# source: https://github.com/Stefal/rtkbase + +# Reverse proxy server to acces a Gnss receiver integrated Web Server (Mosaic-X5 or other) + +import eventlet +eventlet.monkey_patch() + +import os +import requests + +from RTKBaseConfigManager import RTKBaseConfigManager +#from dotenv import load_dotenv # pip package python-dotenv + +from flask_bootstrap import Bootstrap4 +from flask import Flask, render_template, session, request, flash, url_for, Response +from flask import send_file, send_from_directory, redirect, abort +from flask import g +from flask_wtf import FlaskForm +from wtforms import PasswordField, BooleanField, SubmitField +from flask_login import LoginManager, login_user, logout_user, login_required, current_user, UserMixin +from wtforms.validators import ValidationError, DataRequired, EqualTo +#from flask_socketio import SocketIO, emit, disconnect +import urllib + +from werkzeug.security import generate_password_hash +from werkzeug.security import check_password_hash +from werkzeug.utils import safe_join +import urllib + +app = Flask(__name__) +app.debug = True +app.config["SECRET_KEY"] = "secret!" +app.config["LOGIN_DISABLED"] = False + +rtkbase_path = os.path.abspath(os.path.join(os.path.dirname(__file__), "../")) + +login=LoginManager(app) +login.login_view = 'login_page' +#socketio = SocketIO(app) +bootstrap = Bootstrap4(app) + +#Get settings from settings.conf.default and settings.conf +rtkbaseconfig = RTKBaseConfigManager(os.path.join(rtkbase_path, "settings.conf.default"), os.path.join(rtkbase_path, "settings.conf")) +GNSS_RCV_WEB_URL = str("{}{}".format("http://", rtkbaseconfig.get("main", "gnss_rcv_web_ip"))) + +class User(UserMixin): + """ Class for user authentification """ + def __init__(self, username): + self.id=username + self.password_hash = rtkbaseconfig.get("general", "web_password_hash") + + def check_password(self, password): + return check_password_hash(self.password_hash, password) + +class LoginForm(FlaskForm): + """ Class for the loginform""" + #username = StringField('Username', validators=[DataRequired()]) + password = PasswordField('Please enter the RTKBase password:', validators=[DataRequired()]) + remember_me = BooleanField('Remember Me') + submit = SubmitField('Sign In') + +@app.before_request +def inject_release(): + """ + Insert the RTKBase release number as a global variable for Flask/Jinja + """ + g.version = rtkbaseconfig.get("general", "version") + +@login.user_loader +def load_user(id): + return User(id) + +#proxy code from https://stackoverflow.com/a/36601467 +@app.route('/', defaults={'path': ''}, methods=["GET", "POST"]) # ref. https://medium.com/@zwork101/making-a-flask-proxy-server-online-in-10-lines-of-code-44b8721bca6 +@app.route('/', methods=["GET", "POST"]) # NOTE: better to specify which methods to be accepted. Otherwise, only GET will be accepted. Ref: +@login_required +def redirect_to_API_HOST(path): #NOTE var :path will be unused as all path we need will be read from :request ie from flask import request + res = requests.request( # ref. https://stackoverflow.com/a/36601467/248616 + method = request.method, + url = request.url.replace(request.host_url, f'{GNSS_RCV_WEB_URL}/'), + headers = {k:v for k,v in request.headers if k.lower() != 'host'}, # exclude 'host' header + data = request.get_data(), + cookies = request.cookies, + allow_redirects = False, + ) + """ print("method: ", request.method) + print("request posturl: ", request.url) + print("request host: ", request.host_url) + print("url: ", request.url.replace(request.host_url, f'{GNSS_RCV_WEB_URL}/')) + print("header: ", {k:v for k,v in request.headers if k.lower() != 'host'}) + print("data: ", request.get_data()) + print("cookies: ", request.cookies) + print("host url split", urllib.parse.urlsplit(request.host_url)) + print("host url split2", urllib.parse.urlsplit(request.base_url).hostname) + old = urllib.parse.urlparse(request.host_url) + new = urllib.parse.ParseResult(scheme=old.scheme, netloc="{}:{}".format(old.hostname, 9090), + path=old.path, params=old.params, query=old.query, fragment=old.fragment) + print("new_url: ", new.geturl()) """ + #region exlcude some keys in :res response + excluded_headers = ['content-encoding', 'content-length', 'transfer-encoding', 'connection'] #NOTE we here exclude all "hop-by-hop headers" defined by RFC 2616 section 13.5.1 ref. https://www.rfc-editor.org/rfc/rfc2616#section-13.5.1 + headers = [ + (k,v) for k,v in res.raw.headers.items() + if k.lower() not in excluded_headers + ] + #endregion exlcude some keys in :res response + + response = Response(res.content, res.status_code, headers) + print(response) + return response + + +@app.route('/login', methods=['GET', 'POST']) +def login_page(): + if current_user.is_authenticated: + return redirect(url_for('index')) + + loginform = LoginForm() + if loginform.validate_on_submit(): + user = User('admin') + password = loginform.password.data + if not user.check_password(password): + return abort(401) + + login_user(user, remember=loginform.remember_me.data) + next_page = request.args.get('redirect_to_API_HOST') + if not next_page or urllib.parse.urlsplit(next_page).netloc != '': + next_page = url_for('redirect_to_API_HOST') + + return redirect(next_page) + + return render_template('proxy_login.html', title='Sign In', form=loginform) + +@app.route('/logout') +def logout(): + logout_user() + return redirect(url_for('login_page')) + +if __name__ == "__main__": + try: + #check if authentification is required + if not rtkbaseconfig.get_web_authentification(): + app.config["LOGIN_DISABLED"] = True + app.secret_key = rtkbaseconfig.get_secret_key() + Flask.run(app, host = "::", port = rtkbaseconfig.get("main", "gnss_rcv_web_proxy_port", fallback=9090)) # IPv6 "::" is mapped to IPv4 + + except KeyboardInterrupt: + print("Server interrupted by user!!") From 159ac828645cc75ce33774175bfda221ca46aaca Mon Sep 17 00:00:00 2001 From: Stefal Date: Mon, 20 May 2024 18:34:37 +0000 Subject: [PATCH 18/24] rename reverse proxy file --- web_app/mosaic_rproxy_server.py | 150 -------------------------------- 1 file changed, 150 deletions(-) delete mode 100644 web_app/mosaic_rproxy_server.py diff --git a/web_app/mosaic_rproxy_server.py b/web_app/mosaic_rproxy_server.py deleted file mode 100644 index ddd24a64..00000000 --- a/web_app/mosaic_rproxy_server.py +++ /dev/null @@ -1,150 +0,0 @@ -#!/usr/bin/python - -# author: Stéphane Péneau -# source: https://github.com/Stefal/rtkbase - -# Reverse proxy server to acces the Mosaic integrated Web Server - -import eventlet -eventlet.monkey_patch() - -import os -import requests - -from RTKBaseConfigManager import RTKBaseConfigManager -from dotenv import load_dotenv # pip package python-dotenv - -from flask_bootstrap import Bootstrap4 -from flask import Flask, render_template, session, request, flash, url_for, Response -from flask import send_file, send_from_directory, redirect, abort -from flask import g -from flask_wtf import FlaskForm -from wtforms import PasswordField, BooleanField, SubmitField -from flask_login import LoginManager, login_user, logout_user, login_required, current_user, UserMixin -from wtforms.validators import ValidationError, DataRequired, EqualTo -from flask_socketio import SocketIO, emit, disconnect -import urllib - -from werkzeug.security import generate_password_hash -from werkzeug.security import check_password_hash -from werkzeug.utils import safe_join -import urllib - -app = Flask(__name__) -app.debug = True -app.config["SECRET_KEY"] = "secret!" -app.config["LOGIN_DISABLED"] = False - -rtkbase_path = os.path.abspath(os.path.join(os.path.dirname(__file__), "../")) - -login=LoginManager(app) -login.login_view = 'login_page' -socketio = SocketIO(app) -bootstrap = Bootstrap4(app) - -#Get settings from settings.conf.default and settings.conf -rtkbaseconfig = RTKBaseConfigManager(os.path.join(rtkbase_path, "settings.conf.default"), os.path.join(rtkbase_path, "settings.conf")) -GNSS_RCV_WEB_URL = str("{}{}".format("http://", rtkbaseconfig.get("main", "gnss_rcv_web_ip"))) - -class User(UserMixin): - """ Class for user authentification """ - def __init__(self, username): - self.id=username - self.password_hash = rtkbaseconfig.get("general", "web_password_hash") - - def check_password(self, password): - return check_password_hash(self.password_hash, password) - -class LoginForm(FlaskForm): - """ Class for the loginform""" - #username = StringField('Username', validators=[DataRequired()]) - password = PasswordField('Please enter the RTKBase password:', validators=[DataRequired()]) - remember_me = BooleanField('Remember Me') - submit = SubmitField('Sign In') - -@app.before_request -def inject_release(): - """ - Insert the RTKBase release number as a global variable for Flask/Jinja - """ - g.version = rtkbaseconfig.get("general", "version") - -@login.user_loader -def load_user(id): - return User(id) - -#proxy code from https://stackoverflow.com/a/36601467 -@app.route('/', defaults={'path': ''}, methods=["GET", "POST"]) # ref. https://medium.com/@zwork101/making-a-flask-proxy-server-online-in-10-lines-of-code-44b8721bca6 -@app.route('/', methods=["GET", "POST"]) # NOTE: better to specify which methods to be accepted. Otherwise, only GET will be accepted. Ref: -@login_required -def redirect_to_API_HOST(path): #NOTE var :path will be unused as all path we need will be read from :request ie from flask import request - res = requests.request( # ref. https://stackoverflow.com/a/36601467/248616 - method = request.method, - url = request.url.replace(request.host_url, f'{GNSS_RCV_WEB_URL}/'), - headers = {k:v for k,v in request.headers if k.lower() != 'host'}, # exclude 'host' header - data = request.get_data(), - cookies = request.cookies, - allow_redirects = False, - ) - """ print("method: ", request.method) - print("request posturl: ", request.url) - print("request host: ", request.host_url) - print("url: ", request.url.replace(request.host_url, f'{GNSS_RCV_WEB_URL}/')) - print("header: ", {k:v for k,v in request.headers if k.lower() != 'host'}) - print("data: ", request.get_data()) - print("cookies: ", request.cookies) - print("host url split", urllib.parse.urlsplit(request.host_url)) - print("host url split2", urllib.parse.urlsplit(request.base_url).hostname) - old = urllib.parse.urlparse(request.host_url) - new = urllib.parse.ParseResult(scheme=old.scheme, netloc="{}:{}".format(old.hostname, 9090), - path=old.path, params=old.params, query=old.query, fragment=old.fragment) - print("new_url: ", new.geturl()) """ - #region exlcude some keys in :res response - excluded_headers = ['content-encoding', 'content-length', 'transfer-encoding', 'connection'] #NOTE we here exclude all "hop-by-hop headers" defined by RFC 2616 section 13.5.1 ref. https://www.rfc-editor.org/rfc/rfc2616#section-13.5.1 - headers = [ - (k,v) for k,v in res.raw.headers.items() - if k.lower() not in excluded_headers - ] - #endregion exlcude some keys in :res response - - response = Response(res.content, res.status_code, headers) - print(response) - return response - - -@app.route('/login', methods=['GET', 'POST']) -def login_page(): - if current_user.is_authenticated: - return redirect(url_for('index')) - - loginform = LoginForm() - if loginform.validate_on_submit(): - user = User('admin') - password = loginform.password.data - if not user.check_password(password): - return abort(401) - - login_user(user, remember=loginform.remember_me.data) - next_page = request.args.get('redirect_to_API_HOST') - if not next_page or urllib.parse.urlsplit(next_page).netloc != '': - next_page = url_for('redirect_to_API_HOST') - - return redirect(next_page) - - return render_template('proxy_login.html', title='Sign In', form=loginform) - -@app.route('/logout') -def logout(): - logout_user() - return redirect(url_for('login_page')) - -if __name__ == "__main__": - try: - #check if authentification is required - if not rtkbaseconfig.get_web_authentification(): - app.config["LOGIN_DISABLED"] = True - app.secret_key = rtkbaseconfig.get_secret_key() - socketio.run(app, host = "::", port = rtkbaseconfig.get("main", "gnss_rcv_web_proxy_port", fallback=9090)) # IPv6 "::" is mapped to IPv4 - - except KeyboardInterrupt: - print("Server interrupted by user!!") From 3038dae11da3f9eebf178d3df1eca7787d77e060 Mon Sep 17 00:00:00 2001 From: Stefal Date: Wed, 22 May 2024 20:28:15 +0000 Subject: [PATCH 19/24] enable Mosaic reverse proxy service if this receiver is detected --- tools/install.sh | 1 + 1 file changed, 1 insertion(+) diff --git a/tools/install.sh b/tools/install.sh index 0cfecc69..66f18292 100755 --- a/tools/install.sh +++ b/tools/install.sh @@ -611,6 +611,7 @@ start_services() { systemctl restart gpsd.service systemctl restart chrony.service systemctl start rtkbase_archive.timer + grep -q "receiver='Septentrio_Mosaic-X5'" "${rtkbase_path}"/settings.conf && systemctl enable --now rtkbase_gnss_web_proxy.service echo '################################' echo 'END OF INSTALLATION' echo 'You can open your browser to http://'"$(hostname -I)" From 5f361f60e1836d7e662101cb7eb6a07ce4d8a3d9 Mon Sep 17 00:00:00 2001 From: Stefal Date: Sun, 26 May 2024 16:47:17 +0000 Subject: [PATCH 20/24] Fix requirements for modem_config.py --- tools/install.sh | 1 + tools/modem_config.py | 13 +++++++------ 2 files changed, 8 insertions(+), 6 deletions(-) diff --git a/tools/install.sh b/tools/install.sh index 66f18292..09559993 100755 --- a/tools/install.sh +++ b/tools/install.sh @@ -595,6 +595,7 @@ _add_modem_port(){ _configure_modem(){ sudo -u "${RTKBASE_USER}" "${rtkbase_path}/venv/bin/python" -m pip install nmcli --extra-index-url https://www.piwheels.org/simple + sudo -u "${RTKBASE_USER}" "${rtkbase_path}/venv/bin/python" -m pip install git+https://github.com/Stefal/sim-modem.git python3 "${rtkbase_path}"/tools/modem_config.py --config "${rtkbase_path}"/tools/lte_network_mgmt.sh --connection_rename --lte_priority } diff --git a/tools/modem_config.py b/tools/modem_config.py index c695b38b..46fb6853 100755 --- a/tools/modem_config.py +++ b/tools/modem_config.py @@ -3,13 +3,14 @@ import sys import os import argparse -if os.getenv("SUDO_USER") is not None: - homedir = os.path.join("/home", os.getenv("SUDO_USER")) -else: - homedir = os.path.expanduser('~') +#if os.getenv("SUDO_USER") is not None: +# homedir = os.path.join("/home", os.getenv("SUDO_USER")) +#else: +# homedir = os.path.expanduser('~') -sys.path.insert(1, os.path.join(homedir, "sim-modem")) -from src.sim_modem import * +#sys.path.insert(1, os.path.join(homedir, "sim-modem")) +#from src.sim_modem import * +from sim_modem import * def arg_parse(): parser=argparse.ArgumentParser( From e36049d328e1174d8bd841d6dbef507e53e30ec1 Mon Sep 17 00:00:00 2001 From: Stefal Date: Tue, 28 May 2024 16:48:39 +0200 Subject: [PATCH 21/24] remove debug args --- run_cast.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/run_cast.sh b/run_cast.sh index 26079b74..9b0834a4 100755 --- a/run_cast.sh +++ b/run_cast.sh @@ -1,4 +1,4 @@ -#!/bin/bash -xv +#!/bin/bash # # run_cast.sh: script to run NTRIP caster by STR2STR # You can read the RTKLIB manual for more str2str informations: From d202f34af96eca4b865c8ae443860d8154ea4151 Mon Sep 17 00:00:00 2001 From: Stefal Date: Tue, 28 May 2024 16:52:37 +0200 Subject: [PATCH 22/24] start modem-config from the venv --- tools/install.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tools/install.sh b/tools/install.sh index 09559993..bda6b28b 100755 --- a/tools/install.sh +++ b/tools/install.sh @@ -596,7 +596,7 @@ _add_modem_port(){ _configure_modem(){ sudo -u "${RTKBASE_USER}" "${rtkbase_path}/venv/bin/python" -m pip install nmcli --extra-index-url https://www.piwheels.org/simple sudo -u "${RTKBASE_USER}" "${rtkbase_path}/venv/bin/python" -m pip install git+https://github.com/Stefal/sim-modem.git - python3 "${rtkbase_path}"/tools/modem_config.py --config + sudo -u "${RTKBASE_USER}" "${rtkbase_path}/venv/bin/python" "${rtkbase_path}"/tools/modem_config.py --config "${rtkbase_path}"/tools/lte_network_mgmt.sh --connection_rename --lte_priority } From 9c5fdd9d04bc5f86255dd8affc3152974db6ce84 Mon Sep 17 00:00:00 2001 From: Stefal Date: Tue, 28 May 2024 17:00:59 +0200 Subject: [PATCH 23/24] moving Cellular requirements installation step --- tools/install.sh | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/tools/install.sh b/tools/install.sh index bda6b28b..17a84f16 100755 --- a/tools/install.sh +++ b/tools/install.sh @@ -366,6 +366,11 @@ rtkbase_requirements(){ sudo -u "${RTKBASE_USER}" "${python_venv}" -m pip install -r "${rtkbase_path}"/web_app/requirements.txt --extra-index-url https://www.piwheels.org/simple #when we will be able to launch the web server without root, we will use #sudo -u $(logname) python3 -m pip install -r requirements.txt --user. + + #Installing requirements for Cellular modem. Installing them during the Armbian firstrun doesn't work because the network isn't fully up. + sudo -u "${RTKBASE_USER}" "${rtkbase_path}/venv/bin/python" -m pip install nmcli --extra-index-url https://www.piwheels.org/simple + sudo -u "${RTKBASE_USER}" "${rtkbase_path}/venv/bin/python" -m pip install git+https://github.com/Stefal/sim-modem.git + } install_unit_files() { @@ -594,8 +599,6 @@ _add_modem_port(){ } _configure_modem(){ - sudo -u "${RTKBASE_USER}" "${rtkbase_path}/venv/bin/python" -m pip install nmcli --extra-index-url https://www.piwheels.org/simple - sudo -u "${RTKBASE_USER}" "${rtkbase_path}/venv/bin/python" -m pip install git+https://github.com/Stefal/sim-modem.git sudo -u "${RTKBASE_USER}" "${rtkbase_path}/venv/bin/python" "${rtkbase_path}"/tools/modem_config.py --config "${rtkbase_path}"/tools/lte_network_mgmt.sh --connection_rename --lte_priority } From 012a88b2d205d68d3b324c4af9de294e2d6121c8 Mon Sep 17 00:00:00 2001 From: Stefal Date: Tue, 28 May 2024 22:02:35 +0200 Subject: [PATCH 24/24] update changelog --- CHANGELOG.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 661364fa..b913a7e6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,4 +1,10 @@ # Changelog + +## [] - not released +### Added + - Septentrio Mosaic-X5 detection and configuration + - Reverse proxy server with Rtkbase authentication, for Mosaic-X5 web interface + ## [2.5.0] - 2024-01-30 ### Added - udev rules to create ttyGNSS port for usb connected F9P.