Omirion Docs

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:packages scope (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-clients

2. 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 nodejs

3. Install PNPM

curl -fsSL https://get.pnpm.io/install.sh | sh -

4. Install correct Iperf3 version

  1. Removing the old version: sudo apt remove iperf3 libiperf0

  2. Install the dependency: sudo apt install libsctp1

  3. Take a recent Ubuntu distribution from https://launchpad.net/ubuntu/+source/iperf3

  4. Download iperf3_3.xx-1_amd64.deb and libiperf0_3.xx-1_amd64.deb packages (use amd64 version for a standard version of Ubuntu)

  5. Install downloaded packages:

    sudo dpkg -i libiperf0_3.xx-1_amd64.deb iperf3_3.xx-1_amd64.deb

  6. 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-cli

Back as root, create a system-wide symlink:

ln -sf /home/sysadmin/.npm-global/bin/moduliser /usr/local/bin/moduliser

6. Enable ModemManager

systemctl enable ModemManager
systemctl start ModemManager

7. 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) for gpsd direct 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 ModemManager

Verify:

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"
EOF

Enable and start gpsd

systemctl enable gpsd.socket
systemctl start gpsd.socket

gpsd 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
cgps

9. 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>
EOF

10. 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;
        }
    }
});
EOF

11. 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/qmicli

12. 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/chronyc

13. Set User Permissions

Add sysadmin to the dialout group for serial/modem device access:

usermod -aG dialout sysadmin

14. 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/node

The 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=ep

15. Create Log Directories

mkdir -p /var/log/moduliser/logs
mkdir -p /var/log/moduliser/captures
chown -R sysadmin:sysadmin /var/log/moduliser

16. 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.nmconnection

If your carrier requires authentication or a SIM PIN, add these lines under the [gsm] section:

username=YOUR_USERNAME
password=YOUR_PASSWORD
pin=YOUR_PIN

Common French carrier APNs:

CarrierAPN
Orangeorange
SFRsl2sfr
Bouyguesmmsbouygtel.com
Freefree

Reload NetworkManager to apply:

nmcli connection reload

17. 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_build

18. 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.ziphttps://clients.omirion.com/api/files/5d737745-c511-4bab-bc47-3898119856c4/download

Contents:

FilePurposeUsed in step
probeagent_5.9.2-1_arm64.debThe probeagent daemon (arm64)18.2
configuration_develop.xmlShared development license18.6
usage.htmlREST 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.deb

The .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.ini

The 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/probeagent

18.5 Enable the probeagent service at boot

systemctl enable probeagent
systemctl start probeagent

18.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 probeagent

18.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/start

Verification 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

On this page