From 34dbe1d909a890c98d8194967ad957af8881ccfb Mon Sep 17 00:00:00 2001 From: PAN Lei Date: Thu, 24 Feb 2022 17:17:39 +0100 Subject: [PATCH] can restart modemPool and detected disabled sim card --- .gitignore | 2 +- ModemPool.py | 72 +++++++++ SIMError.py | 5 + appointment.json | 12 ++ card_pool.py | 51 ++++++ commandor.py | 6 +- db/DbManager.py | 42 +++++ db/__init__.py | 0 main.py | 155 ++++++++++++++----- pojo/ReserveResultPojo.py | 29 ++++ pojo/__pycache__/__init__.cpython-39.pyc | Bin 150 -> 0 bytes pojo/__pycache__/contact_pojo.cpython-39.pyc | Bin 751 -> 0 bytes pojo/contact_pojo.py | 12 ++ excel_reader.py => utils/excel_reader.py | 16 +- utils/logging.py | 2 +- utils/message_receiver.py | 27 ++++ utils/phone_list.xlsx | Bin 0 -> 4926 bytes 17 files changed, 382 insertions(+), 49 deletions(-) create mode 100644 ModemPool.py create mode 100644 SIMError.py create mode 100644 appointment.json create mode 100644 card_pool.py create mode 100644 db/DbManager.py create mode 100644 db/__init__.py create mode 100644 pojo/ReserveResultPojo.py delete mode 100644 pojo/__pycache__/__init__.cpython-39.pyc delete mode 100644 pojo/__pycache__/contact_pojo.cpython-39.pyc rename excel_reader.py => utils/excel_reader.py (74%) create mode 100644 utils/message_receiver.py create mode 100644 utils/phone_list.xlsx diff --git a/.gitignore b/.gitignore index d78d7e0..c34e7ae 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,4 @@ /.idea/ __pycache__ -pojo/__pycache__ +pojo/__pycache__/ .~contact.xlsx \ No newline at end of file diff --git a/ModemPool.py b/ModemPool.py new file mode 100644 index 0000000..0dcdcd3 --- /dev/null +++ b/ModemPool.py @@ -0,0 +1,72 @@ +import re +import time + +import serial +from serial import Serial + +from utils.excel_reader import ExcelHelper + + +class ModemPool: + BAUDRATE = 115200 + my_phone = "my_phone" + phone_number_position = 10 + + def __init__(self, port_list: list): + self._port_list = port_list + self._serial_list = [] + self._excel_helper = ExcelHelper() + for port in self._port_list: + ser = serial.Serial(port, self.BAUDRATE, timeout=1) + self._serial_list.append(ser) + + def reset_all_modems(self): + for ser in self._serial_list: + self._send_command("AT+CFUN=1,1\r", ser) + # send_command("AT+RESET\r", ser) + # wait for 20 second, so that the modem can init all the sims + time.sleep(20) + + def get_raw_phone_number(self, slot_position): + for index, ser in enumerate(self._serial_list): + print("will get phone number for slot({}) SIM({}), port:{}".format(slot_position, index + 1, ser.port)) + self._select_sim_storage(ser) + msg = self._execut_USSD_cmd("AT+CUSD=1, *132#\r", ser) + if "Unfortunately" in str(msg): + print("error for for slot({}) SIM({}), port:{}".format(slot_position, index + 1, ser.port)) + return + # find phone number + match = re.search(r'33\d{9}', str(msg)) + phone_number = match.group(0) + print("phone is " + phone_number) + if phone_number: + self._excel_helper.write_phone(phone_number) + # write the number to sim card's phonebook + cmd = f'AT+CPBW={self.phone_number_position},\"{phone_number}\"\r' + self._send_command(cmd, ser, wait_time_in_s=2) + self.get_own_number(ser) + + def get_own_number(self, ser: Serial): + print("saved phone number: " + str(self._send_command(f'AT+CPBR={self.phone_number_position}\r', ser))) + + def _select_sim_storage(self, ser): + # use SIM Card storage + cmd_sm = "AT+CPBS=\"SM\"\r" + self._send_command(cmd_sm, ser) + + def _send_command(self, cmd: str, ser, wait_time_in_s: int = 0) -> bytes: + print("send command {}".format(cmd)) + ser.write(cmd.encode()) + msg = ser.read(100) + count = 0 + while 'OK' not in str(msg) and count < wait_time_in_s: + time.sleep(1) + count = count + 1 + msg = ser.read(100) + # msg = ser.read(100) + print(msg) + return msg + + def _execut_USSD_cmd(self, cmd, ser) -> str: + # the timeout for ussd command can be 120 s in mac + return str(self._send_command(cmd, ser, 120)) diff --git a/SIMError.py b/SIMError.py new file mode 100644 index 0000000..64f0f60 --- /dev/null +++ b/SIMError.py @@ -0,0 +1,5 @@ +from enum import Enum + + +class SIMError(Enum): + SIM_DISABLED = "SIM_DISABLED" diff --git a/appointment.json b/appointment.json new file mode 100644 index 0000000..c93daab --- /dev/null +++ b/appointment.json @@ -0,0 +1,12 @@ +{ + "type": "service_account", + "project_id": "appointment-ba40a", + "private_key_id": "9906c564d9dd2e3fce879eebe94dfb47d4cd65a0", + "private_key": "-----BEGIN PRIVATE KEY-----\nMIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQDN2FTfsJYwwLzr\n+08udYl4IRzLss1ZLMp4gPjFjkawMzoEGPjrgeeqAgLk9faJGp6NiRciP5JRffd/\nNVvt0KnfmMjBNnMtyXqT/mCCTY/LPZag1cKkMHAuzHXwVXz7DNnndr6k3LQ5ixe/\nXgdUgcMKC/p6OPBGZB4OS+3Xt8N9gYsrkk9fi+wlJ/nzPVjHEVMFtYO1RngBpNmc\n9EOcHZzaM7+2G+hguJBG9IoNOCKVcG9xR732G+njkfUarFdK9FhT5V4dh/dE2/pT\nZrsrg2Bf4wShrWxjzSrX1+7oM0tPonmwaw19GfWnp6cpo8V1C27u3mRBCugqwzLu\nEvXDC/hrAgMBAAECggEABRyfsvRkLm7C4ktZ4on5sXmFCQv2LIY/uvFc/C71029a\nO/rQx6xwr9if8Mao6iu2j0Y9xFR20j5CFK8jCstZRJu7NI0hHBx6Rk2VYPcDIKV1\nZaYZUNGBH7BlJ2RAF83wZV6eCmMOuLUbEF4J6Y/VY5z7ieh7EwxucKVzER3XpXi0\nMMfXlJIpaUrg6/llwPRdPVdCFmNrtNi1D8exNS0T1/mt/CMOg13ZEkSJcaD7NOju\nckiU+IXigmLbADOcZ4ieGqO8rVfJsr1/y7EbVJoJjRtQq56tFT+G2mIJ5StCS0+u\nW2qsoPC3hGBM04OPsprsXjFzPgaItghDyB0zzfS0/QKBgQDloKf+qHP8LH+ot5qN\nEwo6ysRYzduCY0Bw4nDx5tUq7mTsvzbbeLqwE6x52y1BvhpKxqZMMZ+o9IlNL9wq\nJztHeuSqheBPl+43j5bYQrcqaG42ZdjrSXjO8a4KDWL1Ri2vz+QgWzarGLRY+0ni\nvasr2/mg14CPBfJSgSwH5oEx7QKBgQDlfHBaj1aLTwvaLsQbUYqkgaWtM3goTgb9\nTA0Ivkhc/gz2Yo6/6/IqvZtRhq4FRbyk/JPgfNwKx/eHRO0uS+igVxnRs7ziL3Qq\nPGUoPouJtE/MHtVCRlrzEz8Br5yXYfB5zDsaLgYjnOZgED8cp2Qy2pwuYLA4LgWF\nhu/oaO1otwKBgGPauBMqh71qUF068k9Ur0cfs8B2THVn2bb9EWZwHdScdHDrOdy4\npF47P+6BnC2RkHdh6SELF0XuiOJy5IfEJagQze1FaGTUSbgJjewfHu2nGf43zduL\nSKidOjSO27CTQvzIJ4jWgXBnvs1PATNDjXL2JpiF/haz3Et6dn49A4OFAoGBALyV\nZj8FS7lvW+4QQFeyypwlbmDGyxdUB6pftNZaiFzi6QQQOf69hmRZLCny406x4DQ0\n29C+ypSRf3hJzB8fgitBaJZLfgzhsjSDLR3FSCYBZxH2xImSB2t5hW19QtGkSlnM\n20TITYM2jJqkvzhs1opz26TBEA8awq7YFI6Iq5BBAoGBANFxoCqM6TmlMF32wRcW\nccLz7GiInHSsVvhORZW8fIVdgVWNIo4Xon83WmRBCOHr0QoViHlJqNTZZDKwXlc5\nKl5sdfIlKPLVsthQrV6PyQhRMH0BRYjldiqMSKe9Q3v0SjHDbOVqG9C5pRIqWuAE\nALQsXpH7Ze0e5amwGYJUYBeG\n-----END PRIVATE KEY-----\n", + "client_email": "appointment-ba40a@appspot.gserviceaccount.com", + "client_id": "117807159722779924783", + "auth_uri": "https://accounts.google.com/o/oauth2/auth", + "token_uri": "https://oauth2.googleapis.com/token", + "auth_provider_x509_cert_url": "https://www.googleapis.com/oauth2/v1/certs", + "client_x509_cert_url": "https://www.googleapis.com/robot/v1/metadata/x509/appointment-ba40a%40appspot.gserviceaccount.com" +} diff --git a/card_pool.py b/card_pool.py new file mode 100644 index 0000000..b42428c --- /dev/null +++ b/card_pool.py @@ -0,0 +1,51 @@ +import logging + +import serial + +PORT = "/dev/tty.usbmodem11301" + +BAUDRATE = 115200 + + +class CardPool: + + def __init__(self, port): + self.logger = logging.getLogger("CardPool") + self._serial = serial.Serial(port, BAUDRATE, timeout=1) + + def _send_command(self, cmd: str) -> bytes: + print("send command {}".format(cmd)) + self._serial.write(cmd.encode()) + msg = self._serial.read(100) + self.logger.info(msg) + return msg + + # info: after reset, we need to restart modem pool + def reset(self): + self._send_command("AT+NEXT00\r") + + def switch_to_next(self): + self._send_command("AT+NEXT11\r") + + def switch_to_slot(self, slot_number: int): + if slot_number < 10: + self._send_command("AT+SWIT00-000{}\r".format(slot_number)) + else: + self._send_command("AT+SWIT00-0{}\r".format(slot_number)) + + # not work for the pool + def find_current_slot(self): + self._send_command("AT+USIM\r") + + +if __name__ == '__main__': + card_pool = CardPool(PORT) + # print(card_pool.find_current_slot()) + card_pool.reset() + # card_pool.switch_to_next() + # reset modem pool + # for port in get_devices_ports(): + # ser = serial.Serial(port, BAUDRATE, timeout=1) + # send_command("AT+RESET\r", ser) + # ser.close() + # card_pool.switch_to_slot(12) diff --git a/commandor.py b/commandor.py index c87e7a4..7597e39 100644 --- a/commandor.py +++ b/commandor.py @@ -32,6 +32,6 @@ class Commandor: if __name__ == '__main__': commandor = Commandor() - contact = ContactPojo("0608090706", "1234567890", "Willy", "Arnold", "AZEER", "test@test.fr") - commandor.start_page(contact) - # commandor.send_otp("12345") + # contact = ContactPojo("0649614591", "EE6045381", "TANG", "Wenqian", "AZEER", "clench_groom02@icloud.com") + # commandor.start_page(contact) + commandor.send_otp("918116") diff --git a/db/DbManager.py b/db/DbManager.py new file mode 100644 index 0000000..542471a --- /dev/null +++ b/db/DbManager.py @@ -0,0 +1,42 @@ +import datetime +import json + +import firebase_admin +from firebase_admin import credentials, firestore + +from pojo.ReserveResultPojo import ReserveResultPojo, PublishType +from pojo.contact_pojo import ContactPojo + +ERROR_COLLECTION_NAME = "error_items" +TIMEOUT = "timeout_items" + + +class DataManager: + def __init__(self): + cred = credentials.Certificate("appointment.json") + self._app = firebase_admin.initialize_app(cred) + self._db = firestore.client() + + def get_all_error_items(self): + pass + + def save(self, result: ReserveResultPojo): + if result.type == PublishType.SUCCESS: + # get id + id = result.url.split("/")[-1] + result.id = id + document_name = str(datetime.date.today()) + doc_ref = self._db.collection(document_name).document(result.id) + doc_ref.set(result.to_firestore_dict()) + else: + doc_ref = self._db.collection(ERROR_COLLECTION_NAME).document(result.phone) + doc_ref.set(result.to_firestore_dict()) + + def save_timeout_contact(self, contact: ContactPojo): + doc_ref = self._db.collection(TIMEOUT).document(str(contact.phone)) + doc_ref.set(contact.to_firestore_dict()) + + +if __name__ == '__main__': + manager = DataManager() + manager.save() diff --git a/db/__init__.py b/db/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/main.py b/main.py index bb0acf7..03a86eb 100644 --- a/main.py +++ b/main.py @@ -1,4 +1,5 @@ import datetime +import json import logging import re import sys @@ -6,34 +7,47 @@ import time from gsmmodem import GsmModem +from ModemPool import ModemPool +from card_pool import CardPool from commandor import Commandor -from excel_reader import ExcelReader +from db.DbManager import DataManager +from pojo.ReserveResultPojo import ReserveResultPojo, PublishType +from utils.excel_reader import ExcelHelper from pojo.serial_modem import SerialModem from utils.logging import init_logger +from utils.message_receiver import MessageReceiver BAUDRATE = 115200 OTP_TIMEOUT = 60 -sms_received = False +is_finished = False commandor = Commandor() +timeout_contact_list = [] +PORT = "/dev/tty.usbmodem1301" + +# ser = serial.Serial(PORT, BAUDRATE, timeout=1) + +db_manager = DataManager() def get_devices_ports() -> list: - return ["/dev/tty.usbmodem121101", - "/dev/tty.usbmodem121103", - "/dev/tty.usbmodem121105", - "/dev/tty.usbmodem121107", - "/dev/tty.usbmodem121201", - "/dev/tty.usbmodem121203", - "/dev/tty.usbmodem121205", - "/dev/tty.usbmodem121207", - "/dev/tty.usbmodem121301", - # "/dev/tty.usbmodem1121303", - "/dev/tty.usbmodem121305", - "/dev/tty.usbmodem121307", - "/dev/tty.usbmodem121401", - "/dev/tty.usbmodem121403", - "/dev/tty.usbmodem121405", - "/dev/tty.usbmodem121407"] + return [ + "/dev/tty.usbmodem1121101", + "/dev/tty.usbmodem1121103", + "/dev/tty.usbmodem1121105", + "/dev/tty.usbmodem1121107", + "/dev/tty.usbmodem1121201", + "/dev/tty.usbmodem1121203", + "/dev/tty.usbmodem1121205", + "/dev/tty.usbmodem1121207", + "/dev/tty.usbmodem1121301", + ## "/dev/tty.usbmodem1121303", + "/dev/tty.usbmodem1121305", + "/dev/tty.usbmodem1121307", + "/dev/tty.usbmodem1121401", + "/dev/tty.usbmodem1121403", + "/dev/tty.usbmodem1121405", + "/dev/tty.usbmodem1121407" + ] def has_sim(ser) -> bool: @@ -49,21 +63,26 @@ def has_sim(ser) -> bool: def send_command(cmd: str, ser) -> bytes: print("send command {}".format(cmd)) ser.write(cmd.encode()) + time.sleep(10) msg = ser.read(100) - logger.info(msg) + print(msg) return msg -def get_phone_number(): +def get_phone_number(ser): cmd = "AT+CNUM\r" - send_command(cmd) + send_command(cmd, ser) + + +def execut_USSD_cmd(cmd, ser): + send_command(cmd, ser) def create_modem_for_port(port: str) -> SerialModem: logger.info('Initializing modem... for ' + port) # Uncomment the following line to see what the modem is doing: init_logger() - modem = GsmModem(port, BAUDRATE) + modem = GsmModem(port) modem.connect('0000') number = modem.ownNumber logger.info("The SIM card phone number is:") @@ -77,19 +96,26 @@ def create_modem_for_port(port: str) -> SerialModem: return serial_modem +def timeout_occurred(serial_modem: SerialModem): + timeout_contact_list.append(serial_modem.contact) + db_manager.save_timeout_contact(serial_modem.contact) + + def start_to_handle_sms(serial_modem: SerialModem): serial_modem.modem.smsReceivedCallback = handle_sms - global sms_received - sms_received = False + global is_finished + is_finished = False serial_modem.modem.smsTextMode = False logger.info('Waiting for SMS message, for phone number ' + str(serial_modem.phone_number)) listen_at = time.time() - while not sms_received: + while not is_finished: time.sleep(2) # check whether timeout now = time.time() if (listen_at + OTP_TIMEOUT) < now: logger.info("time out for {}, switch to next contact".format(serial_modem.phone_number)) + # save the contact in timeout + timeout_occurred(serial_modem) return return @@ -103,9 +129,11 @@ def handle_sms(sms): otp = match.group(0) logger.info("otp is " + otp) commandor.send_otp(otp) - time.sleep(20) - global sms_received - sms_received = True + # wait for the sms for 20 seconds + global is_finished + while not is_finished: + time.sleep(2) + is_finished = True def init_modems() -> list: @@ -113,24 +141,71 @@ def init_modems() -> list: for port in get_devices_ports(): modems.append(create_modem_for_port(port)) # read the contact, and contact the 2 objects together - excel_reader = ExcelReader() + excel_reader = ExcelHelper() contacts = excel_reader.read_contacts() - for modem in modems: - contact = [contact for contact in contacts if contact.ccid == modem.ccid][0] - modem.phone_number = contact.phone - modem.contact = contact + for index, modem in enumerate(modems): + contact = [contact for contact in contacts if contact.ccid == modem.ccid] + if len(contact) > 0: + modem.phone_number = contact[0].phone + modem.contact = contact[0] + else: + logger.info("contact not found for {}, position:{}".format(modem.ccid, index + 1)) return modems +def on_message_received(ch, method, properties, body): + logger.info(" [x] Received {} {}".format(body, datetime.datetime.now())) + # parse the received message + result = ReserveResultPojo.from_json(body) + print(result) + db_manager.save(result) + # set the flag to True + global is_finished + is_finished = True + + +# save the result to db + + +def start_listen(): + logger.info("start to listen to message queue") + receiver = MessageReceiver() + receiver.start_listener(on_message_received) + + +def read_all_the_phone_number(): + card_pool = CardPool("/dev/tty.usbmodem11301") + slot_number = 1 + slot_sum = 32 + card_pool.reset() + + for i in range(2, slot_sum + 1): + # i = 2 + card_pool.switch_to_slot(i) + modem_pool = ModemPool(get_devices_ports()) + modem_pool.reset_all_modems() + modem_pool.get_raw_phone_number(i) + + if __name__ == '__main__': - # enable verbose logs for all port init_logger() logger = logging.getLogger() logger.addHandler(logging.StreamHandler(stream=sys.stdout)) - # create modems for all the available port - modem_list = init_modems() - # create listeners for chaque modem + read_all_the_phone_number() + # start_listen() + # reset the sim card pool + # send_command("AT+SWIT01-0001\r", ser) - for modem in modem_list: - commandor.start_page(modem.contact) - start_to_handle_sms(modem) + # enable verbose logs for all port + + # # create modems for all the available port + # modem_list = init_modems() + # create listeners for chaque modem + # for modem in modem_list: + # commandor.start_page(modem.contact) + # start_to_handle_sms(modem) + # # save the timeout contacts + # timeout_list = json.dumps(timeout_contact_list) + # f = open("timeout_list.json", "a") + # f.write(str(timeout_list)) + # f.close() diff --git a/pojo/ReserveResultPojo.py b/pojo/ReserveResultPojo.py new file mode 100644 index 0000000..981131f --- /dev/null +++ b/pojo/ReserveResultPojo.py @@ -0,0 +1,29 @@ +from dataclasses import dataclass +from enum import Enum +from dataclasses_json import dataclass_json + + +class PublishType(Enum): + SUCCESS = "SUCCESS" + ERROR = "ERROR" + + +@dataclass_json +@dataclass +class ReserveResultPojo: + type: PublishType + phone: str + message: str + url: str + id = None + + def to_firestore_dict(self): + dest = { + u'type': self.type.value, + u'id': self.id, + u'message': self.message, + u'phone': self.phone, + u'url': self.url + } + + return dest diff --git a/pojo/__pycache__/__init__.cpython-39.pyc b/pojo/__pycache__/__init__.cpython-39.pyc deleted file mode 100644 index c9fcfb6772428a5d0e03f654f70acb3ef4252e51..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 150 zcmYe~<>g`kg7CHcNg(<$h(HF6K#l_t7qb9~6oz01O-8?!3`HPe1o6vSKeRZts8~Oz zATdwhB|o_|H#M)MSid~KD7&~IF*#KqD4Us>Q<7R-qF<1om9HNkpP83g5+AQuP(MS zl@qVPiJ5E`1;(0hJU<>!zF6&c1p&D#pWXeSg#1KjEdrct(C`946HR+E(1ucC=xd^x z=8r`42lhZ4ro}r_Wxt?~R5We1imL5Z2rWj|vH|BBG`s;Yq@kKL?3NgAgr=8A%NENQ zD;As0mP0Kpz4?~uGo3$*CevHEy-4Rb&ceO%t^43oHokY(qE_Gbt=K#T54aL+(*Kxe8ZIuS`G21q9k$R+{E zrwmYN0>jdMm3idXX9)jxuQo{p)8G3o=I|f=SMZ(q5~9Da>f2y^sQXdb`ju-BgRwEx zpPm00M%9`cvYqOq39%l5l)607e>0!$SlXrBv3ssAA`AkkUWFZ^64(utS#vARPgd1 list: contact_list_in_json = pandas.read_excel(r'./contact.xlsx').to_json(orient='records') @@ -26,6 +35,5 @@ class ExcelReader: if __name__ == '__main__': - reader = ExcelReader() - data = reader.read_contacts() - print(data) + helper = ExcelHelper() + helper.write_phone("88649614591") diff --git a/utils/logging.py b/utils/logging.py index dd59c98..8cc9fab 100644 --- a/utils/logging.py +++ b/utils/logging.py @@ -2,7 +2,7 @@ import logging def init_logger(): - logging.basicConfig(filename="scrapy.log", + logging.basicConfig(filename="appointment.log", filemode='a', format='%(asctime)s,%(msecs)d %(name)s %(levelname)s %(message)s', datefmt='%D:%H:%M:%S', diff --git a/utils/message_receiver.py b/utils/message_receiver.py new file mode 100644 index 0000000..3c4368c --- /dev/null +++ b/utils/message_receiver.py @@ -0,0 +1,27 @@ +import logging +import threading +from datetime import datetime + +import pika + +APPOINTMENT_QUEUE = "APPOINTMENT_QUEUE" + + +class MessageReceiver: + def __init__(self): + self._credentials = pika.PlainCredentials('scrapy_rabbitmq', '4x!hReCbA5v3heKWfPJV-Y') + + def start_listener(self, callback): + t = threading.Thread(target=self._run, args=(callback,)) + t.start() + + def _run(self, callback): + connection = pika.BlockingConnection( + pika.ConnectionParameters(host='rabbitmq.lpaconsulting.fr', port=6672, credentials=self._credentials)) + channel = connection.channel() + channel.queue_declare(queue=APPOINTMENT_QUEUE) + channel.basic_consume(queue=APPOINTMENT_QUEUE, + auto_ack=True, + on_message_callback=callback) + print(' [*] Waiting for messages. To exit press CTRL+C') + channel.start_consuming() diff --git a/utils/phone_list.xlsx b/utils/phone_list.xlsx new file mode 100644 index 0000000000000000000000000000000000000000..02f318fc0ca40c27b78365f2d1f5a96c29077b7b GIT binary patch literal 4926 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 literal 0 HcmV?d00001