GL.iNet X-3000 Spitz AX Opsætning

Overview

This guide walks through a clean setup for a GL.iNet X-3000 Spitz AX as a 4G/5G router with secure remote access.

Prerequisites

  • Active SIM with data plan

  • APN details from your carrier

  • Ethernet cable for initial setup

  • Laptop connected to the router LAN

Initial Access

  1. Connect via LAN to the router.

  2. Open the admin UI at http://192.168.8.1.

  3. Set a strong admin password and store it.

Update Firmware

  1. Go to System -> Upgrade.

  2. Check for the latest firmware and apply it.

  3. Reboot and confirm version.

SIM and Mobile Settings

  1. Insert the SIM.

  2. Go to Network -> Modem.

  3. Set the APN provided by your carrier.

  4. Save and wait for the modem to register.

  5. Confirm “Connected” and verify public IP.

WAN/LAN Setup

  • If using it as primary internet: - Leave WAN as DHCP or static as needed.

  • If using it behind another router: - Enable “Repeater” or bridge mode (if required).

  • Set LAN IP range (e.g., 192.168.10.1/24).

Wi-Fi Setup

  1. Go to Wireless.

  2. Rename SSID (2.4 GHz and 5 GHz).

  3. Set WPA2/WPA3 password.

Remote Access (VPN)

Backups

  1. Go to System -> Backup/Restore.

  2. Download a backup after setup.

Troubleshooting

  • No mobile data: - Re-check APN, SIM activation, and signal strength.

  • Unstable connection: - Lock modem bands or set preferred network mode.

  • Can’t reach admin UI: - Factory reset using the reset pin (hold 10 seconds).

Change Log

  • 0.1.0 Initial draft

  • test test test

MQTT

SIM mangement

Install pakages

$ opkg install luci-app-statistics collectd-mod-mqtt mosquitto-client-ssl collectd-mod-thermal collectd-mod-uptime collectd-mod-dhcpleases collectd-mod-ping

Create config directory to store MQTT config for collectd

$ mkdir -p /etc/collectd/conf.d/

Define variables and replace values of variables according to yours. Leave MQTT_TLS_PROTOCOL and MQTT_CA_CERT blank if not used

MQTT_BROKER_HOST="192.168.8.200"
MQTT_PORT="1883"
MQTT_USERNAME="openwrt"
MQTT_PASSWORD="MitHemmeligePassword!"
MQTT_PREFIX="collectd"

MQTT_TLS_PROTOCOL=""
MQTT_CA_CERT=""

Create shared config to store the variables

cat >/etc/collectd/mqtt_broker.conf <<EOF
MQTT_BROKER_HOST="$MQTT_BROKER_HOST"
MQTT_PORT="$MQTT_PORT"
MQTT_USERNAME="$MQTT_USERNAME"
MQTT_PASSWORD="$MQTT_PASSWORD"
MQTT_PREFIX="$MQTT_PREFIX"
MQTT_TLS_PROTOCOL="$MQTT_TLS_PROTOCOL"
MQTT_CA_CERT="$MQTT_CA_CERT"
EOF

Modify file permissions

$ chmod 600 /etc/collectd/mqtt_broker.conf

Generate collectd config to store in /etc/collectd/conf.d

. /etc/collectd/mqtt_broker.conf && \
cat >/etc/collectd/conf.d/mqtt.conf <<EOF
LoadPlugin mqtt
<Plugin "mqtt">
  <Publish "OpenWRT">
    Host "$MQTT_BROKER_HOST"
    Port "${MQTT_PORT:-1883}"
    User "$MQTT_USERNAME"
    Password "$MQTT_PASSWORD"
    ClientId "$(uci -q get system.@system[0].hostname)"
    Prefix "${MQTT_PREFIX:-collectd}"
    Retain true
  </Publish>
</Plugin>
EOF

Restart collecd service

$ /etc/init.d/collectd restart
cat >/etc/hotplug.d/iface/01-ha-mqtt-wan-and-cell-status <<'SH'
#!/bin/sh
# Publish cellular WAN + signal + carrier + SIM info to MQTT

# Load shared MQTT settings
[ -r /etc/collectd/mqtt_broker.conf ] && . /etc/collectd/mqtt_broker.conf

# ---- USER CONFIG ----
WAN_IFACE="modem_0001_4"
MODEM_SIGNAL_TIME=0
CLEAR_DNS_SLOTS=5
CLEAR_IP6_SLOTS=5
# ---------------------

