diff --git a/Hologram/Network/Cellular.py b/Hologram/Network/Cellular.py index 928c69f..dc4dbc1 100644 --- a/Hologram/Network/Cellular.py +++ b/Hologram/Network/Cellular.py @@ -11,7 +11,7 @@ from Hologram.Event import Event from Exceptions.HologramError import NetworkError from Hologram.Network.Route import Route -from Hologram.Network.Modem import Modem, E303, MS2131, E372, BG96, Nova_U201, NovaM, DriverLoader +from Hologram.Network.Modem import Modem, E303, MS2131, E372, BG96, EC21, Nova_U201, NovaM, DriverLoader from Hologram.Network import Network, NetworkScope import time from serial.tools import list_ports @@ -32,6 +32,7 @@ class Cellular(Network): 'ms2131': MS2131.MS2131, 'e372': E372.E372, 'bg96': BG96.BG96, + 'ec21': EC21.EC21, 'nova': Nova_U201.Nova_U201, 'novam': NovaM.NovaM, '': Modem diff --git a/Hologram/Network/Modem/EC21.py b/Hologram/Network/Modem/EC21.py new file mode 100644 index 0000000..5bb0d8a --- /dev/null +++ b/Hologram/Network/Modem/EC21.py @@ -0,0 +1,191 @@ +# EC21.py - Hologram Python SDK Quectel EC21 modem interface +# +# Author: Hologram +# +# Copyright 2016 - Hologram (Konekt, Inc.) +# +# +# LICENSE: Distributed under the terms of the MIT License +# +import binascii +import time + +from serial.serialutil import Timeout + +from Hologram.Network.Modem import Modem +from Hologram.Event import Event +from UtilClasses import ModemResult +from Exceptions.HologramError import SerialError, NetworkError + +DEFAULT_EC21_TIMEOUT = 200 + +class EC21(Modem): + usb_ids = [('2c7c', '0121')] + + def __init__(self, device_name=None, baud_rate='9600', + chatscript_file=None, event=Event()): + + super().__init__(device_name=device_name, baud_rate=baud_rate, + chatscript_file=chatscript_file, event=event) + self._at_sockets_available = True + self.urc_response = '' + + def connect(self, timeout=DEFAULT_EC21_TIMEOUT): + success = super().connect(timeout) + return success + + def send_message(self, data, timeout=Modem.DEFAULT_SEND_TIMEOUT): + # Waiting for the open socket urc + while self.urc_state != Modem.SOCKET_WRITE_STATE: + self.checkURC() + + self.write_socket(data) + + loop_timeout = Timeout(timeout) + while self.urc_state != Modem.SOCKET_SEND_READ: + self.checkURC() + if self.urc_state != Modem.SOCKET_SEND_READ: + if loop_timeout.expired(): + raise SerialError('Timeout occurred waiting for message status') + time.sleep(self._RETRY_DELAY) + elif self.urc_state == Modem.SOCKET_CLOSED: + return '[1,0]' #this is connection closed for hologram cloud response + + return self.urc_response.rstrip('\r\n') + + def create_socket(self): + self._set_up_pdp_context() + + def connect_socket(self, host, port): + self.command('+QIOPEN', '1,0,\"TCP\",\"%s\",%d,0,1' % (host, port)) + # According to the EC21 Docs + # Have to wait for URC response “+QIOPEN: ,” + + def close_socket(self, socket_identifier=None): + ok, _ = self.command('+QICLOSE', self.socket_identifier) + if ok != ModemResult.OK: + self.logger.error('Failed to close socket') + self.urc_state = Modem.SOCKET_CLOSED + self._tear_down_pdp_context() + + def write_socket(self, data): + hexdata = binascii.hexlify(data) + # We have to do it in chunks of 510 since 512 is actually too long (CMEE error) + # and we need 2n chars for hexified data + for chunk in self._chunks(hexdata, 510): + value = '%d,\"%s\"' % (self.socket_identifier, chunk.decode()) + ok, _ = self.set('+QISENDEX', value, timeout=10) + if ok != ModemResult.OK: + self.logger.error('Failed to write to socket') + raise NetworkError('Failed to write to socket') + + def read_socket(self, socket_identifier=None, payload_length=None): + + if socket_identifier is None: + socket_identifier = self.socket_identifier + + if payload_length is None: + payload_length = self.last_read_payload_length + + ok, resp = self.set('+QIRD', '%d,%d' % (socket_identifier, payload_length)) + if ok == ModemResult.OK: + resp = resp.lstrip('+QIRD: ') + if resp is not None: + resp = resp.strip('"') + try: + resp = resp.decode() + except: + # This is some sort of binary data that can't be decoded so just + # return the bytes. We might want to make this happen via parameter + # in the future so it is more deterministic + self.logger.debug('Could not decode recieved data') + + return resp + + def listen_socket(self, port): + # No equivilent exists for quectel modems + pass + + def is_registered(self): + return self.check_registered('+CREG') or self.check_registered('+CEREG') + + # EFFECTS: Handles URC related AT command responses. + def handleURC(self, urc): + if urc.startswith('+QIOPEN: '): + response_list = urc.lstrip('+QIOPEN: ').split(',') + socket_identifier = int(response_list[0]) + err = int(response_list[-1]) + if err == 0: + self.urc_state = Modem.SOCKET_WRITE_STATE + self.socket_identifier = socket_identifier + else: + self.logger.error('Failed to open socket') + raise NetworkError('Failed to open socket') + return + if urc.startswith('+QIURC: '): + response_list = urc.lstrip('+QIURC: ').split(',') + urctype = response_list[0] + if urctype == '\"recv\"': + self.urc_state = Modem.SOCKET_SEND_READ + self.socket_identifier = int(response_list[1]) + self.last_read_payload_length = int(response_list[2]) + self.urc_response = self._readline_from_serial_port(5) + if urctype == '\"closed\"': + self.urc_state = Modem.SOCKET_CLOSED + self.socket_identifier = int(response_list[-1]) + return + super().handleURC(urc) + + def _is_pdp_context_active(self): + if not self.is_registered(): + return False + + ok, r = self.command('+QIACT?') + if ok == ModemResult.OK: + try: + pdpstatus = int(r.lstrip('+QIACT: ').split(',')[1]) + # 1: PDP active + return pdpstatus == 1 + except (IndexError, ValueError) as e: + self.logger.error(repr(e)) + except AttributeError as e: + self.logger.error(repr(e)) + return False + + def init_serial_commands(self): + self.command("E0") #echo off + self.command("+CMEE", "2") #set verbose error codes + self.command("+CPIN?") + self.set_timezone_configs() + #self.command("+CPIN", "") #set SIM PIN + self.command("+CPMS", "\"ME\",\"ME\",\"ME\"") + self.set_sms_configs() + self.set_network_registration_status() + + def set_network_registration_status(self): + self.command("+CREG", "2") + self.command("+CEREG", "2") + + def _set_up_pdp_context(self): + if self._is_pdp_context_active(): return True + self.logger.info('Setting up PDP context') + self.set('+QICSGP', f'1,1,\"{self._apn}\",\"\",\"\",1') + ok, _ = self.set('+QIACT', '1', timeout=30) + if ok != ModemResult.OK: + self.logger.error('PDP Context setup failed') + raise NetworkError('Failed PDP context setup') + else: + self.logger.info('PDP context active') + + def _tear_down_pdp_context(self): + if not self._is_pdp_context_active(): return True + self.logger.info('Tearing down PDP context') + ok, _ = self.set('+QIDEACT', '1', timeout=30) + if ok != ModemResult.OK: + self.logger.error('PDP Context tear down failed') + else: + self.logger.info('PDP context deactivated') + + @property + def description(self): + return 'Quectel EC21' diff --git a/Hologram/Network/Modem/Modem.py b/Hologram/Network/Modem/Modem.py index 37896f1..c55468c 100644 --- a/Hologram/Network/Modem/Modem.py +++ b/Hologram/Network/Modem/Modem.py @@ -865,14 +865,16 @@ def modem_id(self): @property def iccid(self): - return self._basic_command('+CCID') + return self._basic_command('+CCID').rstrip('F') @property def operator(self): - op = self._basic_set('+UDOPN','12') - if op is not None: - return op.strip('"') - return op + ret = self._basic_command('+COPS?') + if ret is not None: + parts = ret.split(',') + if len(parts) >= 3: + return parts[2].strip('"') + return None @property def location(self): diff --git a/Hologram/Network/Modem/NovaM.py b/Hologram/Network/Modem/NovaM.py index af7ddf5..9fbb95e 100644 --- a/Hologram/Network/Modem/NovaM.py +++ b/Hologram/Network/Modem/NovaM.py @@ -57,16 +57,6 @@ def description(self): def location(self): raise NotImplementedError('The R404 and R410 do not support Cell Locate at this time') - @property - def operator(self): - # R4 series doesn't have UDOPN so need to override - ret = self._basic_command('+COPS?') - parts = ret.split(',') - if len(parts) >= 3: - return parts[2].strip('"') - return None - - # same as Modem::connect_socket except with longer timeout def connect_socket(self, host, port): at_command_val = "%d,\"%s\",%s" % (self.socket_identifier, host, port) diff --git a/Hologram/Network/Modem/Nova_U201.py b/Hologram/Network/Modem/Nova_U201.py index b2a315a..b3e3ddc 100644 --- a/Hologram/Network/Modem/Nova_U201.py +++ b/Hologram/Network/Modem/Nova_U201.py @@ -134,3 +134,10 @@ def location(self): @property def description(self): return 'Hologram Nova Global 3G/2G Cellular USB Modem (U201)' + + @property + def operator(self): + op = self._basic_set('+UDOPN','12') + if op is not None: + return op.strip('"') + return op \ No newline at end of file diff --git a/Hologram/Network/Modem/__init__.py b/Hologram/Network/Modem/__init__.py index fe44d41..84491b7 100644 --- a/Hologram/Network/Modem/__init__.py +++ b/Hologram/Network/Modem/__init__.py @@ -1,4 +1,4 @@ -__all__ = ['Modem', 'MockModem', 'MS2131', 'Nova', 'E303', 'E372'] +__all__ = ['Modem', 'MockModem', 'MS2131', 'Nova', 'E303', 'E372', 'EC21', 'BG96'] from .IModem import IModem from .Modem import Modem