Files
simd/shell/send_sms.sh

374 lines
16 KiB
Bash
Executable File
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#!/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; }
# ═════════════════════════════════════════════════════════════════
# РАЗБОР АРГУМЕНТОВ
# ═════════════════════════════════════════════════════════════════
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 это БАЙТЫ, не символы
}
# ══════════════════════════════════════════════════════════════════
# СБОРКА 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
(( 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"
SMSC_PART=""
if (( USE_SMSC )); then
build_smsc "$SMSC" 91
SMSC_PART="$SMSC_HEX"
fi
# [FO][MR][DA][PID][VP?][DCS][UDL][UD]
TPDU="${FO}${MR_HEX}${DA_PART}00${VP_HEX}${DCS}${UDL_HEX}${TEXT_HEX}"
TPDU_LEN=$(( ${#TPDU} / 2 ))
FULL_PDU="${SMSC_PART}${TPDU}"
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