Files
simd/shell/send_sms.sh

374 lines
16 KiB
Bash
Raw Normal View History

2026-10-25 13:35:24 +03:00
#!/bin/bash
# send_sms.sh — SMS PDU constructor, bitwise fields
# Зависимости: bash, stty, grep, iconv, xxd (для --ucs2)
set -u
# ══════════════════════════════════════════════════════════════════
# ЗНАЧЕНИЯ ПО УМОЛЧАНИЮ
# ══════════════════════════════════════════════════════════════════
PORT="/dev/ttyUSB2"
SMSC="+79585370690"
LOG="/pkg/gnu/unitprint/simd/var/modem_sms.log"
ENCODING="gsm" # gsm | ucs2
CLASS="" # "" = не указан (класс не задан), 0..3
MR=0 # TP-Message-Reference 0..255
REPORT=0 # TP-SRR: 1 = запрашивать delivery report
UDHI=0 # TP-UDHI: 1 = в тексте есть UDH (concatenation)
RP=0 # TP-RP: request reply path
RD=0 # TP-RD: reject duplicates
VALIDITY_MIN="" # TP-VP, минуты (относительный формат)
USE_SMSC=1 # 0 = не включать SMSC в PDU (возьмётся из AT+CSCA)
TOA=91 # 91 = international (+), 81 = national
DRY_RUN=0
RECEIVER=""
TEXT=""
# ══════════════════════════════════════════════════════════════════
# USAGE
# ══════════════════════════════════════════════════════════════════
usage() {
cat <<'EOF'
Использование:
send_sms.sh [ОПЦИИ] <НОМЕР> [ТЕКСТ]
Если ТЕКСТ не задан — генерируется "OTP <6 случайных цифр>".
Кодировка (взаимоисключающие):
--gsm GSM 7-bit, по умолчанию (латиница/цифры)
--ucs2 UCS2 / UTF-16BE (кириллица и др.)
--encoding=gsm|ucs2 то же самое
Класс сообщения (DCS):
--class=0..3 0=Flash, 1=ME, 2=SIM, 3=TE (без явного указания
класс НЕ передаётся → обычный SMS)
--flash синоним --class=0
Заголовок TPDU (FO):
--report TP-SRR=1, запрос отчёта о доставке (медленнее!)
--udhi TP-UDHI=1, в тексте есть UDH
--reply-path TP-RP=1
--reject-duplicates TP-RD=1
--mr=N TP-MR, 0..255 (по умолчанию 0)
--validity=MIN TP-VP, срок жизни в минутах (до 10080 = 7 суток)
Адресация / транспорт:
--port=/dev/ttyUSB2 порт модема
--smsc=+7... SMSC (по умолчанию из конфига)
--no-smsc не включать SMSC в PDU (используется AT+CSCA)
--national TOA=81, НОМЕР передавать без '+'
--log=/path файл для сырых ответов модема
Прочее:
--dry-run напечатать PDU и выйти, ничего не отправлять
-h, --help эта справка
EOF
}
die() { echo "ERROR: $*" >&2; exit 1; }
# ═════════════════════════════════════════════════════════════════
2026-10-25 13:35:24 +03:00
# РАЗБОР АРГУМЕНТОВ
# ═════════════════════════════════════════════════════════════════
2026-10-25 13:35:24 +03:00
POSITIONAL=()
while (( $# > 0 )); do
case "$1" in
-h|--help) usage; exit 0 ;;
--gsm) ENCODING="gsm" ;;
--ucs2) ENCODING="ucs2" ;;
--encoding=*) ENCODING="${1#*=}" ;;
--encoding) shift; ENCODING="${1:-}" ;;
--class=*) CLASS="${1#*=}" ;;
--class) shift; CLASS="${1:-}" ;;
--flash) CLASS="0" ;;
--mr=*) MR="${1#*=}" ;;
--report) REPORT=1 ;;
--udhi) UDHI=1 ;;
--reply-path) RP=1 ;;
--reject-duplicates) RD=1 ;;
--validity=*) VALIDITY_MIN="${1#*=}" ;;
--port=*) PORT="${1#*=}" ;;
--smsc=*) SMSC="${1#*=}" ;;
--log=*) LOG="${1#*=}" ;;
--no-smsc) USE_SMSC=0 ;;
--national) TOA=81 ;;
--dry-run) DRY_RUN=1 ;;
--*) die "неизвестный флаг: $1 (см. --help)" ;;
*) POSITIONAL+=("$1") ;;
esac
shift
done
(( ${#POSITIONAL[@]} >= 1 )) || { usage; exit 1; }
RECEIVER="${POSITIONAL[0]}"
RAND_NUM=$(printf '%06d' $(( RANDOM % 1000000 )))
TEXT="${POSITIONAL[1]:-OTP $RAND_NUM}"
[[ "$ENCODING" == "gsm" || "$ENCODING" == "ucs2" ]] || die "--encoding: только gsm | ucs2"
[[ "$CLASS" == "" || "$CLASS" =~ ^[0-3]$ ]] || die "--class: только 0..3"
[[ "$MR" =~ ^[0-9]+$ && "$MR" -le 255 ]] || die "--mr: 0..255"
if [[ -n "$VALIDITY_MIN" ]]; then
[[ "$VALIDITY_MIN" =~ ^[0-9]+$ ]] || die "--validity: целые минуты"
(( VALIDITY_MIN >= 1 && VALIDITY_MIN <= 10080 )) || die "--validity: 1..10080 минут"
fi
if [[ "$TOA" == "81" ]]; then
[[ "$RECEIVER" =~ ^[0-9]{5,15}$ ]] || die "--national: номер без '+' и символов"
else
[[ "$RECEIVER" =~ ^\+[0-9]{7,15}$ ]] || die "номер должен быть в формате +7XXXXXXXXXX"
fi
# ══════════════════════════════════════════════════════════════════
# BITWISE: TP-DCS (3GPP TS 23.038)
# ══════════════════════════════════════════════════════════════════
build_dcs() {
local alpha=0
[[ "$ENCODING" == "ucs2" ]] && alpha=2 # 10b
local dcs=0
if [[ -n "$CLASS" ]]; then
dcs=$(( dcs | (1 << 4) )) # bit 4: класс задан
dcs=$(( dcs | (CLASS << 2) )) # bits 3-2: класс
dcs=$(( dcs | alpha )) # bits 1-0: алфавит
else
dcs=$(( dcs | (alpha << 2) )) # bits 3-2: алфавит
fi
printf '%02X' "$dcs"
}
# ══════════════════════════════════════════════════════════════════
# BITWISE: TP-FO (3GPP TS 23.040, SMS-SUBMIT)
# ══════════════════════════════════════════════════════════════════
build_fo() {
local fo=1 # bits 1-0: MTI = 01
(( RD )) && fo=$(( fo | (1 << 2) ))
[[ -n "$VALIDITY_MIN" ]] && fo=$(( fo | (2 << 3) )) # VPF = 10
(( REPORT )) && fo=$(( fo | (1 << 5) ))
(( UDHI )) && fo=$(( fo | (1 << 6) ))
(( RP )) && fo=$(( fo | (1 << 7) ))
printf '%02X' "$fo"
}
# TP-VP relative
build_vp() {
local m="$1" v
if (( m <= 720 )); then v=$(( (m + 4) / 5 - 1 ))
elif (( m <= 1440 )); then v=$(( 143 + (m - 720 + 29) / 30 ))
else v=$(( 166 + (m + 1439) / 1440 ))
fi
(( v < 0 )) && v=0
(( v > 196 )) && v=196
printf '%02X' "$v"
}
# ══════════════════════════════════════════════════════════════════
# НОМЕР → TON/NPI + swapped nibbles
# ══════════════════════════════════════════════════════════════════
format_number() {
local num="${1#+}" out="" i
(( ${#num} % 2 )) && num="${num}F"
for (( i=0; i<${#num}; i+=2 )); do
out+="${num:i+1:1}${num:i:1}"
done
printf '%s' "$out"
}
# Для DA (получатель) длина — это КОЛИЧЕСТВО ЦИФР (без '+')
build_da() {
local num="${1#+}"
local digits=${#num}
local len_hex=$(printf '%02X' "$digits")
local num_hex=$(format_number "$1")
DA_HEX="${len_hex}${2}${num_hex}"
}
# Для SMSC длина — это КОЛИЧЕСТВО ОКТЕТОВ (включая байт TOA)
build_smsc() {
local num="${1#+}"
local digits=${#num}
local octets=$(( (digits + 1) / 2 + 1 ))
local len_hex=$(printf '%02X' "$octets")
local num_hex=$(format_number "$1")
SMSC_HEX="${len_hex}${2}${num_hex}"
}
# ══════════════════════════════════════════════════════════════════
# GSM 7-bit / UCS2
# ══════════════════════════════════════════════════════════════════
declare -a SEPTETS
TEXT_HEX=""
build_gsm7() {
local text="$1" i code ch
SEPTETS=()
for (( i=0; i<${#text}; i++ )); do
ch="${text:i:1}"
printf -v code '%d' "'$ch"
if (( code > 127 )); then
die "символ '$ch' не в GSM 7-bit. Используйте --ucs2"
fi
case $code in
64) SEPTETS+=(0) ;; # @
36) SEPTETS+=(2) ;; # $
95) SEPTETS+=(17) ;; # _
10) SEPTETS+=(10) ;;
13) SEPTETS+=(13) ;;
123|125|91|93|126|92|124|8364)
die "расширенный GSM-алфавит ('$ch') пока не поддержан, используйте --ucs2" ;;
*) SEPTETS+=("$code") ;;
esac
done
local n=${#SEPTETS[@]}
local oct_count=$(( (n * 7 + 7) / 8 ))
local -a oct=()
for (( i=0; i<oct_count; i++ )); do oct[i]=0; done
local s v bp bi bo
for (( s=0; s<n; s++ )); do
v=${SEPTETS[$s]}; bp=$(( s * 7 )); bi=$(( bp / 8 )); bo=$(( bp % 8 ))
oct[$bi]=$(( (oct[$bi] | ((v & 0x7F) << bo)) & 0xFF ))
if (( bo > 1 )) && (( bi + 1 < oct_count )); then
oct[$((bi+1))]=$(( oct[$((bi+1))] | (v >> (8 - bo)) ))
fi
done
TEXT_HEX=""
for (( i=0; i<oct_count; i++ )); do
TEXT_HEX+="$(printf '%02X' "${oct[$i]}")"
done
UDL=$n
}
build_ucs2() {
# iconv → xxd напрямую, без сохранения в переменную (bash не держит \x00)
TEXT_HEX=$(printf '%s' "$1" | iconv -f UTF-8 -t UTF-16BE 2>/dev/null \
| xxd -p | tr -d '\n' | tr 'a-f' 'A-F')
if [[ -z "$TEXT_HEX" ]]; then
die "iconv: не удалось сконвертировать текст в UTF-16BE"
fi
UDL=$(( ${#TEXT_HEX} / 2 )) # для UCS2 это БАЙТЫ, не символы
2026-10-25 13:35:24 +03:00
}
# ══════════════════════════════════════════════════════════════════
# СБОРКА PDU
# ══════════════════════════════════════════════════════════════════
[[ -n "$TEXT" ]] || die "пустой текст"
if [[ "$ENCODING" == "gsm" ]]; then build_gsm7 "$TEXT"; else build_ucs2 "$TEXT"; fi
# ══════════════════════════════════════════════════════════════════
# ПРОВЕРКА ДЛИНЫ (ИСПРАВЛЕНО)
# ══════════════════════════════════════════════════════════════════
if [[ "$ENCODING" == "ucs2" ]]; then
MAX_UDL=140 # Лимит в БАЙТАХ для UCS2 (140 байт = 70 символов)
else
MAX_UDL=160 # Лимит в СЕПТЕТАХ для GSM 7-bit
fi
2026-10-25 13:35:24 +03:00
(( UDL <= MAX_UDL )) || die "текст длиннее одного SMS ($UDL > $MAX_UDL). Нужна конкатенация/UDH"
FO=$(build_fo)
DCS=$(build_dcs)
MR_HEX=$(printf '%02X' "$MR")
UDL_HEX=$(printf '%02X' "$UDL")
VP_HEX=""
[[ -n "$VALIDITY_MIN" ]] && VP_HEX=$(build_vp "$VALIDITY_MIN")
build_da "$RECEIVER" "$TOA"
DA_PART="$DA_HEX"
2026-10-25 13:35:24 +03:00
SMSC_PART=""
2026-10-25 13:35:24 +03:00
if (( USE_SMSC )); then
build_smsc "$SMSC" 91
SMSC_PART="$SMSC_HEX"
2026-10-25 13:35:24 +03:00
fi
# [FO][MR][DA][PID][VP?][DCS][UDL][UD]
TPDU="${FO}${MR_HEX}${DA_PART}00${VP_HEX}${DCS}${UDL_HEX}${TEXT_HEX}"
2026-10-25 13:35:24 +03:00
TPDU_LEN=$(( ${#TPDU} / 2 ))
FULL_PDU="${SMSC_PART}${TPDU}"
2026-10-25 13:35:24 +03:00
echo "FO=$FO MR=$MR_HEX PID=00 DCS=$DCS UDL=$UDL_HEX VP=${VP_HEX:-none}"
echo "CLASS=${CLASS:-не задан} ENCODING=$ENCODING"
echo "ТЕКСТ : $TEXT"
echo "TPDU : $TPDU_LEN байт"
echo "PDU : $FULL_PDU"
(( DRY_RUN )) && exit 0
# ══════════════════════════════════════════════════════════════════
# ТРАНСПОРТ: cat-ридер + ожидание маркеров
# ══════════════════════════════════════════════════════════════════
READER_PID=""
cleanup() {
[[ -n "$READER_PID" ]] && kill "$READER_PID" 2>/dev/null
wait "$READER_PID" 2>/dev/null
}
trap cleanup EXIT INT TERM
mkdir -p "$(dirname "$LOG")" 2>/dev/null
: > "$LOG"
stty -F "$PORT" 115200 raw -echo 2>/dev/null || true
cat "$PORT" >> "$LOG" 2>/dev/null &
READER_PID=$!
wait_in_log() {
local timeout_ms="$1" pattern="$2"
local deadline=$(( $(date +%s%N) / 1000000 + timeout_ms ))
while (( $(date +%s%N) / 1000000 < deadline )); do
grep -aqF -- "$pattern" "$LOG" 2>/dev/null && return 0
sleep 0.01
done
return 1
}
send_wait() {
: > "$LOG"
printf '%s\r' "$1" > "$PORT"
local deadline=$(( $(date +%s%N) / 1000000 + ${2:-3000} ))
while (( $(date +%s%N) / 1000000 < deadline )); do
if grep -aqF "OK" "$LOG" 2>/dev/null; then return 0; fi
if grep -aqF "ERROR" "$LOG" 2>/dev/null; then return 1; fi
sleep 0.01
done
return 1
}
# Инициализация
send_wait 'AT' || die "модем не отвечает (AT)"
send_wait 'AT+CMEE=2' || die "AT+CMEE=2"
send_wait 'AT+CMGF=0' || die "AT+CMGF=0"
(( USE_SMSC )) || send_wait 'AT+CSCA?' || die "AT+CSCA? — SMSC не задан"
# Отправка
: > "$LOG"
START=$(date +%s%N)
printf 'AT+CMGS=%d\r' "$TPDU_LEN" > "$PORT"
if ! wait_in_log 5000 '>'; then
echo "ERROR: нет приглашения '>'"; cat "$LOG"; exit 1
fi
printf '%s\x1A' "$FULL_PDU" > "$PORT"
# Ждём либо успех, либо ошибку
DEADLINE=$(( $(date +%s%N) / 1000000 + 30000 ))
RC=2
while (( $(date +%s%N) / 1000000 < DEADLINE )); do
if grep -aqF '+CMGS:' "$LOG"; then RC=0; break; fi
if grep -aqF 'CMS ERROR' "$LOG"; then RC=1; break; fi
sleep 0.01
done
ELAPSED=$(( ($(date +%s%N) - START) / 1000000 ))
case "$RC" in
0) echo "OK: отправлено за ${ELAPSED} мс"; grep -a '+CMGS:' "$LOG"; exit 0 ;;
1) echo "ERROR (${ELAPSED} мс):"; grep -a 'CMS ERROR' "$LOG"; exit 1 ;;
*) echo "TIMEOUT (${ELAPSED} мс)"; cat "$LOG"; exit 2 ;;
esac