diff --git a/gsmmodem/modem.py b/gsmmodem/modem.py index 1272190..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 """ @@ -142,16 +151,20 @@ 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]+)"$') + # 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="", *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, 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 @@ -1526,7 +1559,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...') @@ -1542,8 +1577,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): diff --git a/gsmmodem/serial_comms.py b/gsmmodem/serial_comms.py index 22e722c..a02de76 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 @@ -47,23 +51,37 @@ 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) self.rxThread.daemon = True self.rxThread.start() + if self.notificationPort: + 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() + 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 +102,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: + currentSerial = self.notificationSerial + else: + currentSerial = self.serial + try: readTermSeq = bytearray(self.RX_EOL_SEQ) readTermLen = len(readTermSeq) rxBuffer = bytearray() while self.alive: - data = self.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)) @@ -104,18 +127,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() + currentSerial.close() except Exception: #pragma: no cover pass # Notify the fatal error handler