OS Configuration
Step-by-step guide to manually configure a Linux based OS for the moduliser application. All commands are run as root unless stated otherwise.
Step-by-step guide to manually configure a Linux based OS for the moduliser application. All commands are run as root unless stated otherwise.
Prerequisites
- Linux Base OS - arm64 (Debian or Ubuntu)
- NetworkManager installed and active
- Internet connectivity
- A GitHub personal access token with
read:packagesscope (generate at https://github.com/settings/tokens))
The documentation assumes that the user is sysadmin. You can change it, but be careful to update each command when needed.
1. Install System Packages
sudo apt update
sudo apt install --no-install-recommends -y \
build-essential \
python3 \
curl \
gnupg \
modemmanager \
libqmi-utils \
libmbim-utils \
usb-modeswitch \
usb-modeswitch-data \
chrony \
tcpdump \
libcap2-bin \
git \
gh \
nano \
iputils-ping \
unzip \
libcurl4-openssl-dev \
gpsd \
gpsd-clients2. Install Node.js 24
sudo mkdir -p /etc/apt/keyrings
curl -fsSL https://deb.nodesource.com/gpgkey/nodesource-repo.gpg.key \
| sudo gpg --dearmor -o /etc/apt/keyrings/nodesource.gpg
echo "deb [signed-by=/etc/apt/keyrings/nodesource.gpg] https://deb.nodesource.com/node_24.x nodistro main" \
> /etc/apt/sources.list.d/nodesource.list
sudo apt update
sudo apt install -y nodejs3. Install PNPM
curl -fsSL https://get.pnpm.io/install.sh | sh -4. Install correct Iperf3 version
-
Removing the old version: sudo apt remove iperf3 libiperf0
-
Install the dependency: sudo apt install libsctp1
-
Take a recent Ubuntu distribution from https://launchpad.net/ubuntu/+source/iperf3
-
Download iperf3_3.xx-1_amd64.deb and libiperf0_3.xx-1_amd64.deb packages (use amd64 version for a standard version of Ubuntu)
-
Install downloaded packages:
sudo dpkg -i libiperf0_3.xx-1_amd64.deb iperf3_3.xx-1_amd64.deb
-
Remove downloaded packages that are now unnecessary: rm libiperf0_3.xx-1_amd64.deb iperf3_3.xx-1_amd64.deb
Example
https://launchpad.net/ubuntu/+archive/primary/+files/iperf3_3.18-2ubuntu0.1_amd64.deb
https://launchpad.net/ubuntu/+archive/primary/+files/libiperf0_3.18-2ubuntu0.1_amd64.deb
5. Install Moduliser CLI
Run the following commands as the sysadmin user (not root):
# Create npm global directory
mkdir -p ~/.npm-global
# Configure npm for GitHub registry
# Warning token will expires on Wed, Jun 10 2026.
cat > ~/.npmrc << 'EOF'
@omirion-labs:registry=https://npm.pkg.github.com
//npm.pkg.github.com/:_authToken=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
prefix=/home/sysadmin/.npm-global
EOF
chmod 600 ~/.npmrc
# Add npm-global to PATH
echo 'export PATH="/home/sysadmin/.npm-global/bin:$PATH"' >> ~/.bashrc
source ~/.bashrc
# Install the package
npm install -g @omirion-labs/moduliser-cliBack as root, create a system-wide symlink:
ln -sf /home/sysadmin/.npm-global/bin/moduliser /usr/local/bin/moduliser6. Enable ModemManager
systemctl enable ModemManager
systemctl start ModemManager7. Configure Quectel Modem USB Port Reservation
This udev rule creates stable device symlinks based on USB interface numbers, independent of ttyUSBx numbering which can change across reboots or modem re-enumeration. It also reserves two ports from ModemManager:
- IF 01 (
/dev/quectel-gps) forgpsddirect NMEA access - IF 03 (
/dev/quectel-at-aux) for direct application AT access (CQI, diagnostics, GPS configuration commands)
Use ID_MM_PORT_IGNORE to reserve a single port. Do not use
ID_MM_DEVICE_IGNORE — that flag tells ModemManager to ignore the entire
modem and conflicts with MM's own per-port ID_MM_CANDIDATE=1 rule (from
/lib/udev/rules.d/77-mm-quectel-port-types.rules), so the port stays managed
by MM despite the rule.
cat > /etc/udev/rules.d/78-mm-quectel-reserve-port.rules << 'EOF'
# Quectel RM520N-GL udev rules (Vendor ID: 2c7c)
# Creates stable symlinks based on USB interface number, independent of ttyUSB numbering.
#
# Interface mapping:
# IF 00 (1.0) -> /dev/quectel-diag (DM/diagnostic, ignored)
# IF 01 (1.1) -> /dev/quectel-gps (GPS/NMEA output, reserved for gpsd)
# IF 02 (1.2) -> /dev/quectel-at (AT command port, used by ModemManager)
# IF 03 (1.3) -> /dev/quectel-at-aux (AT auxiliary, reserved for application use)
# Create stable symlinks for each interface
ACTION=="add|change", SUBSYSTEM=="tty", ATTRS{idVendor}=="2c7c", ENV{ID_USB_INTERFACE_NUM}=="00", SYMLINK+="quectel-diag"
ACTION=="add|change", SUBSYSTEM=="tty", ATTRS{idVendor}=="2c7c", ENV{ID_USB_INTERFACE_NUM}=="01", SYMLINK+="quectel-gps"
ACTION=="add|change", SUBSYSTEM=="tty", ATTRS{idVendor}=="2c7c", ENV{ID_USB_INTERFACE_NUM}=="02", SYMLINK+="quectel-at"
ACTION=="add|change", SUBSYSTEM=="tty", ATTRS{idVendor}=="2c7c", ENV{ID_USB_INTERFACE_NUM}=="03", SYMLINK+="quectel-at-aux"
# Reserve GPS port (IF 01) from ModemManager for gpsd direct access
ACTION=="add|change", SUBSYSTEM=="tty", ATTRS{idVendor}=="2c7c", ENV{ID_USB_INTERFACE_NUM}=="01", ENV{ID_MM_PORT_IGNORE}="1"
# Reserve auxiliary AT port (IF 03) from ModemManager for application access
ACTION=="add|change", SUBSYSTEM=="tty", ATTRS{idVendor}=="2c7c", ENV{ID_USB_INTERFACE_NUM}=="03", ENV{ID_MM_PORT_IGNORE}="1"
EOF
udevadm control --reload-rules
udevadm trigger
systemctl restart ModemManagerVerify:
ls -la /dev/quectel-*
# Expected:
# /dev/quectel-at -> ttyUSBx
# /dev/quectel-at-aux -> ttyUSBx
# /dev/quectel-diag -> ttyUSBx
# /dev/quectel-gps -> ttyUSBx
udevadm info /dev/quectel-at-aux | grep ID_MM
# Expected: ID_MM_PORT_IGNORE=1
# Confirm ModemManager is no longer holding the reserved ports
lsof /dev/quectel-gps /dev/quectel-at-aux 2>/dev/null
# Expected: (empty)8. Configure gpsd (GPS Daemon)
Moduliser uses gpsd for real-time GPS location streaming. gpsd connects to the modem's NMEA port via the stable udev symlink /dev/quectel-gps and provides parsed GPS data via a TCP JSON protocol on localhost:2947.
Configure gpsd device
cat > /etc/default/gpsd << 'EOF'
# GPS device (Quectel modem NMEA output port via stable udev symlink)
DEVICES="/dev/quectel-gps"
# gpsd options:
# -n don't wait for client connection to poll GPS
# -b read-only mode, don't toggle DTR/RTS (prevents Quectel modem from stopping NMEA output)
GPSD_OPTIONS="-n -b"
# Automatically hot add/remove USB GPS devices via gpsdctl
USBAUTO="true"
EOFEnable and start gpsd
systemctl enable gpsd.socket
systemctl start gpsd.socketgpsd uses socket activation — the daemon starts automatically when a client connects to port 2947. You do not need to enable gpsd.service directly.
The -b flag is critical for Quectel modems: without it, gpsd toggles DTR/RTS serial control lines when opening the port, which causes the modem to stop outputting NMEA sentences.
Verify gpsd is working
# Check socket is listening
systemctl is-active gpsd.socket
# Expected: active
# Test GPS data stream (requires modem with GPS enabled)
# Press Ctrl+C to stop
cgps9. Configure ModemManager D-Bus Policy
Allow all users to call the GetCellInfo method on ModemManager:
cat > /etc/dbus-1/system.d/modemmanager-getcellinfo.conf << 'EOF'
<!DOCTYPE busconfig PUBLIC
"-//freedesktop//DTD D-BUS Bus Configuration 1.0//EN"
"http://www.freedesktop.org/standards/dbus/1.0/busconfig.dtd">
<busconfig>
<!-- Allow all users to call GetCellInfo method -->
<policy context="default">
<allow send_destination="org.freedesktop.ModemManager1"
send_interface="org.freedesktop.ModemManager1.Modem"
send_member="GetCellInfo"/>
</policy>
</busconfig>
EOF10. Configure ModemManager Polkit Rules
Grant automatic authorization for all ModemManager D-Bus actions to the dialout group and the sysadmin user. Without this, modem operations (mmcli commands) fail with permission errors on headless systems.
cat > /etc/polkit-1/rules.d/50-modemmanager.rules << 'EOF'
polkit.addRule(function(action, subject) {
if (action.id.indexOf("org.freedesktop.ModemManager1") == 0) {
if (subject.isInGroup("dialout") || subject.user == "sysadmin") {
return polkit.Result.YES;
}
}
});
EOF11. Configure QMI Sudoers Rule
Allow users in the dialout group to run qmicli without a password. Required for QMI-based cell information retrieval on Qualcomm modems.
cat > /etc/sudoers.d/qmicli << 'EOF'
# Allows users in the dialout group to run qmicli without entering a password
# Required for QMI-based cell information retrieval on Qualcomm modems
%dialout ALL=(ALL) NOPASSWD: /usr/bin/qmicli
EOF
chmod 440 /etc/sudoers.d/qmicli
# Validate syntax
visudo -c -f /etc/sudoers.d/qmicli12. Configure Chronyc Sudoers Rule
Allow the sysadmin user to run chronyc without a password. Required for moduliser-cli NTP server configuration (add server, makestep, delete).
cat > /etc/sudoers.d/chronyc << 'EOF'
# Sudoers rule for passwordless chronyc access
# Required for moduliser-cli NTP server configuration
sysadmin ALL=(ALL) NOPASSWD: /usr/bin/chronyc
EOF
chmod 440 /etc/sudoers.d/chronyc
# Validate syntax
visudo -c -f /etc/sudoers.d/chronyc13. Set User Permissions
Add sysadmin to the dialout group for serial/modem device access:
usermod -aG dialout sysadmin14. Set Linux Capabilities
Allow unprivileged packet capture, ICMP ping, and interface-bound sockets:
# tcpdump: packet capture without sudo
setcap cap_net_raw,cap_net_admin=eip /usr/bin/tcpdump
# ping: ICMP without sudo
setcap cap_net_raw=ep /usr/bin/ping
# Node.js: SO_BINDTODEVICE for VNM test interface binding
# Required by @omirion-labs/sockutil to force UDP traffic through
# the selected network interface (e.g., wwan0 for cellular modem).
# Without this, the VNM test cannot bind sockets to specific interfaces
# and will fail with "setsockopt SO_BINDTODEVICE failed".
# See: https://github.com/nodejs/node/issues/35769
setcap cap_net_raw=ep /usr/bin/nodeThe Node.js capability must be re-applied after updating Node.js (e.g., apt upgrade nodejs), as the new binary won't inherit the capability.
Verify:
getcap /usr/bin/tcpdump
# Expected: /usr/bin/tcpdump cap_net_admin,cap_net_raw=eip
getcap /usr/bin/ping
# Expected: /usr/bin/ping cap_net_raw=ep
getcap /usr/bin/node
# Expected: /usr/bin/node cap_net_raw=ep15. Create Log Directories
mkdir -p /var/log/moduliser/logs
mkdir -p /var/log/moduliser/captures
chown -R sysadmin:sysadmin /var/log/moduliser16. Configure GSM Auto-Connect
Create a NetworkManager connection profile for the cellular modem. Replace YOUR_APN with your carrier's APN.
UUID=$(uuidgen)
cat > /etc/NetworkManager/system-connections/modem-under-test.nmconnection << EOF
[connection]
id=modem-under-test
uuid=$UUID
type=gsm
autoconnect=true
autoconnect-priority=100
[gsm]
auto-config=no
apn=YOUR_APN
[ipv4]
dns-priority=-100
method=auto
route-metric=50
[ipv6]
addr-gen-mode=default
dns-priority=-100
method=auto
route-metric=50
EOF
chmod 600 /etc/NetworkManager/system-connections/modem-under-test.nmconnectionIf your carrier requires authentication or a SIM PIN, add these lines under the [gsm] section:
username=YOUR_USERNAME
password=YOUR_PASSWORD
pin=YOUR_PINCommon French carrier APNs:
| Carrier | APN |
|---|---|
| Orange | orange |
| SFR | sl2sfr |
| Bouygues | mmsbouygtel.com |
| Free | free |
Reload NetworkManager to apply:
nmcli connection reload17. Install QLog (Quectel Modem Logging)
QLog captures diagnostic logs from the Quectel modem DIAG interface. The pre-built binary is x86-64, so it must be built from source on arm64.
The source archive QLog_Linux_Android_V1.5.18.zip can found at https://clients.omirion.com/api/files/dec5dab6-8783-492c-88de-d5cbeca2e5b9/download
mkdir -p /tmp/qlog_build
cp QLog_Linux_Android_V1.5.18.zip /tmp/qlog_build/
cd /tmp/qlog_build
unzip -o -q QLog_Linux_Android_V1.5.18.zip
cd QLog_Linux_Android_V1.5.18
make linux
cp QLog /usr/local/bin/qlog
chmod 755 /usr/local/bin/qlog
mkdir -p /etc/qlog
cp conf/default.cfg /etc/qlog/default.cfg
# Clean up
rm -rf /tmp/qlog_build18. Install ProbeAgent (Qtrun)
ARM64 ONLY. The ProbeAgent Debian package shipped by Qtrun
(probeagent_5.9.2-1_arm64.deb) is built exclusively for the arm64
architecture. It cannot be installed on x86-64 / amd64 hosts —
dpkg/apt will reject it with an "architecture mismatch" error. If you need
to develop or test moduliser on amd64, skip this section entirely; the rest of
the stack still works without probeagent.
ProbeAgent is the Qtrun cellular/5G probing daemon used by moduliser to drive scripted RF measurement campaigns. The .deb postinst enables probeagent.service via deb-systemd-helper and registers the Qtrun root CA with ca-certificates.
18.1 Download the ProbeAgent release bundle
Everything needed (the Debian package, the dev license, and the REST API doc) ships in a single zip on the Omirion client portal:
release-5.9.2.zip — https://clients.omirion.com/api/files/5d737745-c511-4bab-bc47-3898119856c4/download
Contents:
| File | Purpose | Used in step |
|---|---|---|
probeagent_5.9.2-1_arm64.deb | The probeagent daemon (arm64) | 18.2 |
configuration_develop.xml | Shared development license | 18.6 |
usage.html | REST API reference (/start, /test/*, /import, …) | reference |
Download and extract the zip on your workstation, then copy the two files to the target:
unzip release-5.9.2.zip -d release-5.9.2
scp release-5.9.2/probeagent_5.9.2-1_arm64.deb sysadmin@<pi>:/tmp/
scp release-5.9.2/configuration_develop.xml sysadmin@<pi>:/tmp/18.2 Install the Debian package
On the target, install the .deb — apt resolves the runtime dependencies:
sudo apt install -y /tmp/probeagent_5.9.2-1_arm64.deb
rm /tmp/probeagent_5.9.2-1_arm64.debThe .deb does not include configuration_develop.xml. It only creates
an empty /var/cache/probeagent/ directory — the license file must be dropped
into it manually in step 18.6.
18.3 Configure server.address and data.dir
Edit /etc/probeagent/probeagent.ini to make the REST API reachable on the LAN and to point the data directory under /var/log/moduliser/ so the moduliser CLI (running as sysadmin) can move post-test archives into per-run directories.
# Expose the REST API on TCP 6226 (default ships as an abstract Unix socket)
sed -i 's|^\s*server\.address\s*=.*|server.address = 0.0.0.0:6226|' /etc/probeagent/probeagent.ini
grep -q '^server\.address' /etc/probeagent/probeagent.ini \
|| echo 'server.address = 0.0.0.0:6226' >> /etc/probeagent/probeagent.ini
# Repoint data.dir so moduliser-cli (running as sysadmin) can drain archives
sed -i 's|^\s*data\.dir\s*=.*|data.dir = /var/log/moduliser/probeagent/|' /etc/probeagent/probeagent.ini
grep -q '^data\.dir' /etc/probeagent/probeagent.ini \
|| echo 'data.dir = /var/log/moduliser/probeagent/' >> /etc/probeagent/probeagent.iniThe default data.dir (/var/lib/probeagent) is root-owned 0755, which means the moduliser CLI cannot unlink files in its parent — required to move the archive into /var/log/moduliser/test-runs/<run>/. Moving data.dir under a moduliser-owned location avoids running the CLI as root.
18.4 Create the moduliser-owned data directory
mkdir -p /var/log/moduliser/probeagent
chown sysadmin:sysadmin /var/log/moduliser/probeagent
chmod 755 /var/log/moduliser/probeagent18.5 Enable the probeagent service at boot
systemctl enable probeagent
systemctl start probeagent18.6 Apply the Qtrun development license
ProbeAgent will not run tests without a valid license — calls to /start and any /test/* endpoint will fail until one is registered.
The development license is bound to a fixed machine-id and is shared across all dev units. It expires in early August 2026. The configuration_develop.xml file you copied to /tmp/ in step 18.1 is the dev license.
# Pin /etc/machine-id to the value the dev license was issued for
echo "b777fe54cdab4fc787a5a57790fc3d77" | sudo tee /etc/machine-id
# Drop the license file where probeagent looks for it on startup
# (the value of `setting.cache.dir` + `configuration.xml` in probeagent.ini)
sudo cp /tmp/configuration_develop.xml /var/cache/probeagent/configuration.xml
sudo chmod 644 /var/cache/probeagent/configuration.xml
rm /tmp/configuration_develop.xml
# Restart so probeagent re-reads the license
sudo systemctl restart probeagent18.7 Verify probeagent is running
# Service status
systemctl is-active probeagent
# Expected: active
# Hit the REST API on the LAN bind (port 6226)
curl -s http://localhost:6226/test/status
# Expected JSON, e.g.:
# { "dataConnection": false, "device": 0, "deviceNormal": 0,
# "initialized": false, "task": 0, "testing": false }If initialized is false, call GET /start once to initialize the test service:
curl -s http://localhost:6226/startVerification Checklist
After completing all steps, verify the setup:
# Node.js version
node --version
# Expected: v24.x.x
# Moduliser CLI
moduliser --version
# ModemManager running
systemctl is-active ModemManager
# Expected: active
# gpsd socket listening
systemctl is-active gpsd.socket
# Expected: active
# gpsd device configured with stable symlink and -b flag
cat /etc/default/gpsd | grep -E 'DEVICES|GPSD_OPTIONS'
# Expected:
# DEVICES="/dev/quectel-gps"
# GPSD_OPTIONS="-n -b"
# Stable udev symlinks
ls -la /dev/quectel-*
# Expected: symlinks to ttyUSBx devices
# Reserved ports are not held by ModemManager
lsof /dev/quectel-gps /dev/quectel-at-aux 2>/dev/null
# Expected: (empty — both ports must be free for gpsd and AT-aux access)
# User groups
id sysadmin
# Expected: groups include dialout
# Capabilities
getcap /usr/bin/tcpdump /usr/bin/ping /usr/bin/node
# Expected:
# /usr/bin/tcpdump cap_net_admin,cap_net_raw=eip
# /usr/bin/ping cap_net_raw=ep
# /usr/bin/node cap_net_raw=ep
# Modem detected (if plugged in)
mmcli -L
# Chrony running
systemctl is-active chronyd
# Expected: active
# QLog installed
qlog --version
# Log directories exist
ls -la /var/log/moduliser/
# GSM connection profile (if configured)
nmcli connection show modem-under-test
# ProbeAgent running and reachable on LAN (arm64 only)
systemctl is-active probeagent
# Expected: active
curl -s http://localhost:6226/test/status
# Expected: valid JSON payload