====== Set the frame interval for the NEO-M8L ====== ===== Goal ===== Using a Python script, it should be possible to change the position update interval of a NEO-M8L GNSS module from 1 Hz to 10 Hz. This allows the location to be updated not just once per second, but up to 10 times per second. ===== Prerequisites ===== * NetModule Wireless Router with wireless connection * NetModule Software [[https://share.netmodule.com/public/system-software/5.0/5.0.0.104/|5.0.0.104]] or newer * Not available for legacy products NB1600, NB2700, NB3700, NB3710 ** The wiki was tested with router software 5.0.0.104 ** ---- ===== - Set up the LXC Contianer ===== Activate the LXC container on the router {{:pictures:lxc_aktiv.png?800|}} In the second step, the image of the LXC container in this case, a Debina 13 (trixie). {{:pictures:lxc_install.png?800|}} The easiest way to do this is to install the whole thing over the Internet. However, the router requires a mobile connection to the Internet. From a Jenkins server you get the current image for the container * Jenkins Server Page image-debian / [[https://jenkins.linuxcontainers.org/view/Images/job/image-debian/|Link]] Here you can download a Debian image for various systems and versions. The ''armhf'' tab is important here, and ''trixie'' {{:app-notes:ki_lxc_trixie.png?800|}} The image is the ''“rootfs.tar.xz”'' which is required. With a right click you can copy the link. {{:app-notes:ki_trixie_image.png?800|}} It would be structured like this: https://jenkins.linuxcontainers.org/view/Images/job/image-debian/architecture=armhf,release=trixie,variant=default/4118/artifact/rootfs.tar.xz When the ''“Apply”'' button is pressed, Debian will be installed. Set up the LXC container further. A network interface is enabled here. {{:pictures:lxc_netzwerk.png?800|}} The container has now also been assigned an IP address. ~ $ lxc-info guest0 Name: guest0 State: RUNNING PID: 2255 IP: 192.168.1.149 CPU use: 1.57 seconds BlkIO use: 4.00 KiB Memory use: 6.29 MiB KMem use: 1.18 MiB Link: virt0 TX bytes: 2.28 KiB RX bytes: 853 bytes Total bytes: 3.11 KiB ~ $ Always assign a fixed IP address for the LXC container via the DHCP server. The MAC address of the LXC container is ''00:FF:AA:00:44:00''. {{:pictures:lxc_ip_adresse.png?800|}} Log in to the LXC container and test whether it can access the Internet. ~ $ lxc-attach guest0 root@LXCNAME:~# ping quad9.net PING quad9.net (195.176.255.138) 56(84) bytes of data. 64 bytes from 195.176.255.138 (195.176.255.138): icmp_seq=1 ttl=51 time=37.0 ms 64 bytes from 195.176.255.138 (195.176.255.138): icmp_seq=2 ttl=51 time=40.9 ms --- quad9.net ping statistics --- 2 packets transmitted, 2 received, 0% packet loss, time 3006ms rtt min/avg/max/mdev = 28.823/36.685/40.937/4.768 ms root@LXCNAME:~# The container is installed and set up. Now you can set up further tools on the container. ---- ==== Customize the LXC configuration ==== Adjust the LXC configuration so that the ''/dev/ttyUSB1'' interface is available within the container. ~ $ ls -l /dev/tty* crw-rw-rw- 1 root root 5, 0 Sep 30 09:17 /dev/tty crw------- 1 root root 166, 0 Sep 28 17:55 /dev/ttyACM0 crw------- 1 root root 166, 1 Sep 28 17:55 /dev/ttyACM1 crw------- 1 root root 166, 2 Sep 28 17:55 /dev/ttyACM2 crw------- 1 root root 166, 3 Sep 28 17:55 /dev/ttyACM3 crw------- 1 root root 249, 0 Sep 28 17:55 /dev/ttyNull0 crw------- 1 root root 4, 64 Sep 28 17:55 /dev/ttyS0 crw------- 1 root root 4, 65 Sep 28 17:55 /dev/ttyS1 ''crw------- 1 root root 188, 1 Sep 29 17:26 /dev/ttyUSB1'' crw------- 1 root root 188, 2 Sep 29 17:32 /dev/ttyUSB2 crw------- 1 root root 188, 3 Sep 29 17:32 /dev/ttyUSB3 crw------- 1 root root 188, 4 Sep 29 17:32 /dev/ttyUSB4 crw------- 1 root root 188, 5 Sep 29 17:32 /dev/ttyUSB5 crw------- 1 root root 188, 6 Sep 29 17:32 /dev/ttyUSB6 ~ $ The entry ''/dev/ttyUSB1'' is important; this interface gives us access to the GNSS module. ** This change is made directly in the router software. When updating the router software, this entry must be made again because it is not part of the official software. ** Add these entries to the end of the ''config'' file. ~ $ vi /etc/lxc/guest0/config # Setting up a USB drive in the LXC container lxc.cgroup.devices.allow = c 188:1 rwm lxc.mount.entry = /dev/ttyUSB1 dev/ttyUSB1 none bind,optional,create=file 0 0 ---- ===== - Make additional settings in the container ===== ~ $ lxc-attach guest0 root@LXCNAME:~# apt install gpsd python3-gps gpsd-clients python3-paramiko nano root@LXCNAME:~# The next step is to perform a basic test of access to the GNSS module. ~ $ lxc-attach guest0 root@LXCNAME:~# cgps -s 192.168.1.1:2947 root@LXCNAME:~# cgps: WARNING gpsd server release 3.23.1, expected 3.25, API: 3.14 root@LXCNAME:~# The GNSS data is then clearly displayed in the console using a Python plotting tool. {{:app-notes:gnssfix_cgps.png?800|}} To configure the frame interval, you'll need Python programs and a library. Create a subfolder for the whole thing root@LXCNAME:~# mkdir ubx_neo_m8l root@LXCNAME:~# cd ubx_neo_m8l root@LXCNAME:~# ls -l root@LXCNAME:~/ubx_neo_m8l/# ls -l -rwxr-xr-x 1 root root 1055 Sep 29 13:37 set_cfg_rate_neom8l.py -rwxr-xr-x 1 root root 2642 Sep 29 15:21 ssh_start.py drw-r--r-- 3 root root 4096 Sep 29 13:33 ubxlib root@LXCNAME:~/ubx_neo_m8l/ The script ''set_cfg_rate_neom8l.py'' can be downloaded via the ''View Code'' link, and the ubxlib can be downloaded from the ''link'' using the password. ++++show code| #!/usr/bin/python3 import sys import logging from ubxlib.server_tty import GnssUBlox from ubxlib.ubx_cfg_rate import UbxCfgRate from ubxlib.ubx_cfg_cfg import UbxCfgCfgAction FORMAT = '%(asctime)-15s %(levelname)-8s %(message)s' logging.basicConfig(format=FORMAT) logger = logging.getLogger('ubxlib') logger.setLevel(logging.INFO) if len(sys.argv) != 2: print(f"Usage: {sys.argv[0]} (allowed: 1-10)") sys.exit(1) try: rate_hz = int(sys.argv[1]) if not (1 <= rate_hz <= 10): raise ValueError except ValueError: print("Error: frame interval must be an integer between 1 and 10") sys.exit(1) ubx = GnssUBlox('/dev/ttyUSB1') ubx.setup() msg = UbxCfgRate() msg.set_rate_in_hz(rate_hz) msg.f.timeRef = 0 ack_nak = ubx.set(msg) print(f"CFG-RATE ({rate_hz} Hz) result:", ack_nak) # Save permanently save_msg = UbxCfgCfgAction() save_msg.save(UbxCfgCfgAction.MASK_All) # includes all categories, inc RxmConf ack_nak_save = ubx.set(save_msg) print("CFG-CFG (save) result:", ack_nak_save) ubx.cleanup() ++++ * Program library ''ubxlib'' for the Python script / [[https://files.netmodule.com/share/JoIjsXPj|Link]] The password for downloading the library : ''gnss_cfg_rate'' The ''ssh_start.py'' script can be downloaded via the “show code” link. ++++show code| #!/usr/bin/python3 """ Establish an SSH connection (key-based) to the router, briefly disable gpsd (wwanmd toggles the port from down to up, leaving it briefly free), and within that window call set_cfg_rate_neom8l.py with the desired rate_hz value, and then re-enable gpsd. Calling the subroutine: python3 run_set_rate.py """ import paramiko import subprocess import time import sys HOST = "192.168.1.1" # The router's IP address as seen from the container PORT = 22 USER = "root" SSH_KEY_PATH = "/root/.ssh/id_rsa" # Path to the private SSH key RETRY_COUNT = 2 # attempts RETRY_DELAY = 0.5 # Seconds between attempts def run_remote2command(client, cmd): stdin, stdout, stderr = client.exec_command(cmd) out = stdout.read().decode().strip() err = stderr.read().decode().strip() status = stdout.channel.recv_exit_status() return status, out, err def main(): if len(sys.argv) != 2: print(f"Usage: {sys.argv[0]} (allowed: 1-10)") sys.exit(1) try: rate_hz = int(sys.argv[1]) if not (1 <= rate_hz <= 10): raise ValueError except ValueError: print("Error: rate_hz must be an integer between 1 and 10.") sys.exit(1) # Establish an SSH Connection to the Router ''Key-Based'' key = paramiko.RSAKey.from_private_key_file(SSH_KEY_PATH) client = paramiko.SSHClient() client.set_missing_host_key_policy(paramiko.AutoAddPolicy()) client.connect(HOST, port=PORT, username=USER, pkey=key, timeout=10) try: # Disable gpsd -> wwanmd toggles the port; it is open for a short time print("Set gpsd.1.status=0 ...") run_remote_command(client, 'cli set -s gpsd.1.status="0"') success = False for attempt in range(1, RETRY_COUNT + 1): print(f"Experiment {attempt}/{RETRY_COUNT}: call set_cfg_rate_neom8l.py on...") result = subprocess.run( ["python3", "set_cfg_rate_neom8l.py", str(rate_hz)], capture_output=True, text=True, timeout=10 ) print("STDOUT:", result.stdout) if result.stderr: print("STDERR:", result.stderr) if result.returncode == 0 and "SerialException" not in result.stderr: success = True break time.sleep(RETRY_DELAY) if not success: print("Note: set_cfg_rate_neom8l.py failed to run successfully in all attempts.") finally: # Turn gpsd back on print("Set gpsd.0.status=1 ...") run_remote_command(client, 'cli set -s gpsd.1.status="1"') client.close() if __name__ == "__main__": main() ++++ To enable SSH access, an SSH key pair must be generated within the container. root@LXCNAME:~# ssh-keygen -t rsa -b 2048 -f /root/.ssh/id_rsa -N "" root@LXCNAME:~# cat /root/.ssh/id_rsa.pub root@LXCNAME:~# ssh-rsa AAAAB3NzaC1yc2EAAAAD- ... root@LXCNAME root@LXCNAME:~# The file ''id_rsa.pub'' must then be added to the router under ''SSH Authorization''. {{:app-notes:gnssfix_key.png?800|}} Once all of this has been completed, you can run the Python script ''ssh_start.py'', and the frame interval will be set in the GNSS module. python3 ssh_start.py 10 # The Python script is called this way. # Values from 1 to 10 can be passed as arguments. root@LXCNAME:~/ubx_neo_m8l# python3 ssh_start.py 10 Setze gpsd.1.status=0 ... Versuch 1/2: call set_cfg_rate_neom8l.py on... STDOUT: STDERR: 2026-09-30 10:39:20,427 INFO instantiating GnssUBlox on tty 2026-09-30 10:39:20,428 INFO using device /dev/ttyUSB1 Traceback (most recent call last): serial.serialutil.SerialException: device reports readiness to read but returned no data (device disconnected or multiple access on port?) root@LXCNAME:~/ubx_neo_m8l# Then test the whole thing with a get script. root@LXCNAME:~/ubx_neo_m8l# python3 get_cfg_rate_neom8l.py 2026-09-30 11:53:44,917 INFO instantiating GnssUBlox on tty 2026-09-30 11:53:44,917 INFO using device /dev/ttyUSB1 UBX-CFG-RATE cls:06 id:08 measRate: 100 navRate: 1 timeRef: 0 root@LXCNAME:~/ubx_neo_m8l# ---- ===== - Additional information ===== Download the ''set_cfg_rate_neom8l.py'' python script: ++++show code| #!/usr/bin/python3 import sys import logging from ubxlib.server_tty import GnssUBlox from ubxlib.ubx_cfg_rate import UbxCfgRate from ubxlib.ubx_cfg_cfg import UbxCfgCfgAction FORMAT = '%(asctime)-15s %(levelname)-8s %(message)s' logging.basicConfig(format=FORMAT) logger = logging.getLogger('ubxlib') logger.setLevel(logging.INFO) if len(sys.argv) != 2: print(f"Usage: {sys.argv[0]} (allowed: 1-10)") sys.exit(1) try: rate_hz = int(sys.argv[1]) if not (1 <= rate_hz <= 10): raise ValueErro except ValueError: print("Error: frame interval must be an integer between 1 and 10") sys.exit(1) ubx = GnssUBlox('/dev/ttyUSB1') ubx.setup() msg = UbxCfgRate() msg.set_rate_in_hz(rate_hz) msg.f.timeRef = 0 ack_nak = ubx.set(msg) print(f"CFG-RATE ({rate_hz} Hz) result:", ack_nak) # Save permanently save_msg = UbxCfgCfgAction() save_msg.save(UbxCfgCfgAction.MASK_All) # includes all categories, inc RxmConf ack_nak_save = ubx.set(save_msg) print("CFG-CFG (save) result:", ack_nak_save) ubx.cleanup() ++++ ---- Download the ''ssh_start.py'' python script: ++++show code| #!/usr/bin/python3 """ Establish an SSH connection (key-based) to the router, briefly disable gpsd (wwanmd toggles the port from down to up, leaving it briefly free), and within that window call set_cfg_rate_neom8l.py with the desired rate_hz value, and then re-enable gpsd. Calling the subroutine: python3 run_set_rate.py """ import paramiko import subprocess import time import sys HOST = "192.168.1.1" # The router's IP address as seen from the container PORT = 22 USER = "root" SSH_KEY_PATH = "/root/.ssh/id_rsa" # Path to the private SSH key RETRY_COUNT = 2 # attempts RETRY_DELAY = 0.5 # Seconds between attempts def run_remote2command(client, cmd): stdin, stdout, stderr = client.exec_command(cmd) out = stdout.read().decode().strip() err = stderr.read().decode().strip() status = stdout.channel.recv_exit_status() return status, out, err def main(): if len(sys.argv) != 2: print(f"Usage: {sys.argv[0]} (allowed: 1-10)") sys.exit(1) try: rate_hz = int(sys.argv[1]) if not (1 <= rate_hz <= 10): raise ValueError except ValueError: print("Error: rate_hz must be an integer between 1 and 10.") sys.exit(1) # Establish an SSH Connection to the Router ''Key-Based'' key = paramiko.RSAKey.from_private_key_file(SSH_KEY_PATH) client = paramiko.SSHClient() client.set_missing_host_key_policy(paramiko.AutoAddPolicy()) client.connect(HOST, port=PORT, username=USER, pkey=key, timeout=10) try: # Disable gpsd -> wwanmd toggles the port; it is open for a short time print("Set gpsd.1.status=0 ...") run_remote_command(client, 'cli set -s gpsd.1.status="0"') success = False for attempt in range(1, RETRY_COUNT + 1): print(f"Experiment {attempt}/{RETRY_COUNT}: call set_cfg_rate_neom8l.py on...") result = subprocess.run( ["python3", "set_cfg_rate_neom8l.py", str(rate_hz)], capture_output=True, text=True, timeout=10 ) print("STDOUT:", result.stdout) if result.stderr: print("STDERR:", result.stderr) if result.returncode == 0 and "SerialException" not in result.stderr: success = True break time.sleep(RETRY_DELAY) if not success: print("Note: set_cfg_rate_neom8l.py failed to run successfully in all attempts.") finally: # Turn gpsd back on print("Set gpsd.0.status=1 ...") run_remote_command(client, 'cli set -s gpsd.1.status="1"') client.close() if __name__ == "__main__": main() ++++ ---- Download the library ''ubxlib'': Program library ''ubxlib'' for the Python script / [[https://files.netmodule.com/share/JoIjsXPj|Link]] The password for downloading the library : ''gnss_cfg_rate'' ---- Download the ''get_cfg_rate_neom8l.py'' python script: ++++show code| #!/usr/bin/python3 """ Retrieves the current UBX-CFG-RATE configuration (navigation rate) from the module """ import logging from ubxlib.server_tty import GnssUBlox from ubxlib.ubx_cfg_rate import UbxCfgRatePoll FORMAT = '%(asctime)-15s %(levelname)-8s %(message)s' logging.basicConfig(format=FORMAT) logger = logging.getLogger('ubxlib') logger.setLevel(logging.INFO) ubx = GnssUBlox('/dev/ttyUSB1') ubx.setup() poll_msg = UbxCfgRatePoll() response = ubx.poll(poll_msg) print(response) ubx.cleanup() ++++ ----