import re import threading import time from typing import Callable, Optional import serial class GSMGateway: 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}")