Skip to content

Instantly share code, notes, and snippets.

@bachkukkik
Created May 1, 2026 11:44
Show Gist options
  • Select an option

  • Save bachkukkik/4aad1008e868be345d9135358a6049c5 to your computer and use it in GitHub Desktop.

Select an option

Save bachkukkik/4aad1008e868be345d9135358a6049c5 to your computer and use it in GitHub Desktop.
uConsole 4G/LTE Cellular Internet Setup Guide + System Tray Signal Indicator
#!/usr/bin/env python3
import subprocess
import threading
import gi
gi.require_version('Gtk', '3.0')
gi.require_version('AyatanaAppIndicator3', '0.1')
from gi.repository import Gtk, AyatanaAppIndicator3, GLib
APP_ID = 'cellular-indicator'
TECH_ICONS = {
'lte': 'network-cellular-4g-symbolic',
'4g': 'network-cellular-4g-symbolic',
'3g': 'network-cellular-3g-symbolic',
'umts': 'network-cellular-3g-symbolic',
'hspa': 'network-cellular-hspa-symbolic',
'2g': 'network-cellular-2g-symbolic',
'gsm': 'network-cellular-2g-symbolic',
'edge': 'network-cellular-edge-symbolic',
'gprs': 'network-cellular-gprs-symbolic',
}
SIGNAL_ICONS = {
'none': 'network-cellular-signal-none-symbolic',
'weak': 'network-cellular-signal-weak-symbolic',
'ok': 'network-cellular-signal-ok-symbolic',
'good': 'network-cellular-signal-good-symbolic',
'excellent': 'network-cellular-signal-excellent-symbolic',
}
def get_modem_info():
try:
result = subprocess.run(
['mmcli', '-m', 'any', '-J'],
capture_output=True, text=True, timeout=5
)
if result.returncode != 0:
return None
import json
data = json.loads(result.stdout)
modem = data.get('modem', {})
generic = modem.get('generic', {})
state = generic.get('state', 'unknown')
signal_quality = 0
sq = generic.get('signal-quality', {})
if isinstance(sq, dict):
signal_quality = int(sq.get('value', '0'))
access_techs = generic.get('access-technologies', [])
access_tech = access_techs[0] if access_techs else ''
operator_name = ''
p3gpp = modem.get('3gpp', {})
if isinstance(p3gpp, dict):
operator_name = str(p3gpp.get('operator-name', ''))
return {
'state': state,
'signal_quality': signal_quality,
'access_tech': access_tech,
'operator_name': operator_name,
}
except Exception:
return None
def get_signal_icon(quality):
if quality >= 87:
return SIGNAL_ICONS['excellent']
elif quality >= 62:
return SIGNAL_ICONS['good']
elif quality >= 37:
return SIGNAL_ICONS['ok']
elif quality > 0:
return SIGNAL_ICONS['weak']
return SIGNAL_ICONS['none']
def get_tech_icon(tech):
tech_lower = tech.lower() if tech else ''
for key, icon in TECH_ICONS.items():
if key in tech_lower:
return icon
return None
def quality_to_label(quality):
if quality >= 87:
return '\u2588\u2588\u2588\u2588'
elif quality >= 62:
return '\u2588\u2588\u2588\u2591'
elif quality >= 37:
return '\u2588\u2588\u2591\u2591'
elif quality > 0:
return '\u2588\u2591\u2591\u2591'
return '\u2591\u2591\u2591\u2591'
def run_cmd(cmd, timeout=60):
try:
subprocess.run(cmd, capture_output=True, text=True, timeout=timeout)
except Exception:
pass
class CellularIndicator:
def __init__(self):
self.indicator = AyatanaAppIndicator3.Indicator.new(
APP_ID,
'network-cellular-disabled-symbolic',
AyatanaAppIndicator3.IndicatorCategory.SYSTEM_SERVICES
)
self.indicator.set_status(AyatanaAppIndicator3.IndicatorStatus.ACTIVE)
self.indicator.set_title('Cellular')
self.modem_active = False
self.busy = False
self.menu = Gtk.Menu()
self.status_item = Gtk.MenuItem(label='No modem')
self.status_item.set_sensitive(False)
self.status_item.show()
self.menu.append(self.status_item)
self.sep1 = Gtk.SeparatorMenuItem()
self.sep1.show()
self.menu.append(self.sep1)
self.enable_item = Gtk.MenuItem(label='Enable 4G Modem')
self.enable_item.connect('activate', self.on_enable)
self.enable_item.show()
self.menu.append(self.enable_item)
self.disable_item = Gtk.MenuItem(label='Disable 4G Modem')
self.disable_item.connect('activate', self.on_disable)
self.disable_item.hide()
self.menu.append(self.disable_item)
self.sep2 = Gtk.SeparatorMenuItem()
self.sep2.show()
self.menu.append(self.sep2)
self.operator_item = Gtk.MenuItem(label='')
self.operator_item.set_sensitive(False)
self.operator_item.show()
self.menu.append(self.operator_item)
self.tech_item = Gtk.MenuItem(label='')
self.tech_item.set_sensitive(False)
self.tech_item.show()
self.menu.append(self.tech_item)
self.signal_item = Gtk.MenuItem(label='')
self.signal_item.set_sensitive(False)
self.signal_item.show()
self.menu.append(self.signal_item)
self.sep3 = Gtk.SeparatorMenuItem()
self.sep3.show()
self.menu.append(self.sep3)
self.quit_item = Gtk.MenuItem(label='Quit')
self.quit_item.connect('activate', self.on_quit)
self.quit_item.show()
self.menu.append(self.quit_item)
self.indicator.set_menu(self.menu)
self.update_info()
GLib.timeout_add_seconds(5, self.update_info)
def update_info(self):
if self.busy:
return True
info = get_modem_info()
if info is None or info['state'] in ('failed', 'unknown'):
self.modem_active = False
self.indicator.set_icon_full(
'network-cellular-disabled-symbolic', 'No modem')
self.indicator.set_label('', '')
self.status_item.set_label('No modem detected')
self.operator_item.set_label('')
self.tech_item.set_label('')
self.signal_item.set_label('')
self.enable_item.show()
self.disable_item.hide()
self.sep2.hide()
return True
quality = info['signal_quality']
tech = info['access_tech']
operator = info['operator_name']
state = info['state']
self.modem_active = True
self.enable_item.hide()
self.disable_item.show()
self.sep2.show()
if state == 'connected':
tech_icon = get_tech_icon(tech) or get_signal_icon(quality)
label = quality_to_label(quality)
self.indicator.set_icon_full(
tech_icon, f'Cellular: {tech.upper()} {quality}%')
self.indicator.set_label(f' {label} ', '')
self.status_item.set_label('State: Connected')
elif state == 'registered':
icon = get_tech_icon(tech) or get_signal_icon(quality)
label = quality_to_label(quality)
self.indicator.set_icon_full(
icon, f'Cellular: {tech.upper()} {quality}%')
self.indicator.set_label(f' {label} ', '')
self.status_item.set_label('State: Registered (no data)')
elif state == 'searching':
self.indicator.set_icon_full(
'network-cellular-acquiring-symbolic', 'Searching...')
self.indicator.set_label(' ... ', '')
self.status_item.set_label('State: Searching...')
elif state == 'enabled':
self.indicator.set_icon_full(
'network-cellular-offline-symbolic', 'No signal')
self.indicator.set_label('', '')
self.status_item.set_label('State: No signal')
else:
self.indicator.set_icon_full(
'network-cellular-disabled-symbolic', state)
self.indicator.set_label('', '')
self.status_item.set_label(f'State: {state}')
self.operator_item.set_label(
f'Operator: {operator}' if operator else 'Operator: N/A')
self.tech_item.set_label(
f'Technology: {tech.upper()}' if tech else 'Technology: N/A')
self.signal_item.set_label(f'Signal: {quality}%')
return True
def on_enable(self, widget):
if self.busy:
return
self.busy = True
self.enable_item.set_sensitive(False)
self.disable_item.set_sensitive(False)
self.status_item.set_label('Enabling 4G modem...')
self.indicator.set_icon_full(
'network-cellular-acquiring-symbolic', 'Enabling...')
self.indicator.set_label(' ... ', '')
def _worker():
run_cmd(['sudo', 'uconsole-4g', 'enable'], timeout=60)
run_cmd(['sleep', '5'])
run_cmd(['sudo', 'nmcli', 'c', 'up', '4gnet'], timeout=30)
GLib.idle_add(self._op_done)
threading.Thread(target=_worker, daemon=True).start()
def on_disable(self, widget):
if self.busy:
return
self.busy = True
self.enable_item.set_sensitive(False)
self.disable_item.set_sensitive(False)
self.status_item.set_label('Disabling 4G modem...')
self.indicator.set_icon_full(
'network-cellular-disabled-symbolic', 'Disabling...')
self.indicator.set_label('', '')
def _worker():
run_cmd(['sudo', 'nmcli', 'c', 'down', '4gnet'], timeout=15)
run_cmd(['sudo', 'uconsole-4g', 'disable'], timeout=60)
GLib.idle_add(self._op_done)
threading.Thread(target=_worker, daemon=True).start()
def _op_done(self):
self.busy = False
self.enable_item.set_sensitive(True)
self.disable_item.set_sensitive(True)
self.update_info()
def on_quit(self, widget):
Gtk.main_quit()
def main():
CellularIndicator()
Gtk.main()
if __name__ == '__main__':
main()

