Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
60 changes: 50 additions & 10 deletions gsmmodem/modem.py
Original file line number Diff line number Diff line change
Expand Up @@ -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 """
Expand All @@ -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
Expand Down Expand 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
Expand All @@ -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:
Expand All @@ -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)
Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -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)

Expand Down Expand Up @@ -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

Expand Down Expand Up @@ -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...')
Expand All @@ -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):
Expand Down
43 changes: 33 additions & 10 deletions gsmmodem/serial_comms.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand All @@ -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):
Expand All @@ -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))
Expand All @@ -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:
#' <RX timeout>'
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
Expand Down