Saba-python/secure_sms/infrastructure/gsm.py

171 lines
6.1 KiB
Python

import abc
import re
import threading
import time
from typing import Callable, Optional
import serial
class IMessageGateway(abc.ABC):
@property
@abc.abstractmethod
def is_connected(self) -> bool: pass
@abc.abstractmethod
def connect(self) -> bool: pass
@abc.abstractmethod
def disconnect(self) -> None: pass
@abc.abstractmethod
def send_frames(self, phone: str, frames: list[str]) -> bool: pass
class GSMGateway(IMessageGateway):
def __init__(
self,
port: str,
baudrate: int,
message_callback: Optional[Callable[[str, str], None]] = None,
):
self.port = port
self.baudrate = baudrate
self.message_callback = message_callback
self.serial_conn = None
self.is_running = False
self.read_thread = None
self.lock = threading.Lock()
@property
def is_connected(self) -> bool:
return bool(self.serial_conn and self.serial_conn.is_open)
def connect(self) -> bool:
try:
self.serial_conn = serial.Serial(
port=self.port,
baudrate=self.baudrate,
timeout=1,
)
self.is_running = True
self.send_at_cmd("AT")
self.send_at_cmd("AT+CMGF=1")
self.send_at_cmd('AT+CNMI=2,1,0,0,0')
self.read_thread = threading.Thread(target=self._read_loop, daemon=True)
self.read_thread.start()
return True
except Exception:
self.serial_conn = None
self.is_running = False
return False
def disconnect(self):
self.is_running = False
if self.serial_conn and self.serial_conn.is_open:
self.serial_conn.close()
if self.read_thread:
self.read_thread.join(timeout=1.5)
def send_at_cmd(self, command: str, expected_response: str = "OK", timeout: float = 2.0) -> tuple[bool, list[str]]:
with self.lock:
if not self.is_connected:
return False, ["offline"]
self.serial_conn.reset_input_buffer()
self.serial_conn.write((command + "\r\n").encode("ascii"))
start = time.time()
lines = []
while time.time() - start < timeout:
if self.serial_conn.in_waiting:
line = self.serial_conn.readline().decode("ascii", errors="ignore").strip()
if line:
lines.append(line)
if expected_response in line or "ERROR" in line:
break
else:
time.sleep(0.05)
return expected_response in "\n".join(lines), lines
def send_frames(self, phone: str, frames: list[str]) -> bool:
if not self.is_connected:
return False
for frame in frames:
if not self._send_single_sms(phone, frame):
return False
time.sleep(0.8)
return True
def _send_single_sms(self, phone: str, body: str) -> bool:
with self.lock:
try:
self.serial_conn.reset_input_buffer()
self.serial_conn.write(f'AT+CMGS="{phone}"\r\n'.encode("ascii"))
start = time.time()
prompt_ready = False
while time.time() - start < 2:
if self.serial_conn.in_waiting:
char = self.serial_conn.read().decode("ascii", errors="ignore")
if char == ">":
prompt_ready = True
break
time.sleep(0.05)
if not prompt_ready:
self.serial_conn.write(chr(27).encode("ascii"))
return False
self.serial_conn.write(body.encode("ascii") + chr(26).encode("ascii"))
start = time.time()
while time.time() - start < 10:
if self.serial_conn.in_waiting:
line = self.serial_conn.readline().decode("ascii", errors="ignore").strip()
if "OK" in line:
return True
if "ERROR" in line:
return False
else:
time.sleep(0.1)
except Exception:
return False
return False
def _read_loop(self):
while self.is_running:
try:
if self.is_connected and self.serial_conn.in_waiting and self.lock.acquire(blocking=False):
try:
line = self.serial_conn.readline().decode("ascii", errors="ignore").strip()
if line.startswith("+CMTI:"):
match = re.search(r'\+CMTI:\s*".*?",(\d+)', line)
if match:
index = int(match.group(1))
threading.Thread(
target=self._process_incoming_sms,
args=(index,),
daemon=True,
).start()
finally:
self.lock.release()
except Exception:
pass
time.sleep(0.1)
def _process_incoming_sms(self, index: int):
time.sleep(0.7)
ok, lines = self.send_at_cmd(f"AT+CMGR={index}", expected_response="OK", timeout=3)
if not ok:
return
sender = "ناشناس"
body_lines = []
reading_body = False
for line in lines:
if line.startswith("+CMGR:"):
parts = line.split(",")
if len(parts) >= 2:
sender = parts[1].strip('"')
reading_body = True
continue
if reading_body and line not in {"OK", "ERROR"}:
body_lines.append(line)
if self.message_callback:
self.message_callback(sender, "\n".join(body_lines))
self.send_at_cmd(f"AT+CMGD={index}")