Skip to content

Commit

Permalink
Add EC21 to supported modems (#109)
Browse files Browse the repository at this point in the history
  • Loading branch information
DomAmato committed Nov 10, 2022
1 parent a52de20 commit a3bf1cb
Show file tree
Hide file tree
Showing 6 changed files with 208 additions and 17 deletions.
3 changes: 2 additions & 1 deletion Hologram/Network/Cellular.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down
191 changes: 191 additions & 0 deletions Hologram/Network/Modem/EC21.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,191 @@
# EC21.py - Hologram Python SDK Quectel EC21 modem interface
#
# Author: Hologram <support@hologram.io>
#
# 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: <connectID>,<err>”

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'
12 changes: 7 additions & 5 deletions Hologram/Network/Modem/Modem.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down
10 changes: 0 additions & 10 deletions Hologram/Network/Modem/NovaM.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
7 changes: 7 additions & 0 deletions Hologram/Network/Modem/Nova_U201.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
2 changes: 1 addition & 1 deletion Hologram/Network/Modem/__init__.py
Original file line number Diff line number Diff line change
@@ -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

0 comments on commit a3bf1cb

Please sign in to comment.