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
-
Not available for legacy products NB1600, NB2700, NB3700, NB3710
The wiki was tested with router software 5.0.0.104
1. Set up the LXC Contianer
Activate the LXC container on the router
In the second step, the image of the LXC container in this case, a Debina 13 (trixie).
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 / Link
Here you can download a Debian image for various systems and versions.
The armhf tab is important here, and trixie
The image is the “rootfs.tar.xz” which is required. With a right click you can copy the link.
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.
The container has now also been assigned an IP address.
USER
~ $ 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.
Log in to the LXC container and test whether it can access the Internet.
USER
~ $ 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.
USER
~ $ 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
2. Make additional settings in the container
USER
~ $ 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.
USER
~ $ 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.
To configure the frame interval, you'll need Python programs and a library.
Create a subfolder for the whole thing
USER
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
- set_cfg_rate_neom8l.py
#!/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]} <frame interval in Hz> (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 / 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
- ssh_start.py
#!/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 <rate_hz>
"""
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]} <rate_hz> (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.
USER
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.
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.
USER
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.
USER
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#
Download the set_cfg_rate_neom8l.py python script:
show code
- set_cfg_rate_neom8l.py
#!/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]} <frame interval in Hz> (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
- ssh_start.py
#!/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 <rate_hz>
"""
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]} <rate_hz> (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 / Link
The password for downloading the library : gnss_cfg_rate
Download the get_cfg_rate_neom8l.py python script:
show code
- get_cfg_rate_neom8l.py
#!/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()