log() { logger -t ha-mqtt-wan "$*"; }
need() { command -v "$1" >/dev/null 2>&1; }

[ -n "$ACTION" ] || exit 0
[ "$INTERFACE" = "$WAN_IFACE" ] || exit 0

need mosquitto_pub || { log "missing mosquitto_pub"; exit 0; }
need jsonfilter    || { log "missing jsonfilter"; exit 0; }
need ifstatus      || { log "missing ifstatus"; exit 0; }
need ubus          || { log "missing ubus"; exit 0; }

# Ensure required MQTT vars exist (fail fast in logs)
[ -n "$MQTT_BROKER_HOST" ] || { log "MQTT_BROKER_HOST not set"; exit 0; }
[ -n "$MQTT_PORT" ] || MQTT_PORT="1883"
[ -n "$MQTT_USERNAME" ] || MQTT_USERNAME=""
[ -n "$MQTT_PASSWORD" ] || MQTT_PASSWORD=""
[ -n "$MQTT_PREFIX" ] || MQTT_PREFIX="collectd"

HOSTNAME="$(uci -q get system.@system[0].hostname)"
[ -n "$HOSTNAME" ] || HOSTNAME="$(hostname 2>/dev/null)"

BASE="$MQTT_PREFIX/$HOSTNAME/wan-info/$WAN_IFACE"

auth_args="-h $MQTT_BROKER_HOST -p $MQTT_PORT"
[ -n "$MQTT_USERNAME" ] && auth_args="$auth_args -u $MQTT_USERNAME"
[ -n "$MQTT_PASSWORD" ] && auth_args="$auth_args -P $MQTT_PASSWORD"
[ -n "$MQTT_TLS_PROTOCOL" ] && auth_args="$auth_args --tls-version $MQTT_TLS_PROTOCOL"
[ -n "$MQTT_CA_CERT" ] && auth_args="$auth_args --cafile $MQTT_CA_CERT"

pub_abs() {
  mosquitto_pub $auth_args -t "$1" -r -m "$2" >/dev/null 2>&1 \
    || log "MQTT publish failed: $1"
}
pub()     { pub_abs "$BASE-$1" "$2"; }

clear_slots() {
  suffix="$1"; cnt="$2"
  i=0
  while [ $i -lt "$cnt" ]; do
    pub "$suffix-$i" ""
    i=$((i + 1))
  done
}

publish_ifstatus() {
  IFJSON="$(ifstatus "$WAN_IFACE" 2>/dev/null)"
  [ -n "$IFJSON" ] || return

  jget() { echo "$IFJSON" | jsonfilter -e "$1" 2>/dev/null; }

  pub "up"         "$(jget '@.up')"
  pub "pending"    "$(jget '@.pending')"
  pub "available"  "$(jget '@.available')"
  pub "autostart"  "$(jget '@.autostart')"
  pub "dynamic"    "$(jget '@.dynamic')"
  pub "uptime"     "$(jget '@.uptime')"
  pub "l3_device"  "$(jget '@.l3_device')"
  pub "proto"      "$(jget '@.proto')"
  pub "device"     "$(jget '@.device')"
  pub "metric"     "$(jget '@.metric')"
  pub "dns_metric" "$(jget '@.dns_metric')"
  pub "delegation" "$(jget '@.delegation')"

  ip4="$(jget '@["ipv4-address"][0].address')"
  m4="$(jget '@["ipv4-address"][0].mask')"
  gw4="$(jget '@.route[0].nexthop')"
  pub "ip4" "$ip4"
  pub "ip4_mask" "$m4"
  pub "gw4" "$gw4"

  lease="$(jget '@.data.leasetime')"
  dhcp_host="$(jget '@.data.hostname')"
  pub "lease" "$lease"
  pub "dhcp_hostname" "$dhcp_host"

  clear_slots "dns" "$CLEAR_DNS_SLOTS"
  dns_list="$(echo "$IFJSON" | jsonfilter -e '@["dns-server"][@]' 2>/dev/null)"
  i=0
  for d in $dns_list; do
    pub "dns-$i" "$d"
    i=$((i + 1))
  done

  clear_slots "ip6" "$CLEAR_IP6_SLOTS"
  ip6_list="$(echo "$IFJSON" | jsonfilter -e '@["ipv6-address"][@].address' 2>/dev/null)"
  i=0
  for a in $ip6_list; do
    pub "ip6-$i" "$a"
    i=$((i + 1))
  done
}

publish_modem_signal() {
  MSJSON="$(ubus call modem.signal get_signals "{\"time\":$MODEM_SIGNAL_TIME}" 2>/dev/null)"
  [ -n "$MSJSON" ] || return

  network_type="$(echo "$MSJSON" | jsonfilter -e '@.signals[0].network_type' 2>/dev/null)"
  rssi="$(echo "$MSJSON"        | jsonfilter -e '@.signals[0].rssi' 2>/dev/null)"
  rsrp="$(echo "$MSJSON"        | jsonfilter -e '@.signals[0].rsrp' 2>/dev/null)"
  rsrq="$(echo "$MSJSON"        | jsonfilter -e '@.signals[0].rsrq' 2>/dev/null)"
  sinr="$(echo "$MSJSON"        | jsonfilter -e '@.signals[0].sinr' 2>/dev/null)"
  strength="$(echo "$MSJSON"    | jsonfilter -e '@.signals[0].strength' 2>/dev/null)"
  mode="$(echo "$MSJSON"        | jsonfilter -e '@.signals[0].mode' 2>/dev/null)"
  ts="$(echo "$MSJSON"          | jsonfilter -e '@.signals[0].timestamp' 2>/dev/null)"
  slot="$(echo "$MSJSON"        | jsonfilter -e '@.signals[0].slot' 2>/dev/null)"

  pub "rat" "$network_type"
  pub "signal-rssi" "$rssi"
  pub "signal-rsrp" "$rsrp"
  pub "signal-rsrq" "$rsrq"
  pub "signal-sinr" "$sinr"
  pub "signal-bars" "$strength"
  pub "signal-mode" "$mode"
  pub "signal-slot" "$slot"
  pub "signal-timestamp" "$ts"
}

publish_carrier() {
  ubus call AT get_result '{"cmd":"AT+COPS=3,0","timeout":3}' >/dev/null 2>&1
  AJ="$(ubus call AT get_result '{"cmd":"AT+COPS?","timeout":3}' 2>/dev/null)"
  [ -n "$AJ" ] || return

  raw="$(echo "$AJ" | jsonfilter -e '@.data' 2>/dev/null | tr -d '\r')"
  carrier="$(echo "$raw" | sed -n 's/.*+COPS: [0-9],[0-9],"\([^"]*\)".*/\1/p' | head -n1)"
  [ -n "$carrier" ] && pub "carrier" "$carrier"

  act="$(echo "$raw" | sed -n 's/.*+COPS: [0-9],[0-9],".*",\([0-9]\+\).*/\1/p' | head -n1)"
  [ -n "$act" ] && pub "carrier-act" "$act"
}

publish_sim_info() {
  AJ="$(ubus call AT get_result '{"cmd":"AT+QUIMSLOT?","timeout":3}' 2>/dev/null)"
  s="$(echo "$AJ" | jsonfilter -e '@.data' 2>/dev/null | tr -d '\r' \
    | sed -n 's/.*+QUIMSLOT: \([0-9]\+\).*/\1/p' | head -n1)"
  [ -n "$s" ] && pub "sim-slot" "$s"

  AJ2="$(ubus call AT get_result '{"cmd":"AT+CCID","timeout":3}' 2>/dev/null)"
  iccid="$(echo "$AJ2" | jsonfilter -e '@.data' 2>/dev/null | tr -d '\r' \
    | sed -n 's/.*+CCID: \([0-9]\+\).*/\1/p' | head -n1)"
  if [ -z "$iccid" ]; then
    AJ3="$(ubus call AT get_result '{"cmd":"AT+QCCID","timeout":3}' 2>/dev/null)"
    iccid="$(echo "$AJ3" | jsonfilter -e '@.data' 2>/dev/null | tr -d '\r' \
      | sed -n 's/.*+QCCID: \([0-9]\+\).*/\1/p' | head -n1)"
  fi
  [ -n "$iccid" ] && pub "sim-iccid" "$iccid"
}

clear_all() {
  pub_abs "$BASE-status" "OFF"
  pub "ip4" ""; pub "ip4_mask" ""; pub "gw4" ""
  pub "uptime" ""; pub "l3_device" ""; pub "proto" ""; pub "device" ""
  pub "metric" ""; pub "dns_metric" ""; pub "delegation" ""
  pub "lease" ""; pub "dhcp_hostname" ""
  pub "rat" ""; pub "signal-rssi" ""; pub "signal-rsrp" ""; pub "signal-rsrq" ""
  pub "signal-sinr" ""; pub "signal-bars" ""; pub "signal-mode" ""
  pub "signal-slot" ""; pub "signal-timestamp" ""
  pub "carrier" ""; pub "carrier-act" ""
  pub "sim-slot" ""; pub "sim-iccid" ""
  clear_slots "dns" "$CLEAR_DNS_SLOTS"
  clear_slots "ip6" "$CLEAR_IP6_SLOTS"
}

case "$ACTION" in
  ifdown)
    clear_all
    log "ifdown $WAN_IFACE"
    ;;
  ifup|ifupdate)
    pub_abs "$BASE-status" "ON"
    publish_ifstatus
    publish_modem_signal
    publish_carrier
    publish_sim_info
    log "$ACTION $WAN_IFACE published"
    ;;
esac

exit 0
SH
  • Make file executable

$ chmod +x /etc/hotplug.d/iface/01-ha-mqtt-wan-and-cell-status
cat >/usr/bin/mqtt_sim_switcher.sh <<'SH'
#!/bin/sh
# MQTT SIM switcher for GL-X3000:
# - Receives SIM slot commands via MQTT
# - Uses GL's /usr/bin/switch_sim_slot (safe + updates GL state + redials)
# - Publishes SIM state (and carrier) to MQTT
# - Watches for SIM changes made in GL UI and republishes state
#
# Shared MQTT config is loaded from: /etc/collectd/mqtt_broker.conf

# Load shared MQTT settings
[ -r /etc/collectd/mqtt_broker.conf ] && . /etc/collectd/mqtt_broker.conf

# ---- LOCAL CONFIG ----
WAN_IFACE="modem_0001_4"
POLL_SECONDS=10   # how often to detect SIM changes from GL UI
# ----------------------

need() { command -v "$1" >/dev/null 2>&1; }
log()  { logger -t mqtt-sim-switcher "$*"; }

need mosquitto_sub   || exit 1
need mosquitto_pub   || exit 1
need ubus            || exit 1
need jsonfilter      || exit 1
[ -x /usr/bin/switch_sim_slot ] || exit 1

# Validate MQTT config (fail fast)
[ -n "$MQTT_BROKER_HOST" ] || { log "MQTT_BROKER_HOST not set (check /etc/collectd/mqtt_broker.conf)"; exit 1; }
[ -n "$MQTT_PORT" ] || MQTT_PORT="1883"
[ -n "$MQTT_PREFIX" ] || MQTT_PREFIX="collectd"

HOSTNAME="$(uci -q get system.@system[0].hostname)"
[ -n "$HOSTNAME" ] || HOSTNAME="$(hostname 2>/dev/null)"

TOPIC_SET="$MQTT_PREFIX/$HOSTNAME/modem/sim/set"
TOPIC_STATE="$MQTT_PREFIX/$HOSTNAME/modem/sim/state"
TOPIC_CARRIER="$MQTT_PREFIX/$HOSTNAME/modem/sim/carrier"
TOPIC_RESULT="$MQTT_PREFIX/$HOSTNAME/modem/sim/result"

AUTH_ARGS="-h $MQTT_BROKER_HOST -p $MQTT_PORT"
[ -n "$MQTT_USERNAME" ] && AUTH_ARGS="$AUTH_ARGS -u $MQTT_USERNAME"
[ -n "$MQTT_PASSWORD" ] && AUTH_ARGS="$AUTH_ARGS -P $MQTT_PASSWORD"
[ -n "$MQTT_TLS_PROTOCOL" ] && AUTH_ARGS="$AUTH_ARGS --tls-version $MQTT_TLS_PROTOCOL"
[ -n "$MQTT_CA_CERT" ] && AUTH_ARGS="$AUTH_ARGS --cafile $MQTT_CA_CERT"

pub_result() {
  mosquitto_pub $AUTH_ARGS -t "$TOPIC_RESULT" -r -m "$1" >/dev/null 2>&1 \
    || log "MQTT publish failed: $TOPIC_RESULT"
}

get_sim_slot() {
  # Prefer GL state file first
  f="/tmp/run/dual_sim/$WAN_IFACE/current_sim"
  slot=""
  if [ -r "$f" ]; then
    slot="$(cat "$f" 2>/dev/null | tr -cd '0-9' | head -c1)"
  fi

  # Fallback to AT
  if [ -z "$slot" ]; then
    AJ="$(ubus call AT get_result '{"cmd":"AT+QUIMSLOT?","timeout":3}' 2>/dev/null)"
    slot="$(echo "$AJ" | jsonfilter -e '@.data' 2>/dev/null \
      | tr -d '\r' \
      | sed -n 's/.*+QUIMSLOT: \([0-9]\+\).*/\1/p' \
      | head -n1)"
  fi

  printf '%s' "$slot"
}

get_carrier() {
  AJ="$(ubus call AT get_result '{"cmd":"AT+COPS?","timeout":3}' 2>/dev/null)"
  raw="$(echo "$AJ" | jsonfilter -e '@.data' 2>/dev/null | tr -d '\r')"
  carrier="$(echo "$raw" | sed -n 's/.*+COPS: [0-9],[0-9],"\([^"]*\)".*/\1/p' | head -n1)"
  printf '%s' "$carrier"
}

publish_state() {
  slot="$(get_sim_slot)"
  if [ -n "$slot" ]; then
    mosquitto_pub $AUTH_ARGS -t "$TOPIC_STATE" -r -m "$slot" >/dev/null 2>&1 \
      || log "MQTT publish failed: $TOPIC_STATE"
  fi

  carrier="$(get_carrier)"
  if [ -n "$carrier" ]; then
    mosquitto_pub $AUTH_ARGS -t "$TOPIC_CARRIER" -r -m "$carrier" >/dev/null 2>&1 \
      || log "MQTT publish failed: $TOPIC_CARRIER"
  fi
}

normalize_payload() {
  p="$(printf '%s' "$1" | tr -d '\r\n' | sed 's/^ *//; s/ *$//' | tr -d '"')"
  [ "$p" = "(null)" ] && p=""
  p="$(printf '%s' "$p" | tr 'A-Z' 'a-z')"
  printf '%s' "$p"
}

switch_sim() {
  sim="$(normalize_payload "$1")"

  # Ignore empties
  [ -z "$sim" ] && { log "RX empty payload ignored"; return; }

  # Accept 1,2,main and a few aliases
  case "$sim" in
    1|2|main) ;;
    sim1|sim_1|sim-1|slot1|slot_1|slot-1|sim\ 1|slot\ 1) sim="1" ;;
    sim2|sim_2|sim-2|slot2|slot_2|slot-2|sim\ 2|slot\ 2) sim="2" ;;
    *)
      pub_result "ERR invalid payload (use 1, 2, or main)"
      log "RX invalid payload=[$sim]"
      return
      ;;
  esac

  log "Switch request: $sim"
  pub_result "OK switching to $sim ..."

  if [ "$sim" = "main" ]; then
    /usr/bin/switch_sim_slot auto sw main >/dev/null 2>&1
    rc=$?
  else
    /usr/bin/switch_sim_slot auto sw "$sim" >/dev/null 2>&1
    rc=$?
  fi

  if [ $rc -ne 0 ]; then
    pub_result "ERR switch_sim_slot failed (rc=$rc)"
    log "switch_sim_slot failed rc=$rc"
    return
  fi

  publish_state
  pub_result "OK switched to $sim"
}

watch_sim_changes() {
  last=""
  while true; do
    slot="$(get_sim_slot)"
    if [ -n "$slot" ] && [ "$slot" != "$last" ]; then
      last="$slot"
      publish_state
      log "SIM change detected -> $slot (published)"
    fi
    sleep "$POLL_SECONDS"
  done
}

# Startup publish
publish_state
pub_result "OK listener started"

# Background watcher (captures GL UI switches)
watch_sim_changes &

# Subscribe forever
mosquitto_sub $AUTH_ARGS -v -t "$TOPIC_SET" 2>/dev/null | while read -r topic payload; do
  log "RX topic=$topic payload_raw=[$payload]"
  switch_sim "$payload"
done
SH
$ chmod +x /etc/init.d/mqtt-sim-switcher
$ /etc/init.d/mqtt-sim-switcher enable
$ /etc/init.d/mqtt-sim-switcher start