integrate the gsmmodem into project

This commit is contained in:
2022-03-16 21:36:33 +01:00
parent 5132f4fc4c
commit 444d06b83b
9 changed files with 243 additions and 228 deletions
+80
View File
@@ -0,0 +1,80 @@
import weakref
from gsmmodem.exceptions import CmeError, InterruptedException, InvalidStateException
class Call(object):
""" A voice call """
DTMF_COMMAND_BASE = '+VTS='
dtmfSupport = False # Indicates whether or not DTMF tones can be sent in calls
def __init__(self, gsmModem, callId, callType, number, callStatusUpdateCallbackFunc=None):
"""
:param gsmModem: GsmModem instance that created this object
:param number: The number that is being called
"""
self._gsmModem = weakref.proxy(gsmModem)
self._callStatusUpdateCallbackFunc = callStatusUpdateCallbackFunc
# Unique ID of this call
self.id = callId
# Call type (VOICE == 0, etc)
self.type = callType
# The remote number of this call (destination or origin)
self.number = number
# Flag indicating whether the call has been answered or not (backing field for "answered" property)
self._answered = False
# Flag indicating whether or not the call is active
# (meaning it may be ringing or answered, but not ended because of a hangup event)
self.active = True
@property
def answered(self):
return self._answered
@answered.setter
def answered(self, answered):
self._answered = answered
if self._callStatusUpdateCallbackFunc:
self._callStatusUpdateCallbackFunc(self)
def sendDtmfTone(self, tones):
""" Send one or more DTMF tones to the remote party (only allowed for an answered call)
Note: this is highly device-dependent, and might not work
:param digits: A str containining one or more DTMF tones to play, e.g. "3" or "\*123#"
:raise CommandError: if the command failed/is not supported
:raise InvalidStateException: if the call has not been answered, or is ended while the command is still executing
"""
if self.answered:
dtmfCommandBase = self.DTMF_COMMAND_BASE.format(cid=self.id)
toneLen = len(tones)
for tone in list(tones):
try:
self._gsmModem.write('AT{0}{1}'.format(dtmfCommandBase, tone), timeout=(5 + toneLen))
except CmeError as e:
if e.code == 30:
# No network service - can happen if call is ended during DTMF transmission (but also if DTMF is sent immediately after call is answered)
raise InterruptedException('No network service', e)
elif e.code == 3:
# Operation not allowed - can happen if call is ended during DTMF transmission
raise InterruptedException('Operation not allowed', e)
else:
raise e
else:
raise InvalidStateException('Call is not active (it has not yet been answered, or it has ended).')
def hangup(self):
""" End the phone call.
Does nothing if the call is already inactive.
"""
if self.active:
self._gsmModem.write('ATH')
self.answered = False
self.active = False
if self.id in self._gsmModem.activeCalls:
del self._gsmModem.activeCalls[self.id]
+40
View File
@@ -0,0 +1,40 @@
from gsmmodem.modem import Call
class IncomingCall(Call):
CALL_TYPE_MAP = {'VOICE': 0}
""" Represents an incoming call, conveniently allowing access to call meta information and -control """
def __init__(self, gsmModem, number, ton, callerName, callId, callType):
"""
:param gsmModem: GsmModem instance that created this object
:param number: Caller number
:param ton: TON (type of number/address) in integer format
:param callType: Type of the incoming call (VOICE, FAX, DATA, etc)
"""
if callType in self.CALL_TYPE_MAP:
callType = self.CALL_TYPE_MAP[callType]
super(IncomingCall, self).__init__(gsmModem, callId, callType, number)
# Type attribute of the incoming call
self.ton = ton
self.callerName = callerName
# Flag indicating whether the call is ringing or not
self.ringing = True
# Amount of times this call has rung (before answer/hangup)
self.ringCount = 1
def answer(self):
""" Answer the phone call.
:return: self (for chaining method calls)
"""
if self.ringing:
self._gsmModem.write('ATA')
self.ringing = False
self.answered = True
return self
def hangup(self):
""" End the phone call. """
self.ringing = False
super(IncomingCall, self).hangup()
+27
View File
@@ -0,0 +1,27 @@
import weakref
from gsmmodem.models.Sms import Sms
class ReceivedSms(Sms):
""" An SMS message that has been received (MT) """
def __init__(self, gsmModem, status, number, time, text, smsc=None, udh=[], index=None):
super(ReceivedSms, self).__init__(number, text, smsc)
self._gsmModem = weakref.proxy(gsmModem)
self.status = status
self.time = time
self.udh = udh
self.index = index
def reply(self, message):
""" Convenience method that sends a reply SMS to the sender of this message """
return self._gsmModem.sendSms(self.number, message)
def sendSms(self, dnumber, message):
""" Convenience method that sends a SMS to someone else """
return self._gsmModem.sendSms(dnumber, message)
def getModem(self):
""" Convenience method that returns the gsm modem instance """
return self._gsmModem
+27
View File
@@ -0,0 +1,27 @@
from gsmmodem.models.Sms import Sms
from gsmmodem.models.StatusReport import StatusReport
class SentSms(Sms):
""" An SMS message that has been sent (MO) """
ENROUTE = 0 # Status indicating message is still enroute to destination
DELIVERED = 1 # Status indicating message has been received by destination handset
FAILED = 2 # Status indicating message delivery has failed
def __init__(self, number, text, reference, smsc=None):
super(SentSms, self).__init__(number, text, smsc)
self.report = None # Status report for this SMS (StatusReport object)
self.reference = reference
@property
def status(self):
""" Status of this SMS. Can be ENROUTE, DELIVERED or FAILED
The actual status report object may be accessed via the 'report' attribute
if status is 'DELIVERED' or 'FAILED'
"""
if self.report == None:
return SentSms.ENROUTE
else:
return SentSms.DELIVERED if self.report.deliveryStatus == StatusReport.DELIVERED else SentSms.FAILED
+24
View File
@@ -0,0 +1,24 @@
import abc
class Sms(object):
""" Abstract SMS message base class """
__metaclass__ = abc.ABCMeta
# Some constants to ease handling SMS statuses
STATUS_RECEIVED_UNREAD = 0
STATUS_RECEIVED_READ = 1
STATUS_STORED_UNSENT = 2
STATUS_STORED_SENT = 3
STATUS_ALL = 4
# ...and a handy converter for text mode statuses
TEXT_MODE_STATUS_MAP = {'REC UNREAD': STATUS_RECEIVED_UNREAD,
'REC READ': STATUS_RECEIVED_READ,
'STO UNSENT': STATUS_STORED_UNSENT,
'STO SENT': STATUS_STORED_SENT,
'ALL': STATUS_ALL}
def __init__(self, number, text, smsc=None):
self.number = number
self.text = text
self.smsc = smsc
+24
View File
@@ -0,0 +1,24 @@
import weakref
from gsmmodem.models.Sms import Sms
class StatusReport(Sms):
""" An SMS status/delivery report
Note: the 'status' attribute of this class refers to this status report SM's status (whether
it has been read, etc). To find the status of the message that caused this status report,
use the 'deliveryStatus' attribute.
"""
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):
super(StatusReport, self).__init__(number, None, smsc)
self._gsmModem = weakref.proxy(gsmModem)
self.status = status
self.reference = reference
self.timeSent = timeSent
self.timeFinalized = timeFinalized
self.deliveryStatus = deliveryStatus
View File
+21 -223
View File
@@ -2,18 +2,27 @@
""" High-level API classes for an attached GSM modem """
import sys, re, logging, weakref, time, threading, abc, codecs
from datetime import datetime
from time import sleep
import logging
import re
import sys
import threading
import time
import weakref
from .serial_comms import SerialComms
from .exceptions import CommandError, InvalidStateException, CmeError, CmsError, InterruptedException, TimeoutException, PinRequiredError, IncorrectPinError, SmscNumberUnknownError
from .pdu import encodeSmsSubmitPdu, decodeSmsPdu, encodeGsm7, encodeTextMode
from .util import SimpleOffsetTzInfo, lineStartingWith, allLinesMatchingPattern, parseTextModeTimeStr, removeAtPrefix
#from . import compat # For Python 2.6 compatibility
from gsmmodem.util import lineMatching
from gsmmodem.exceptions import EncodingError
from gsmmodem.util import lineMatching
from .exceptions import CommandError, InvalidStateException, CmeError, CmsError, TimeoutException, PinRequiredError, \
SmscNumberUnknownError
from .models.Call import Call
from .models.IncomingCall import IncomingCall
from .models.ReceivedSms import ReceivedSms
from .models.SentSms import SentSms
from .models.Sms import Sms
from .models.StatusReport import StatusReport
from .pdu import encodeSmsSubmitPdu, decodeSmsPdu, encodeGsm7, encodeTextMode
from .serial_comms import SerialComms
from .util import lineStartingWith, parseTextModeTimeStr, removeAtPrefix
PYTHON_VERSION = sys.version_info[0]
CTRLZ = '\x1a'
@@ -23,102 +32,6 @@ if PYTHON_VERSION >= 3:
xrange = range
dictValuesIter = dict.values
dictItemsIter = dict.items
else: #pragma: no cover
dictValuesIter = dict.itervalues
dictItemsIter = dict.iteritems
class Sms(object):
""" Abstract SMS message base class """
__metaclass__ = abc.ABCMeta
# Some constants to ease handling SMS statuses
STATUS_RECEIVED_UNREAD = 0
STATUS_RECEIVED_READ = 1
STATUS_STORED_UNSENT = 2
STATUS_STORED_SENT = 3
STATUS_ALL = 4
# ...and a handy converter for text mode statuses
TEXT_MODE_STATUS_MAP = {'REC UNREAD': STATUS_RECEIVED_UNREAD,
'REC READ': STATUS_RECEIVED_READ,
'STO UNSENT': STATUS_STORED_UNSENT,
'STO SENT': STATUS_STORED_SENT,
'ALL': STATUS_ALL}
def __init__(self, number, text, smsc=None):
self.number = number
self.text = text
self.smsc = smsc
class ReceivedSms(Sms):
""" An SMS message that has been received (MT) """
def __init__(self, gsmModem, status, number, time, text, smsc=None, udh=[], index=None):
super(ReceivedSms, self).__init__(number, text, smsc)
self._gsmModem = weakref.proxy(gsmModem)
self.status = status
self.time = time
self.udh = udh
self.index = index
def reply(self, message):
""" Convenience method that sends a reply SMS to the sender of this message """
return self._gsmModem.sendSms(self.number, message)
def sendSms(self, dnumber, message):
""" Convenience method that sends a SMS to someone else """
return self._gsmModem.sendSms(dnumber, message)
def getModem(self):
""" Convenience method that returns the gsm modem instance """
return self._gsmModem
class SentSms(Sms):
""" An SMS message that has been sent (MO) """
ENROUTE = 0 # Status indicating message is still enroute to destination
DELIVERED = 1 # Status indicating message has been received by destination handset
FAILED = 2 # Status indicating message delivery has failed
def __init__(self, number, text, reference, smsc=None):
super(SentSms, self).__init__(number, text, smsc)
self.report = None # Status report for this SMS (StatusReport object)
self.reference = reference
@property
def status(self):
""" Status of this SMS. Can be ENROUTE, DELIVERED or FAILED
The actual status report object may be accessed via the 'report' attribute
if status is 'DELIVERED' or 'FAILED'
"""
if self.report == None:
return SentSms.ENROUTE
else:
return SentSms.DELIVERED if self.report.deliveryStatus == StatusReport.DELIVERED else SentSms.FAILED
class StatusReport(Sms):
""" An SMS status/delivery report
Note: the 'status' attribute of this class refers to this status report SM's status (whether
it has been read, etc). To find the status of the message that caused this status report,
use the 'deliveryStatus' attribute.
"""
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):
super(StatusReport, self).__init__(number, None, smsc)
self._gsmModem = weakref.proxy(gsmModem)
self.status = status
self.reference = reference
self.timeSent = timeSent
self.timeFinalized = timeFinalized
self.deliveryStatus = deliveryStatus
class GsmModem(SerialComms):
""" Main class for interacting with an attached GSM modem """
@@ -434,7 +347,7 @@ class GsmModem(SerialComms):
else:
raise PinRequiredError('AT+CPIN')
def write(self, data, waitForResponse=True, timeout=10, parseError=True, writeTerm=TERMINATOR, expectedResponseTermSeq=None):
def write(self, data, waitForResponse=True, timeout:float =10, parseError=True, writeTerm=TERMINATOR, expectedResponseTermSeq=None):
""" Write data to the modem.
This method adds the ``\\r\\n`` end-of-line sequence to the data parameter, and
@@ -1290,7 +1203,7 @@ class GsmModem(SerialComms):
call = activeCall
call.ringCount += 1
if call == None:
callId = len(self.activeCalls) + 1;
callId = len(self.activeCalls) + 1
call = IncomingCall(self, callerNumber, ton, callerName, callId, callType)
self.activeCalls[callId] = call
self.incomingCallCallback(call)
@@ -1601,121 +1514,6 @@ class GsmModem(SerialComms):
if timeLeft <= 0:
raise TimeoutException()
class Call(object):
""" A voice call """
DTMF_COMMAND_BASE = '+VTS='
dtmfSupport = False # Indicates whether or not DTMF tones can be sent in calls
def __init__(self, gsmModem, callId, callType, number, callStatusUpdateCallbackFunc=None):
"""
:param gsmModem: GsmModem instance that created this object
:param number: The number that is being called
"""
self._gsmModem = weakref.proxy(gsmModem)
self._callStatusUpdateCallbackFunc = callStatusUpdateCallbackFunc
# Unique ID of this call
self.id = callId
# Call type (VOICE == 0, etc)
self.type = callType
# The remote number of this call (destination or origin)
self.number = number
# Flag indicating whether the call has been answered or not (backing field for "answered" property)
self._answered = False
# Flag indicating whether or not the call is active
# (meaning it may be ringing or answered, but not ended because of a hangup event)
self.active = True
@property
def answered(self):
return self._answered
@answered.setter
def answered(self, answered):
self._answered = answered
if self._callStatusUpdateCallbackFunc:
self._callStatusUpdateCallbackFunc(self)
def sendDtmfTone(self, tones):
""" Send one or more DTMF tones to the remote party (only allowed for an answered call)
Note: this is highly device-dependent, and might not work
:param digits: A str containining one or more DTMF tones to play, e.g. "3" or "\*123#"
:raise CommandError: if the command failed/is not supported
:raise InvalidStateException: if the call has not been answered, or is ended while the command is still executing
"""
if self.answered:
dtmfCommandBase = self.DTMF_COMMAND_BASE.format(cid=self.id)
toneLen = len(tones)
for tone in list(tones):
try:
self._gsmModem.write('AT{0}{1}'.format(dtmfCommandBase,tone), timeout=(5 + toneLen))
except CmeError as e:
if e.code == 30:
# No network service - can happen if call is ended during DTMF transmission (but also if DTMF is sent immediately after call is answered)
raise InterruptedException('No network service', e)
elif e.code == 3:
# Operation not allowed - can happen if call is ended during DTMF transmission
raise InterruptedException('Operation not allowed', e)
else:
raise e
else:
raise InvalidStateException('Call is not active (it has not yet been answered, or it has ended).')
def hangup(self):
""" End the phone call.
Does nothing if the call is already inactive.
"""
if self.active:
self._gsmModem.write('ATH')
self.answered = False
self.active = False
if self.id in self._gsmModem.activeCalls:
del self._gsmModem.activeCalls[self.id]
class IncomingCall(Call):
CALL_TYPE_MAP = {'VOICE': 0}
""" Represents an incoming call, conveniently allowing access to call meta information and -control """
def __init__(self, gsmModem, number, ton, callerName, callId, callType):
"""
:param gsmModem: GsmModem instance that created this object
:param number: Caller number
:param ton: TON (type of number/address) in integer format
:param callType: Type of the incoming call (VOICE, FAX, DATA, etc)
"""
if callType in self.CALL_TYPE_MAP:
callType = self.CALL_TYPE_MAP[callType]
super(IncomingCall, self).__init__(gsmModem, callId, callType, number)
# Type attribute of the incoming call
self.ton = ton
self.callerName = callerName
# Flag indicating whether the call is ringing or not
self.ringing = True
# Amount of times this call has rung (before answer/hangup)
self.ringCount = 1
def answer(self):
""" Answer the phone call.
:return: self (for chaining method calls)
"""
if self.ringing:
self._gsmModem.write('ATA')
self.ringing = False
self.answered = True
return self
def hangup(self):
""" End the phone call. """
self.ringing = False
super(IncomingCall, self).hangup()
class Ussd(object):
""" Unstructured Supplementary Service Data (USSD) message.
-5
View File
@@ -18,11 +18,6 @@ if PYTHON_VERSION >= 3:
unichr = chr
toByteArray = lambda x: bytearray(codecs.decode(x, 'hex_codec')) if type(x) == bytes else bytearray(codecs.decode(bytes(x, 'ascii'), 'hex_codec')) if type(x) == str else x
rawStrToByteArray = lambda x: bytearray(bytes(x, 'latin-1'))
else: #pragma: no cover
MAX_INT = sys.maxint
dictItemsIter = dict.iteritems
toByteArray = lambda x: bytearray(x.decode('hex')) if type(x) in (str, unicode) else x
rawStrToByteArray = bytearray
TEXT_MODE = ('\n\r !\"#%&\'()*+,-./0123456789:;<=>?ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz') # TODO: Check if all of them are supported inside text mode
# Tables can be found at: http://en.wikipedia.org/wiki/GSM_03.38#GSM_7_bit_default_alphabet_and_extension_table_of_3GPP_TS_23.038_.2F_GSM_03.38