#!/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 1 )) && (( bi + 1 < oct_count )); then oct[$((bi+1))]=$(( oct[$((bi+1))] | (v >> (8 - bo)) )) fi done TEXT_HEX="" for (( i=0; i/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