🎨 Sync 2026-10-12 21:43:19

This commit is contained in:
github-actions[bot] committed 2026-10-12 21:43:19 +08:00
1 parent 3a82c347ab
commit bfe30ffbef
51 files changed
+8523 -149

No files matched your search

@@ -0,0 +1,164 @@
#!/bin/sh
# Modem AT Command Safe Executor (Zero-dependency, Dynamic Timeout)
# Dante <dante@seckv.com>
DEVICE=""
COMMAND=""
TIMEOUT=3
BAUDRATE=115200
SMS_TO=""
SMS_MSG=""
while [ $# -gt 0 ]; do
case "$1" in
-d) DEVICE="$2"; shift 2 ;;
-c) COMMAND="$2"; shift 2 ;;
-t) TIMEOUT="$2"; shift 2 ;;
-b) BAUDRATE="$2"; shift 2 ;;
--sms-to) SMS_TO="$2"; shift 2 ;;
--sms-msg) SMS_MSG="$2"; shift 2 ;;
*) shift ;;
esac
done
if [ -z "$DEVICE" ]; then
echo "Usage: $0 -d <device> [-c <command>] [--sms-to <number> --sms-msg <text>]" >&2
exit 1
fi
if [ -z "$COMMAND" ] && [ -z "$SMS_TO" ]; then
echo "Usage: Either -c <command> or --sms-to <number> must be specified" >&2
exit 1
fi
case "$DEVICE" in
/dev/ttyUSB*|/dev/ttyACM*) ;;
*)
echo "ERROR: Invalid device path $DEVICE" >&2
exit 2
;;
esac
if [ ! -c "$DEVICE" ]; then
echo "ERROR: Device $DEVICE not found" >&2
exit 3
fi
# Dynamic Timeout auto-detection: long-running commands like AT+COPS=? or scan
if [ -n "$SMS_TO" ]; then
[ "$TIMEOUT" -lt 15 ] && TIMEOUT=15
else
case "$COMMAND" in
*COPS=\?*|*COPS\?*|*QSCAN*|*GTCCINFO*|*SCAN*)
[ "$TIMEOUT" -lt 60 ] && TIMEOUT=60
;;
esac
fi
LOCKFILE="/var/lock/modem_$(basename "$DEVICE").lock"
mkdir -p /var/lock
# Acquire exclusive file lock compatible with Busybox flock (-n -x FD)
exec 9>"$LOCKFILE"
locked=0
waited=0
while [ $waited -le "$TIMEOUT" ]; do
if flock -n -x 9 2>/dev/null; then
locked=1
break
fi
sleep 1
waited=$((waited + 1))
done
if [ $locked -eq 0 ]; then
echo "ERROR: Device $DEVICE busy, lock timed out" >&2
exit 4
fi
# Fast path: Prioritize native sms_tool engine if installed (zero echo loop, robust termios)
if command -v sms_tool >/dev/null 2>&1; then
if [ -n "$SMS_TO" ]; then
OUT=$(sms_tool -d "$DEVICE" -b "$BAUDRATE" send "$SMS_TO" "$SMS_MSG" 2>&1)
RC=$?
else
OUT=$(sms_tool -d "$DEVICE" -b "$BAUDRATE" at "$COMMAND" 2>&1)
RC=$?
fi
flock -u 9 2>/dev/null || true
CLEAN_OUT=$(echo "$OUT" | tr -d "\r")
if [ $RC -eq 0 ] && ! echo "$CLEAN_OUT" | grep -qE "ERROR|\+CME ERROR|\+CMS ERROR"; then
if [ -z "$CLEAN_OUT" ]; then
echo "OK"
else
echo "$CLEAN_OUT"
case "$CLEAN_OUT" in
*OK*) ;;
*) echo "OK" ;;
esac
fi
exit 0
else
echo "$CLEAN_OUT"
exit ${RC:-1}
fi
fi
command -v stty >/dev/null 2>&1 && stty -F "$DEVICE" "$BAUDRATE" raw -echo -echoe -echok min 0 time 2 2>/dev/null || true
OUT_FILE="/tmp/at_out_$$.tmp"
rm -f "$OUT_FILE"
(
exec 3<>"$DEVICE"
# Drain stale residual buffer
dd if="$DEVICE" of=/dev/null bs=512 count=1 2>/dev/null || true
cat <&3 > "$OUT_FILE" &
CAT_PID=$!
if [ -n "$SMS_TO" ]; then
# Text mode SMS sending
printf "AT+CMGF=1\r\n" >&3
sleep 1
printf "AT+CMGS=\"%s\"\r\n" "$SMS_TO" >&3
sleep 1
printf "%s\x1A" "$SMS_MSG" >&3
elapsed=0
while [ $elapsed -lt "$TIMEOUT" ]; do
sleep 1
elapsed=$((elapsed + 1))
if [ -f "$OUT_FILE" ]; then
if grep -q "\+CMGS:" "$OUT_FILE" 2>/dev/null && grep -q "OK" "$OUT_FILE" 2>/dev/null; then
break
elif grep -qE "\+CMS ERROR|\+CME ERROR|ERROR" "$OUT_FILE" 2>/dev/null; then
break
fi
fi
done
else
printf "%s\r\n" "$COMMAND" >&3
elapsed=0
while [ $elapsed -lt "$TIMEOUT" ]; do
sleep 1
elapsed=$((elapsed + 1))
if [ -f "$OUT_FILE" ] && grep -qE "OK|ERROR|\+CME ERROR|\+CMS ERROR" "$OUT_FILE" 2>/dev/null; then
break
fi
done
fi
kill -9 "$CAT_PID" 2>/dev/null || true
exec 3>&-
)
flock -u 9 2>/dev/null || true
if [ -f "$OUT_FILE" ]; then
cat "$OUT_FILE" | tr -d "\r"
rm -f "$OUT_FILE"
fi
exit 0
@@ -0,0 +1,60 @@
#!/bin/sh
# Background Status Collector with Active Terminal Yielding
# Dante <dante@seckv.com>
RUN_DIR="/tmp/run/modem-manager"
CACHE_DEVICES="$RUN_DIR/devices.json"
CACHE_STATUS="$RUN_DIR/status.json"
TERMINAL_LOCK="/var/lock/modem_terminal_active.lock"
YIELD_WINDOW=10
mkdir -p "$RUN_DIR" /var/lock
is_terminal_active() {
if [ -f "$TERMINAL_LOCK" ]; then
local lock_mtime=$(date -r "$TERMINAL_LOCK" +%s 2>/dev/null || echo 0)
local now=$(date +%s)
local diff=$((now - lock_mtime))
if [ "$diff" -ge 0 ] && [ "$diff" -le "$YIELD_WINDOW" ]; then
return 0
fi
fi
return 1
}
collect_single() {
if is_terminal_active; then
return 0
fi
[ -f "$CACHE_DEVICES" ] || /usr/libexec/modem-detect --cache >/dev/null 2>&1
# Check user manually configured port from UCI first
local user_port=$(uci -q get modem-manager.global.port || uci -q get modem-manager.default.port)
ucode -e '
import * as fs from "fs";
let m = loadfile("/usr/share/rpcd/ucode/luci.modem")();
let plugin = m["luci.modem"];
let devs_data = plugin.get_devices.call();
let dev = (devs_data?.devices && length(devs_data.devices) > 0) ? devs_data.devices[0] : null;
let user_port = "'"$user_port"'";
let target_port = (user_port && user_port != "") ? user_port : (dev?.primary_port || null);
if (target_port) {
let status = plugin.collect_raw_status.call({ args: { port: target_port, primary_net: dev?.primary_net || "usb0" } });
if (status) {
let tmp = "/tmp/run/modem-manager/status.json.tmp";
fs.writefile(tmp, sprintf("%J\n", status));
fs.rename(tmp, "/tmp/run/modem-manager/status.json");
}
}
' 2>/dev/null || true
}
while true; do
collect_single
INTERVAL=$(uci -q get modem-manager.global.check_interval || echo 15)
[ "$INTERVAL" -ge 5 ] 2>/dev/null || INTERVAL=15
sleep "$INTERVAL"
done
+189
View File
@@ -0,0 +1,189 @@
#!/bin/sh
# Cellular Modem Auto-Detector for OpenWrt
# Outputs discovered modems in JSON format and updates /tmp/run/modem-manager/devices.json
RUN_DIR="/tmp/run/modem-manager"
CACHE_FILE="$RUN_DIR/devices.json"
KNOWN_VENDORS="2c7c 2cb7 1e0e 12d1 19d2 1bbb 05c6 413c 0bdb 1199 2977 1508 1bc7 0e8d 1782 0489 1fac 2dee 33f8 3731 2df3 20a6"
get_sys_attr() {
local path="$1"
if [ -f "$path" ]; then
tr -d '\r\n' < "$path" 2>/dev/null
fi
}
detect_all() {
echo "{"
echo ' "devices": ['
local FIRST=1
for dev in /sys/bus/usb/devices/*; do
[ -d "$dev" ] || continue
[ -f "$dev/idVendor" ] || continue
[ -f "$dev/idProduct" ] || continue
local VID=$(get_sys_attr "$dev/idVendor")
local PID=$(get_sys_attr "$dev/idProduct")
local DEVCLASS=$(get_sys_attr "$dev/bDeviceClass")
case "$VID" in
1d6b|2109|1d50) continue ;;
esac
[ "$DEVCLASS" = "09" ] && continue
local IS_MODEM=0
for kv in $KNOWN_VENDORS; do
if [ "$VID" = "$kv" ]; then
IS_MODEM=1
break
fi
done
local DEVNAME=$(basename "$dev")
# Direct sysfs tree discovery for serial ports under this USB device
local TTYS=""
for t in $(find "$dev" -maxdepth 5 \( -name "ttyUSB*" -o -name "ttyACM*" \) 2>/dev/null); do
local b=$(basename "$t")
case "$b" in
ttyUSB*|ttyACM*)
if [ -e "/dev/$b" ]; then
case " $TTYS " in
*" /dev/$b "*) ;;
*) TTYS="$TTYS /dev/$b" ;;
esac
fi
;;
esac
done
# Fallback tty search via /sys/class/tty
if [ -z "$TTYS" ]; then
for t in /sys/class/tty/ttyUSB* /sys/class/tty/ttyACM*; do
[ -e "$t" ] || continue
local t_target=$(readlink "$t" 2>/dev/null || readlink -f "$t" 2>/dev/null)
case "$t_target" in
*"$DEVNAME"*)
local b=$(basename "$t")
[ -e "/dev/$b" ] && case " $TTYS " in *" /dev/$b "*) ;; *) TTYS="$TTYS /dev/$b" ;; esac
;;
esac
done
fi
TTYS=$(echo "$TTYS" | sed 's/^ *//')
# Network interfaces discovery under this USB device
local NETS=""
for n in $(find "$dev" -maxdepth 5 -name "net" 2>/dev/null); do
[ -d "$n" ] || continue
for iface in "$n"/*; do
[ -d "$iface" ] || continue
local b=$(basename "$iface")
case "$b" in
lo|br-*|eth*|lan*|wan*|sfp*) ;;
*) case " $NETS " in *" $b "*) ;; *) NETS="$NETS $b" ;; esac ;;
esac
done
done
# Fallback net search via /sys/class/net
if [ -z "$NETS" ]; then
for n in /sys/class/net/*; do
[ -d "$n" ] || continue
local n_target=$(readlink "$n" 2>/dev/null || readlink -f "$n" 2>/dev/null)
case "$n_target" in
*"$DEVNAME"*)
local b=$(basename "$n")
case "$b" in
lo|br-*|eth*|lan*|wan*|sfp*) ;;
*) case " $NETS " in *" $b "*) ;; *) NETS="$NETS $b" ;; esac ;;
esac
;;
esac
done
fi
NETS=$(echo "$NETS" | sed 's/^ *//')
if [ -n "$TTYS" ] || [ -n "$NETS" ] || [ $IS_MODEM -eq 1 ]; then
IS_MODEM=1
fi
[ $IS_MODEM -eq 1 ] || continue
local MFG=$(get_sys_attr "$dev/manufacturer")
local PROD=$(get_sys_attr "$dev/product")
local SER=$(get_sys_attr "$dev/serial")
local PRIMARY_TTY=""
for t in $TTYS; do
case "$t" in
*ttyUSB2|*ttyACM0)
PRIMARY_TTY="$t"
break
;;
esac
done
[ -z "$PRIMARY_TTY" ] && PRIMARY_TTY=$(echo "$TTYS" | awk '{print $1}')
local PRIMARY_NET=$(echo "$NETS" | awk '{print $1}')
local DRIVER="option"
for drv in $(find "$dev" -name "driver" 2>/dev/null); do
local drv_target=$(basename "$(readlink -f "$drv" 2>/dev/null)")
case "$drv_target" in
cdc_mbim|cdc_ncm|qmi_wwan|cdc_ether|rndis_host)
DRIVER="$drv_target"
break
;;
esac
done
[ $FIRST -eq 1 ] || echo ","
FIRST=0
local TTY_JSON=""
for t in $TTYS; do
[ -n "$TTY_JSON" ] && TTY_JSON="$TTY_JSON, "
TTY_JSON="$TTY_JSON\"$t\""
done
local NET_JSON=""
for n in $NETS; do
[ -n "$NET_JSON" ] && NET_JSON="$NET_JSON, "
NET_JSON="$NET_JSON\"$n\""
done
cat <<EOF
{
"id": "$DEVNAME",
"vid": "$VID",
"pid": "$PID",
"vendor": "${MFG:-Generic}",
"model": "${PROD:-Modem}",
"serial": "${SER}",
"driver": "$DRIVER",
"primary_port": "$PRIMARY_TTY",
"primary_net": "$PRIMARY_NET",
"ports": [ $TTY_JSON ],
"nets": [ $NET_JSON ]
}
EOF
done
echo " ]"
echo "}"
}
mkdir -p "$RUN_DIR"
OUTPUT=$(detect_all)
# Atomic update cache
echo "$OUTPUT" > "${CACHE_FILE}.tmp" && mv -f "${CACHE_FILE}.tmp" "$CACHE_FILE"
# Output to stdout if not purely --cache mode
if [ "$1" != "--cache" ]; then
echo "$OUTPUT"
fi
exit 0