uConsole 4G/LTE Cellular Internet Setup Guide

This guide covers setting up cellular internet on the ClockworkPi uConsole with the official 4G LTE extension board (SIM7600G-H modem). Written for Debian-based OS images (Bookworm/Trixie) on CM4/CM5 compute modules.

Tested on: Raspberry Pi Compute Module 5 Lite, Debian 13 (Trixie), kernel 6.12.


Prerequisites

  • uConsole with CM4 or CM5 compute module
  • Official uConsole 4G LTE extension board installed
  • Activated SIM card with data plan, inserted into the extension board
  • OS image with ModemManager, NetworkManager, and nmcli installed

Step 1: Power On the 4G Extension

The 4G extension is powered off by default. You need to enable it each boot:

sudo uconsole-4g enable

Wait about 20 seconds for it to complete. Do not press Ctrl+C.

On older OS images, the script may be called enable_4g_cm4.sh or uconsole-4g-cm4 enable.


Step 2: Verify the Modem Appeared

lsusb | grep -i "simtech\|1e0e"

You should see:

Bus 001 Device 005: ID 1e0e:9001 Qualcomm / Option SimTech, Incorporated

Check ModemManager detects it:

mmcli -L

You should see:

/org/freedesktop/ModemManager1/Modem/0 [QUALCOMM INCORPORATED] SIMCOM_SIM7600G-H

