From a3b2c30c54f6e8370d1ad2433f183d999094e3e7 Mon Sep 17 00:00:00 2001 From: 11AnJo <63150129+11AnJo@users.noreply.github.com> Date: Wed, 23 Oct 2024 23:08:40 +0200 Subject: [PATCH 1/5] Add index to a StatusReport --- gsmmodem/modem.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/gsmmodem/modem.py b/gsmmodem/modem.py index 1272190..f77acb2 100644 --- a/gsmmodem/modem.py +++ b/gsmmodem/modem.py @@ -110,7 +110,7 @@ class StatusReport(Sms): DELIVERED = 0 # SMS delivery status: delivery successful FAILED = 68 # SMS delivery status: delivery failed - def __init__(self, gsmModem, status, reference, number, timeSent, timeFinalized, deliveryStatus, smsc=None): + def __init__(self, gsmModem, status, reference, number, timeSent, timeFinalized, deliveryStatus, smsc=None, index=None): super(StatusReport, self).__init__(number, None, smsc) self._gsmModem = weakref.proxy(gsmModem) self.status = status @@ -118,6 +118,7 @@ def __init__(self, gsmModem, status, reference, number, timeSent, timeFinalized, self.timeSent = timeSent self.timeFinalized = timeFinalized self.deliveryStatus = deliveryStatus + self.index = index class GsmModem(SerialComms): @@ -1156,7 +1157,7 @@ def listStoredSms(self, status=Sms.STATUS_ALL, memory=None, delete=False): if smsDict['type'] == 'SMS-DELIVER': sms = ReceivedSms(self, int(msgStat), smsDict['number'], smsDict['time'], smsDict['text'], smsDict['smsc'], smsDict.get('udh', []), msgIndex) elif smsDict['type'] == 'SMS-STATUS-REPORT': - sms = StatusReport(self, int(msgStat), smsDict['reference'], smsDict['number'], smsDict['time'], smsDict['discharge'], smsDict['status']) + sms = StatusReport(self, int(msgStat), smsDict['reference'], smsDict['number'], smsDict['time'], smsDict['discharge'], smsDict['status'], msgIndex) else: raise CommandError('Invalid PDU type for readStoredSms(): {0}'.format(smsDict['type'])) messages.append(sms) From 676c4f43679fbb7671c7ff07494a1e8f4a6a378c Mon Sep 17 00:00:00 2001 From: 11AnJo <63150129+11AnJo@users.noreply.github.com> Date: Sat, 2 Nov 2024 16:58:30 +0100 Subject: [PATCH 2/5] fixed: USSD response parsing --- gsmmodem/modem.py | 20 +++++++++++++------- 1 file changed, 13 insertions(+), 7 deletions(-) diff --git a/gsmmodem/modem.py b/gsmmodem/modem.py index f77acb2..6452e0e 100644 --- a/gsmmodem/modem.py +++ b/gsmmodem/modem.py @@ -110,7 +110,7 @@ class StatusReport(Sms): DELIVERED = 0 # SMS delivery status: delivery successful FAILED = 68 # SMS delivery status: delivery failed - def __init__(self, gsmModem, status, reference, number, timeSent, timeFinalized, deliveryStatus, smsc=None, index=None): + def __init__(self, gsmModem, status, reference, number, timeSent, timeFinalized, deliveryStatus, smsc=None): super(StatusReport, self).__init__(number, None, smsc) self._gsmModem = weakref.proxy(gsmModem) self.status = status @@ -118,7 +118,6 @@ def __init__(self, gsmModem, status, reference, number, timeSent, timeFinalized, self.timeSent = timeSent self.timeFinalized = timeFinalized self.deliveryStatus = deliveryStatus - self.index = index class GsmModem(SerialComms): @@ -143,7 +142,7 @@ class GsmModem(SerialComms): # Used for parsing SMS message reads (PDU mode) CMGR_REGEX_PDU = None # Used for parsing USSD event notifications - CUSD_REGEX = re.compile('\+CUSD:\s*(\d),\s*"(.*?)",\s*(\d+)', re.DOTALL) + CUSD_REGEX = re.compile(r'\+CUSD:\s*(\d)(?:,\s*"(.*?)")?(?:,\s*(\d+))?', re.DOTALL) # Used for parsing SMS status reports CDSI_REGEX = re.compile('\+CDSI:\s*"([^"]+)",(\d+)$') CDS_REGEX = re.compile('\+CDS:\s*([0-9]+)"$') @@ -1157,7 +1156,7 @@ def listStoredSms(self, status=Sms.STATUS_ALL, memory=None, delete=False): if smsDict['type'] == 'SMS-DELIVER': sms = ReceivedSms(self, int(msgStat), smsDict['number'], smsDict['time'], smsDict['text'], smsDict['smsc'], smsDict.get('udh', []), msgIndex) elif smsDict['type'] == 'SMS-STATUS-REPORT': - sms = StatusReport(self, int(msgStat), smsDict['reference'], smsDict['number'], smsDict['time'], smsDict['discharge'], smsDict['status'], msgIndex) + sms = StatusReport(self, int(msgStat), smsDict['reference'], smsDict['number'], smsDict['time'], smsDict['discharge'], smsDict['status']) else: raise CommandError('Invalid PDU type for readStoredSms(): {0}'.format(smsDict['type'])) messages.append(sms) @@ -1527,7 +1526,9 @@ def _parseCusdResponse(self, lines): else: # Single standard +CUSD response cusdMatches = [self.CUSD_REGEX.match(lines[0])] - message = None + self.log.info(f'lines: {lines} --- cusdMatches: {cusdMatches}') + + message = "" sessionActive = True if len(cusdMatches) > 1: self.log.debug('Multiple +CUSD responses received; filtering...') @@ -1543,8 +1544,13 @@ def _parseCusdResponse(self, lines): if sessionActive and cusdMatch.group(1) != '1': sessionActive = False else: - sessionActive = cusdMatches[0].group(1) == '1' - message = cusdMatches[0].group(2) + cusdMatch = cusdMatches[0] + if cusdMatch: + sessionActive = cusdMatch.group(1) == '1' + message = cusdMatch.group(2) if cusdMatch.group(2) else None + if cusdMatch.group(1) == '2': + # Set session as inactive if it's a network-terminated message without content + sessionActive = False return Ussd(self, sessionActive, message) def _placeHolderCallback(self, *args): From 9b9588a9a73fe8c2a586ad59487be1290fcc5c1e Mon Sep 17 00:00:00 2001 From: 11AnJo <63150129+11AnJo@users.noreply.github.com> Date: Thu, 12 Dec 2024 22:53:03 +0100 Subject: [PATCH 3/5] Added: support for separate notification port in Huawei modems Huawei modems are sending notifications on a different port than normal communication. Now you can specify port and baudrate of this port and notifications will work properly. --- gsmmodem/modem.py | 4 ++-- gsmmodem/serial_comms.py | 35 +++++++++++++++++++++++++++-------- 2 files changed, 29 insertions(+), 10 deletions(-) diff --git a/gsmmodem/modem.py b/gsmmodem/modem.py index 6452e0e..0d4f212 100644 --- a/gsmmodem/modem.py +++ b/gsmmodem/modem.py @@ -147,8 +147,8 @@ class GsmModem(SerialComms): CDSI_REGEX = re.compile('\+CDSI:\s*"([^"]+)",(\d+)$') CDS_REGEX = re.compile('\+CDS:\s*([0-9]+)"$') - def __init__(self, port, baudrate=115200, incomingCallCallbackFunc=None, smsReceivedCallbackFunc=None, smsStatusReportCallback=None, requestDelivery=True, AT_CNMI="", *a, **kw): - super(GsmModem, self).__init__(port, baudrate, notifyCallbackFunc=self._handleModemNotification, *a, **kw) + def __init__(self, port, baudrate=115200, incomingCallCallbackFunc=None, smsReceivedCallbackFunc=None, smsStatusReportCallback=None, requestDelivery=True, AT_CNMI="", notificationPort=None, notificationBaudrate=115200, *a, **kw): + super(GsmModem, self).__init__(port, baudrate, notifyCallbackFunc=self._handleModemNotification, notificationPort=notificationPort, notificationBaudrate=notificationBaudrate, *a, **kw) self.incomingCallCallback = incomingCallCallbackFunc or self._placeholderCallback self.smsReceivedCallback = smsReceivedCallbackFunc or self._placeholderCallback self.smsStatusReportCallback = smsStatusReportCallback or self._placeholderCallback diff --git a/gsmmodem/serial_comms.py b/gsmmodem/serial_comms.py index 22e722c..99503b1 100644 --- a/gsmmodem/serial_comms.py +++ b/gsmmodem/serial_comms.py @@ -22,7 +22,7 @@ class SerialComms(object): # Default timeout for serial port reads (in seconds) timeout = 1 - def __init__(self, port, baudrate=115200, notifyCallbackFunc=None, fatalErrorCallbackFunc=None, *args, **kwargs): + def __init__(self, port, baudrate=115200, notifyCallbackFunc=None, fatalErrorCallbackFunc=None, notificationPort=None, notificationBaudrate=115200, *args, **kwargs): """ Constructor :param fatalErrorCallbackFunc: function to call if a fatal error occurs in the serial device reading thread @@ -32,6 +32,10 @@ def __init__(self, port, baudrate=115200, notifyCallbackFunc=None, fatalErrorCal self.port = port self.baudrate = baudrate + self.notificationPort = notificationPort + self.notificationBaudrate = notificationBaudrate + + self._responseEvent = None # threading.Event() self._expectResponseTermSeq = None # expected response terminator sequence self._response = None # Buffer containing response to a written command @@ -55,15 +59,25 @@ def connect(self): self.rxThread.daemon = True self.rxThread.start() + if self.notificationPort: + self.notificationSerial = serial.Serial(port=self.notificationPort, baudrate=self.notificationBaudrate, timeout=self.timeout, *self.com_args, **self.com_kwargs) + self.notificationThread = threading.Thread(target=self._readLoop, args=(True,)) + self.notificationThread.daemon = True + self.notificationThread.start() + def close(self): """ Stops the read thread, waits for it to exit cleanly, then closes the underlying serial port """ self.alive = False self.rxThread.join() self.serial.close() - def _handleLineRead(self, line, checkForResponseTerm=True): + if self.notificationPort: + self.notificationThread.join() + self.notificationSerial.close() + + def _handleLineRead(self, line, checkForResponseTerm=True, isNotificationSerial=False): #print 'sc.hlineread:',line - if self._responseEvent and not self._responseEvent.is_set(): + if self._responseEvent and not self._responseEvent.is_set() and not isNotificationSerial: # A response event has been set up (another thread is waiting for this response) self._response.append(line) if not checkForResponseTerm or self.RESPONSE_TERM.match(line): @@ -84,17 +98,22 @@ def _handleLineRead(self, line, checkForResponseTerm=True): def _placeholderCallback(self, *args, **kwargs): """ Placeholder callback function (does nothing) """ - def _readLoop(self): + def _readLoop(self, isNotificationSerial=False): """ Read thread main loop Reads lines from the connected device """ + if isNotificationSerial: + serial = self.notificationSerial + else: + serial = self.serial + try: readTermSeq = bytearray(self.RX_EOL_SEQ) readTermLen = len(readTermSeq) rxBuffer = bytearray() while self.alive: - data = self.serial.read(1) + data = serial.read(1) if len(data) != 0: # check for timeout #print >> sys.stderr, ' RX:', data,'({0})'.format(ord(data)) rxBuffer.append(ord(data)) @@ -104,18 +123,18 @@ def _readLoop(self): rxBuffer = bytearray() if len(line) > 0: #print 'calling handler' - self._handleLineRead(line) + self._handleLineRead(line,isNotificationSerial=isNotificationSerial) elif self._expectResponseTermSeq: if rxBuffer[-len(self._expectResponseTermSeq):] == self._expectResponseTermSeq: line = rxBuffer.decode() rxBuffer = bytearray() - self._handleLineRead(line, checkForResponseTerm=False) + self._handleLineRead(line, checkForResponseTerm=False,isNotificationSerial=isNotificationSerial) #else: #' ' except serial.SerialException as e: self.alive = False try: - self.serial.close() + serial.close() except Exception: #pragma: no cover pass # Notify the fatal error handler From 07d32fe4b69466f9926c0771f51f650374e74300 Mon Sep 17 00:00:00 2001 From: 11AnJo <63150129+11AnJo@users.noreply.github.com> Date: Thu, 2 Jan 2025 17:50:21 +0100 Subject: [PATCH 4/5] Bugfix in serial_comms.py --- gsmmodem/serial_comms.py | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/gsmmodem/serial_comms.py b/gsmmodem/serial_comms.py index 99503b1..672e936 100644 --- a/gsmmodem/serial_comms.py +++ b/gsmmodem/serial_comms.py @@ -60,7 +60,8 @@ def connect(self): self.rxThread.start() if self.notificationPort: - self.notificationSerial = serial.Serial(port=self.notificationPort, baudrate=self.notificationBaudrate, timeout=self.timeout, *self.com_args, **self.com_kwargs) + self.notificationSerial = serial.Serial(dsrdtr=True, rtscts=True, port=self.notificationPort, baudrate=self.notificationBaudrate, + timeout=self.timeout,*self.com_args,**self.com_kwargs) self.notificationThread = threading.Thread(target=self._readLoop, args=(True,)) self.notificationThread.daemon = True self.notificationThread.start() @@ -104,16 +105,16 @@ def _readLoop(self, isNotificationSerial=False): Reads lines from the connected device """ if isNotificationSerial: - serial = self.notificationSerial + currentSerial = self.notificationSerial else: - serial = self.serial + currentSerial = self.serial try: readTermSeq = bytearray(self.RX_EOL_SEQ) readTermLen = len(readTermSeq) rxBuffer = bytearray() while self.alive: - data = serial.read(1) + data = currentSerial.read(1) if len(data) != 0: # check for timeout #print >> sys.stderr, ' RX:', data,'({0})'.format(ord(data)) rxBuffer.append(ord(data)) @@ -134,7 +135,7 @@ def _readLoop(self, isNotificationSerial=False): except serial.SerialException as e: self.alive = False try: - serial.close() + currentSerial.close() except Exception: #pragma: no cover pass # Notify the fatal error handler From 1eb8ae825ab6fb043d22f55044e504685d4583f4 Mon Sep 17 00:00:00 2001 From: 11AnJo <63150129+11AnJo@users.noreply.github.com> Date: Fri, 20 Jun 2025 17:26:02 +0200 Subject: [PATCH 5/5] Added: Signal strength notifcation --- gsmmodem/modem.py | 43 +++++++++++++++++++++++++++++++++++----- gsmmodem/serial_comms.py | 11 ++++++---- 2 files changed, 45 insertions(+), 9 deletions(-) diff --git a/gsmmodem/modem.py b/gsmmodem/modem.py index 0d4f212..ba70b28 100644 --- a/gsmmodem/modem.py +++ b/gsmmodem/modem.py @@ -119,6 +119,15 @@ def __init__(self, gsmModem, status, reference, number, timeSent, timeFinalized, self.timeFinalized = timeFinalized self.deliveryStatus = deliveryStatus +class Rssi(object): + """ Received Signal Strength Indicator (RSSI) value + + This class is used to represent the RSSI value returned by the modem. + """ + + def __init__(self, rssi, time=None): + self.rssi = rssi # RSSI value + self.time = time or datetime.now(SimpleOffsetTzInfo(0)) # Time when the RSSI was read class GsmModem(SerialComms): """ Main class for interacting with an attached GSM modem """ @@ -146,12 +155,16 @@ class GsmModem(SerialComms): # Used for parsing SMS status reports CDSI_REGEX = re.compile('\+CDSI:\s*"([^"]+)",(\d+)$') CDS_REGEX = re.compile('\+CDS:\s*([0-9]+)"$') + # Used for parsing Rrsi (Received Signal Strength Indicator) values + RSSI_REGEX = re.compile(r'\^RSSI:\s*(-?\d+)') + - def __init__(self, port, baudrate=115200, incomingCallCallbackFunc=None, smsReceivedCallbackFunc=None, smsStatusReportCallback=None, requestDelivery=True, AT_CNMI="", notificationPort=None, notificationBaudrate=115200, *a, **kw): + def __init__(self, port, baudrate=115200, incomingCallCallbackFunc=None, smsReceivedCallbackFunc=None, smsStatusReportCallback=None, signalStrengthCallbackFunc=None, requestDelivery=True, AT_CNMI="", notificationPort=None, notificationBaudrate=115200, *a, **kw): super(GsmModem, self).__init__(port, baudrate, notifyCallbackFunc=self._handleModemNotification, notificationPort=notificationPort, notificationBaudrate=notificationBaudrate, *a, **kw) self.incomingCallCallback = incomingCallCallbackFunc or self._placeholderCallback self.smsReceivedCallback = smsReceivedCallbackFunc or self._placeholderCallback self.smsStatusReportCallback = smsStatusReportCallback or self._placeholderCallback + self.signalStrengthCallback = signalStrengthCallbackFunc or self._placeholderCallback self.requestDelivery = requestDelivery self.AT_CNMI = AT_CNMI or "2,1,0,2" # Flag indicating whether caller ID for incoming call notification has been set up @@ -186,7 +199,7 @@ def __init__(self, port, baudrate=115200, incomingCallCallbackFunc=None, smsRece #Pool of detected DTMF self.dtmfpool = [] - def connect(self, pin=None, waitingForModemToStartInSeconds=0): + def connect(self, pin=None, waitingForModemToStartInSeconds=10): """ Opens the port and initializes the modem and SIM card :param pin: The SIM card PIN code, if any @@ -205,6 +218,8 @@ def connect(self, pin=None, waitingForModemToStartInSeconds=0): break except TimeoutException: waitingForModemToStartInSeconds -= 0.5 + if waitingForModemToStartInSeconds <= 0: + raise TimeoutException('Modem did not respond to AT command after waiting for {0} seconds'.format(waitingForModemToStartInSeconds)) # Send some initialization commands to the modem try: @@ -227,7 +242,6 @@ def connect(self, pin=None, waitingForModemToStartInSeconds=0): self.write('AT+CFUN=1') except CommandError: pass # just ignore if the +CFUN command isn't supported - self.write('AT+CMEE=1') # enable detailed error messages (even if it has already been set - ATZ may reset this) if not pinCheckComplete: self._unlockSim(pin) @@ -1219,6 +1233,9 @@ def __threadedHandleModemNotification(self, lines): # New incoming DTMF self._handleIncomingDTMF(line) return + elif line.startswith('^RSSI'): + self._handleSignalStrengthChange(line) + return else: # Check for call status updates for updateRegex, handlerFunc in self._callStatusUpdates: @@ -1358,8 +1375,8 @@ def _handleSmsReceived(self, notificationLine): sms = self.readStoredSms(msgIndex, msgMemory) try: self.smsReceivedCallback(sms) - except Exception: - self.log.error('error in smsReceivedCallback', exc_info=True) + except: + self.log.exception('error in smsReceivedCallback') else: self.deleteStoredSms(msgIndex) @@ -1410,6 +1427,22 @@ def _handleSmsStatusReportTe(self, length, notificationLine): except Exception: self.log.error('error in smsStatusReportCallback', exc_info=True) + def _handleSignalStrengthChange(self, notificationLine): + """ Handler for signal strength change notification line """ + self.log.debug('Signal strength changed') + rssiMatch = self.RSSI_REGEX.match(notificationLine) + if rssiMatch: + try: + if self.signalStrengthCallback: + rssi = Rssi(int(rssiMatch.group(1))) + self.signalStrengthCallback(rssi) + else: + self.log.debug('No signal strength callback set') + except Exception: + self.log.error('error in signalStrengthCallback', exc_info=True) + else: + self.log.debug('Failed to parse RSSI notification line: %s', notificationLine) + def readStoredSms(self, index, memory=None): """ Reads and returns the SMS message at the specified index diff --git a/gsmmodem/serial_comms.py b/gsmmodem/serial_comms.py index 672e936..a02de76 100644 --- a/gsmmodem/serial_comms.py +++ b/gsmmodem/serial_comms.py @@ -51,8 +51,9 @@ def __init__(self, port, baudrate=115200, notifyCallbackFunc=None, fatalErrorCal def connect(self): """ Connects to the device and starts the read thread """ - self.serial = serial.Serial(dsrdtr=True, rtscts=True, port=self.port, baudrate=self.baudrate, - timeout=self.timeout,*self.com_args,**self.com_kwargs) + logging.info('Connecting to serial port %s at %d baud', self.port, self.baudrate) + self.serial = serial.Serial(dsrdtr=False, rtscts=True, port=self.port, baudrate=self.baudrate, timeout=15,*self.com_args,**self.com_kwargs) + logging.info(self.serial.get_settings()) # Start read thread self.alive = True self.rxThread = threading.Thread(target=self._readLoop) @@ -60,8 +61,10 @@ def connect(self): self.rxThread.start() if self.notificationPort: - self.notificationSerial = serial.Serial(dsrdtr=True, rtscts=True, port=self.notificationPort, baudrate=self.notificationBaudrate, - timeout=self.timeout,*self.com_args,**self.com_kwargs) + logging.info('Connecting to serial port %s at %d baud', self.notificationPort, self.notificationBaudrate) + self.notificationSerial = serial.Serial(dsrdtr=True, rtscts=True, port=self.notificationPort, baudrate=self.notificationBaudrate, timeout=15, *self.com_args,**self.com_kwargs) + logging.info(self.notificationSerial.get_settings()) + self.notificationThread = threading.Thread(target=self._readLoop, args=(True,)) self.notificationThread.daemon = True self.notificationThread.start()