155 lines
5.7 KiB
Python
155 lines
5.7 KiB
Python
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}")
|