If not, restart ModemManager:

sudo systemctl restart ModemManager
sleep 5
mmcli -L

Step 3: Create the GSM Connection

Check the primary port:

mmcli -m any | grep "primary port"

The modem can present two different primary ports depending on which kernel drivers are loaded:

Primary Port Interface Driver Data Mode
cdc-wdm0 wwan0 qmi_wwan QMI/WWAN (recommended)
ttyUSB2 ppp0 option (USB serial) PPP (unreliable)

We strongly recommend QMI/WWAN mode. PPP mode over ttyUSB2 is known to silently drop data — the connection appears "connected" but traffic doesn't flow. QMI/WWAN is rock-solid.

If your primary port is ttyUSB2 instead of cdc-wdm0, see the troubleshooting section below to switch to QMI mode.

Create the connection (replace <APN> with your carrier's APN, see table below):

sudo nmcli c add type gsm ifname cdc-wdm0 con-name 4gnet apn <APN>

Example with AIS Thailand:

sudo nmcli c add type gsm ifname cdc-wdm0 con-name 4gnet apn internet

Step 4: Connect

sudo nmcli c up 4gnet

Verify:

ip addr show wwan0
ping -c 3 -I wwan0 8.8.8.8

You should see 0% packet loss. The wwan0 interface should have an IP address.


APN Reference (Thailand)

Carrier APN Notes
AIS internet Prepaid/Postpaid mobile internet
DTAC www.dtac.co.th
True Move internet
True Move H truenet

For other countries, check with your carrier or search [carrier name] APN settings.


Troubleshooting

Primary port is ttyUSB2 (no cdc-wdm0)

This means the qmi_wwan kernel module isn't loading. Check:

lsmod | grep qmi_wwan

If it's not loaded, check for a blacklist:

grep -r "blacklist.*qmi" /etc/modprobe.d/

If you find one, remove it and reboot:

sudo rm /etc/modprobe.d/blacklist-qmi.conf  # or whatever the file is named
sudo reboot

Modem shows "registered" but not "connected" (no data)

The GSM connection profile's interface name may not match the modem's current primary port:

  1. Check: mmcli -m any | grep "primary port"
  2. Check: nmcli connection show 4gnet | grep connection.interface-name
  3. If they don't match, update:
    sudo nmcli c modify 4gnet connection.interface-name <PORT>
    sudo nmcli c down 4gnet && sleep 3 && sudo nmcli c up 4gnet

Connection activation failed

Error: Connection activation failed: No suitable device found for this connection
  • Check modem is powered on: mmcli -L
  • Check SIM state: mmcli -m any — look for sim-missing in failed reason
  • Delete and recreate: sudo nmcli c delete 4gnet then re-run Step 3

SIM missing

  • Remove and re-insert the SIM card
  • Make sure the SIM is seated correctly in the extension board slot
  • Verify the SIM works in a phone

No internet even though connected

  • Check routing: ip route
  • Try specifying interface: ping -I wwan0 8.8.8.8
  • Check DNS: cat /etc/resolv.conf
  • If using PPP (ppp0) instead of WWAN, this is a known issue — switch to QMI/WWAN mode (see above)
  • Reconnect: sudo nmcli c down 4gnet && sleep 3 && sudo nmcli c up 4gnet

4G extension not powering on

  • Make sure the script exists: which uconsole-4g
  • For CM4: try sudo uconsole-4g-cm4 enable or sudo enable_4g_cm4.sh
  • Check GPIO pins: sudo pinctrl get 24 15

Bonus: Cellular Signal Tray Indicator

The built-in wfplug-netman panel plugin on Raspberry Pi OS may not show cellular signal alongside WiFi. This custom tray indicator adds:

  • Cellular signal strength bars in the system tray
  • Technology icon (4G/3G/2G)
  • Dropdown menu with operator, technology, and signal info
  • Enable/Disable 4G Modem directly from the tray menu
  • Auto-updates every 5 seconds
  • Auto-starts on login via systemd user service

Prerequisites

Already installed on Raspberry Pi OS:

  • python3-gi (GTK + GObject bindings)
  • gir1.2-ayatanaappindicator3-0.1 (tray indicator library)
  • ModemManager with mmcli

Installation

  1. Save the cellular-indicator.py script (see companion file in this Gist) to ~/.local/bin/:
mkdir -p ~/.local/bin
cp cellular-indicator.py ~/.local/bin/
chmod +x ~/.local/bin/cellular-indicator.py
  1. Create a passwordless sudo rule so the indicator can enable/disable the modem without prompting:
sudo bash -c 'cat << EOF > /etc/sudoers.d/uconsole-4g
YOUR_USERNAME ALL=(root) NOPASSWD: /usr/bin/uconsole-4g, /usr/local/bin/uconsole-4g-cm4.sh, /usr/bin/pinctrl
EOF'
sudo chmod 440 /etc/sudoers.d/uconsole-4g

Replace YOUR_USERNAME with your actual username. Verify the sudoers file is valid:

sudo visudo -c
  1. Create the systemd user service:
mkdir -p ~/.config/systemd/user
cat > ~/.config/systemd/user/cellular-indicator.service << 'EOF'
[Unit]
Description=Cellular Signal Tray Indicator
After=graphical-session.target

[Service]
Type=simple
ExecStart=%h/.local/bin/cellular-indicator.py
Restart=on-failure
RestartSec=5

[Install]
WantedBy=default.target
EOF
  1. Enable and start:
systemctl --user daemon-reload
systemctl --user enable --now cellular-indicator.service

Usage

  • The indicator appears in your system tray (near WiFi/clock)
  • Click the icon to see a dropdown menu
  • When no modem is active: menu shows "Enable 4G Modem"
  • When modem is active: menu shows connection details + "Disable 4G Modem"
  • Enabling takes ~25 seconds (power on + connect); disabling ~25 seconds
  • Signal bars update every 5 seconds

Control

# Start/stop
systemctl --user start cellular-indicator.service
systemctl --user stop cellular-indicator.service

# Enable/disable autostart
systemctl --user enable cellular-indicator.service
systemctl --user disable cellular-indicator.service

# View logs
journalctl --user -u cellular-indicator.service -f

Quick Start (TL;DR)

# 1. Power on
sudo uconsole-4g enable

# 2. Check modem appeared + primary port
mmcli -m any | grep "primary port"
# Should be cdc-wdm0 (QMI/WWAN mode)

# 3. Create GSM connection (replace APN)
sudo nmcli c add type gsm ifname cdc-wdm0 con-name 4gnet apn <APN>
sudo nmcli c up 4gnet

# 4. Verify
ping -c 3 -I wwan0 8.8.8.8

References


Last updated: May 2026

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment