Sending AT commands through com port

Hey everyone :sweat_smile:

I’ve been working on a small Go program for my X3000 modem that connects directly to the SIM module’s serial port and communicates through AT commands. It looks for new messages in the incoming SMS directory and automatically sends a reply through the COM port if the message matches a specific regex pattern.

It works about half the time, but the other half it just locks up. I think the GL.iNet core packages are running into a race condition whenever both streams overlap. When that happens, everything starts to glitch, sometimes it fails completely, and sometimes it even takes down my internet connection.

Is there a cleaner or more reliable way to handle AT commands on the client side? Maybe some sort of queue I could send commands through instead of pushing them directly?

I would recommend hooking up /etc/forward

Rename it to /etc/forward.real and make sure that you call exec /etc/forward.real "$@" at the end of your script. You might need to wrap a shell script around your go app, I am not sure if you can run it directly.

/etc/forward will be used as soon as you enable forwarding an SMS inside the GL interface. So this script will always be notified about new SMS.

That looks interesting but this cant work dynamically right? As I can’t provide a number to forward it to

Since you write your own script, you can do whatever you like - so dynamically is possible of course.

This one is my example; I'm not sure if it still works.

#!/bin/sh
# Wrapper for GL.iNet smstools:
# Detects O2 data usage warning messages and automatically replies "WEITER"
# Then calls the original /etc/forward.real script.

LOG="/var/log/o2_autoweiter.log"
OUTBASE="/etc/spool/sms/outgoing/0001:01:00.0"   # adjust if your modem slot path changes
O2_SHORT="80112"

# --- Text recognition patterns ---
# Positive: data usage warning + instruction to reply
RE_REQUIRE='(80%|100%|Highspeed[^\.]*Datenvolumen|Inklusivvolumen)'
RE_WEITER_HINT='(antworte|antworten|mit)[^\.]*WEITER'
# Negative: success or confirmation messages (no further reply)
RE_NEG='(erfolgreich aktiviert|wurde erfolgreich aktiviert|bereits aktiviert|aktiviert\.?\s*Ihr o2 Team)'

# --- State files ---
STAMP="/tmp/o2_weiter_last"      # timestamp of last WEITER sent
PENDING="/tmp/o2_weiter_pending" # prevents new WEITER while waiting for confirmation
SEEN_DIR="/tmp/o2_seen"; mkdir -p "$SEEN_DIR"

THROTTLE=60        # seconds between two WEITER messages (minimum)
PENDING_TTL=240    # seconds to keep pending before it expires

log(){ echo "[$(date +'%F %T')] $*" >> "$LOG"; logger -t o2_autoweiter "$*" >/dev/null 2>&1; }

mk_out(){
  mkdir -p "$OUTBASE"
  tmp="$(mktemp "$OUTBASE"/o2weiter.XXXXXX)" || return 1
  {
    echo "To: $O2_SHORT"
    echo "Alphabet: ISO"
    echo
    echo "WEITER"
  } >"$tmp"
  mv "$tmp" "$OUTBASE/weiter-$(date +%s).sms" 2>/dev/null || true
}

# --- Main logic ---
event="$1"; infile="$2"; device="$3"

if [ "$event" = "RECEIVED" ] && [ -n "$infile" ] && [ -f "$infile" ]; then
  bname="$(basename "$infile")"
  [ -f "$SEEN_DIR/$bname" ] && { log "Already processed ($bname) – skipping."; exec /etc/forward.real "$@"; exit 0; }

  # Split header/body at the first empty line
  line="$(awk '/^\s*$/{print NR; exit}' "$infile")"
  if [ -n "$line" ]; then
    header="$(sed -n "1,${line}p" "$infile")"
    body="$(sed -n "$((line+1)),\$p" "$infile")"
  else
    header="$(sed -n '1,20p' "$infile")"
    body="$(sed -n '21,$p' "$infile")"
  fi

  from="$(printf '%s\n' "$header" | sed -n 's/^From:[[:space:]]*\(.*\)$/\1/p' | head -n1)"
  [ -z "$from" ] && from="$(printf '%s\n' "$header" | sed -n 's/^Sender:[[:space:]]*\(.*\)$/\1/p' | head -n1)"
  preview="$(printf '%s' "$body" | tr '\n' ' ' | sed 's/[[:cntrl:]]//g' | cut -c1-220)"
  log "EVENT=$event dev=${device:-?} file=$bname from=${from:-unknown} body='$preview'"

  # --- Handle different SMS types ---
  # Success / confirmation messages
  if printf '%s' "$body" | grep -Eiq "$RE_NEG"; then
    [ -f "$PENDING" ] && rm -f "$PENDING"
    log "Confirmation SMS detected – no WEITER sent, pending cleared."

  # Ignore self-sent "WEITER" echoes
  elif printf '%s' "$body" | grep -Eq '^\s*WEITER\s*$'; then
    log "Own WEITER detected – ignoring."

  # Data limit warnings with 'reply WEITER' instruction
  elif printf '%s' "$body" | grep -Eiq "$RE_REQUIRE" && printf '%s' "$body" | grep -Eiq "$RE_WEITER_HINT"; then
    now="$(date +%s)"

    # Check pending (wait for confirmation)
    if [ -f "$PENDING" ]; then
      age=$(( now - $(cat "$PENDING" 2>/dev/null || echo 0) ))
      if [ "$age" -lt "$PENDING_TTL" ]; then
        log "Pending active ($age s) – skipping new WEITER."
        touch "$SEEN_DIR/$bname"
        exec /etc/forward.real "$@"; exit 0
      else
        log "Pending expired ($age s) – allowing new WEITER."
        rm -f "$PENDING"
      fi
    fi

    # Check throttle
    last=0; [ -f "$STAMP" ] && last="$(cat "$STAMP" 2>/dev/null || echo 0)"
    if [ $(( now - last )) -lt "$THROTTLE" ]; then
      log "Throttle active: last WEITER sent $(( now - last ))s ago – skipping."
    else
      if mk_out; then
        echo "$now" >"$STAMP"; echo "$now" >"$PENDING"
        log "→ WEITER added to outgoing (text match)."
      else
        log "Error: failed to create outgoing message."
      fi
    fi
  fi

  touch "$SEEN_DIR/$bname"
fi

# --- Always run the original handler afterwards ---
exec /etc/forward.real "$@"
1 Like