Skip to content

Commit a3bf1cb

Browse files
authored
Add EC21 to supported modems (#109)
1 parent a52de20 commit a3bf1cb

File tree

6 files changed

+208
-17
lines changed

6 files changed

+208
-17
lines changed

Hologram/Network/Cellular.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111
from Hologram.Event import Event
1212
from Exceptions.HologramError import NetworkError
1313
from Hologram.Network.Route import Route
14-
from Hologram.Network.Modem import Modem, E303, MS2131, E372, BG96, Nova_U201, NovaM, DriverLoader
14+
from Hologram.Network.Modem import Modem, E303, MS2131, E372, BG96, EC21, Nova_U201, NovaM, DriverLoader
1515
from Hologram.Network import Network, NetworkScope
1616
import time
1717
from serial.tools import list_ports
@@ -32,6 +32,7 @@ class Cellular(Network):
3232
'ms2131': MS2131.MS2131,
3333
'e372': E372.E372,
3434
'bg96': BG96.BG96,
35+
'ec21': EC21.EC21,
3536
'nova': Nova_U201.Nova_U201,
3637
'novam': NovaM.NovaM,
3738
'': Modem

Hologram/Network/Modem/EC21.py

Lines changed: 191 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,191 @@
1+
# EC21.py - Hologram Python SDK Quectel EC21 modem interface
2+
#
3+
# Author: Hologram <[email protected]>
4+
#
5+
# Copyright 2016 - Hologram (Konekt, Inc.)
6+
#
7+
#
8+
# LICENSE: Distributed under the terms of the MIT License
9+
#
10+
import binascii
11+
import time
12+
13+
from serial.serialutil import Timeout
14+
15+
from Hologram.Network.Modem import Modem
16+
from Hologram.Event import Event
17+
from UtilClasses import ModemResult
18+
from Exceptions.HologramError import SerialError, NetworkError
19+
20+
DEFAULT_EC21_TIMEOUT = 200
21+
22+
class EC21(Modem):
23+
usb_ids = [('2c7c', '0121')]
24+
25+
def __init__(self, device_name=None, baud_rate='9600',
26+
chatscript_file=None, event=Event()):
27+
28+
super().__init__(device_name=device_name, baud_rate=baud_rate,
29+
chatscript_file=chatscript_file, event=event)
30+
self._at_sockets_available = True
31+
self.urc_response = ''
32+
33+
def connect(self, timeout=DEFAULT_EC21_TIMEOUT):
34+
success = super().connect(timeout)
35+
return success
36+
37+
def send_message(self, data, timeout=Modem.DEFAULT_SEND_TIMEOUT):
38+
# Waiting for the open socket urc
39+
while self.urc_state != Modem.SOCKET_WRITE_STATE:
40+
self.checkURC()
41+
42+
self.write_socket(data)
43+
44+
loop_timeout = Timeout(timeout)
45+
while self.urc_state != Modem.SOCKET_SEND_READ:
46+
self.checkURC()
47+
if self.urc_state != Modem.SOCKET_SEND_READ:
48+
if loop_timeout.expired():
49+
raise SerialError('Timeout occurred waiting for message status')
50+
time.sleep(self._RETRY_DELAY)
51+
elif self.urc_state == Modem.SOCKET_CLOSED:
52+
return '[1,0]' #this is connection closed for hologram cloud response
53+
54+
return self.urc_response.rstrip('\r\n')
55+
56+
def create_socket(self):
57+
self._set_up_pdp_context()
58+
59+
def connect_socket(self, host, port):
60+
self.command('+QIOPEN', '1,0,\"TCP\",\"%s\",%d,0,1' % (host, port))
61+
# According to the EC21 Docs
62+
# Have to wait for URC response “+QIOPEN: <connectID>,<err>”
63+
64+
def close_socket(self, socket_identifier=None):
65+
ok, _ = self.command('+QICLOSE', self.socket_identifier)
66+
if ok != ModemResult.OK:
67+
self.logger.error('Failed to close socket')
68+
self.urc_state = Modem.SOCKET_CLOSED
69+
self._tear_down_pdp_context()
70+
71+
def write_socket(self, data):
72+
hexdata = binascii.hexlify(data)
73+
# We have to do it in chunks of 510 since 512 is actually too long (CMEE error)
74+
# and we need 2n chars for hexified data
75+
for chunk in self._chunks(hexdata, 510):
76+
value = '%d,\"%s\"' % (self.socket_identifier, chunk.decode())
77+
ok, _ = self.set('+QISENDEX', value, timeout=10)
78+
if ok != ModemResult.OK:
79+
self.logger.error('Failed to write to socket')
80+
raise NetworkError('Failed to write to socket')
81+
82+
def read_socket(self, socket_identifier=None, payload_length=None):
83+
84+
if socket_identifier is None:
85+
socket_identifier = self.socket_identifier
86+
87+
if payload_length is None:
88+
payload_length = self.last_read_payload_length
89+
90+
ok, resp = self.set('+QIRD', '%d,%d' % (socket_identifier, payload_length))
91+
if ok == ModemResult.OK:
92+
resp = resp.lstrip('+QIRD: ')
93+
if resp is not None:
94+
resp = resp.strip('"')
95+
try:
96+
resp = resp.decode()
97+
except:
98+
# This is some sort of binary data that can't be decoded so just
99+
# return the bytes. We might want to make this happen via parameter
100+
# in the future so it is more deterministic
101+
self.logger.debug('Could not decode recieved data')
102+
103+
return resp
104+
105+
def listen_socket(self, port):
106+
# No equivilent exists for quectel modems
107+
pass
108+
109+
def is_registered(self):
110+
return self.check_registered('+CREG') or self.check_registered('+CEREG')
111+
112+
# EFFECTS: Handles URC related AT command responses.
113+
def handleURC(self, urc):
114+
if urc.startswith('+QIOPEN: '):
115+
response_list = urc.lstrip('+QIOPEN: ').split(',')
116+
socket_identifier = int(response_list[0])
117+
err = int(response_list[-1])
118+
if err == 0:
119+
self.urc_state = Modem.SOCKET_WRITE_STATE
120+
self.socket_identifier = socket_identifier
121+
else:
122+
self.logger.error('Failed to open socket')
123+
raise NetworkError('Failed to open socket')
124+
return
125+
if urc.startswith('+QIURC: '):
126+
response_list = urc.lstrip('+QIURC: ').split(',')
127+
urctype = response_list[0]
128+
if urctype == '\"recv\"':
129+
self.urc_state = Modem.SOCKET_SEND_READ
130+
self.socket_identifier = int(response_list[1])
131+
self.last_read_payload_length = int(response_list[2])
132+
self.urc_response = self._readline_from_serial_port(5)
133+
if urctype == '\"closed\"':
134+
self.urc_state = Modem.SOCKET_CLOSED
135+
self.socket_identifier = int(response_list[-1])
136+
return
137+
super().handleURC(urc)
138+
139+
def _is_pdp_context_active(self):
140+
if not self.is_registered():
141+
return False
142+
143+
ok, r = self.command('+QIACT?')
144+
if ok == ModemResult.OK:
145+
try:
146+
pdpstatus = int(r.lstrip('+QIACT: ').split(',')[1])
147+
# 1: PDP active
148+
return pdpstatus == 1
149+
except (IndexError, ValueError) as e:
150+
self.logger.error(repr(e))
151+
except AttributeError as e:
152+
self.logger.error(repr(e))
153+
return False
154+
155+
def init_serial_commands(self):
156+
self.command("E0") #echo off
157+
self.command("+CMEE", "2") #set verbose error codes
158+
self.command("+CPIN?")
159+
self.set_timezone_configs()
160+
#self.command("+CPIN", "") #set SIM PIN
161+
self.command("+CPMS", "\"ME\",\"ME\",\"ME\"")
162+
self.set_sms_configs()
163+
self.set_network_registration_status()
164+
165+
def set_network_registration_status(self):
166+
self.command("+CREG", "2")
167+
self.command("+CEREG", "2")
168+
169+
def _set_up_pdp_context(self):
170+
if self._is_pdp_context_active(): return True
171+
self.logger.info('Setting up PDP context')
172+
self.set('+QICSGP', f'1,1,\"{self._apn}\",\"\",\"\",1')
173+
ok, _ = self.set('+QIACT', '1', timeout=30)
174+
if ok != ModemResult.OK:
175+
self.logger.error('PDP Context setup failed')
176+
raise NetworkError('Failed PDP context setup')
177+
else:
178+
self.logger.info('PDP context active')
179+
180+
def _tear_down_pdp_context(self):
181+
if not self._is_pdp_context_active(): return True
182+
self.logger.info('Tearing down PDP context')
183+
ok, _ = self.set('+QIDEACT', '1', timeout=30)
184+
if ok != ModemResult.OK:
185+
self.logger.error('PDP Context tear down failed')
186+
else:
187+
self.logger.info('PDP context deactivated')
188+
189+
@property
190+
def description(self):
191+
return 'Quectel EC21'

Hologram/Network/Modem/Modem.py

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -865,14 +865,16 @@ def modem_id(self):
865865

866866
@property
867867
def iccid(self):
868-
return self._basic_command('+CCID')
868+
return self._basic_command('+CCID').rstrip('F')
869869

870870
@property
871871
def operator(self):
872-
op = self._basic_set('+UDOPN','12')
873-
if op is not None:
874-
return op.strip('"')
875-
return op
872+
ret = self._basic_command('+COPS?')
873+
if ret is not None:
874+
parts = ret.split(',')
875+
if len(parts) >= 3:
876+
return parts[2].strip('"')
877+
return None
876878

877879
@property
878880
def location(self):

Hologram/Network/Modem/NovaM.py

Lines changed: 0 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -57,16 +57,6 @@ def description(self):
5757
def location(self):
5858
raise NotImplementedError('The R404 and R410 do not support Cell Locate at this time')
5959

60-
@property
61-
def operator(self):
62-
# R4 series doesn't have UDOPN so need to override
63-
ret = self._basic_command('+COPS?')
64-
parts = ret.split(',')
65-
if len(parts) >= 3:
66-
return parts[2].strip('"')
67-
return None
68-
69-
7060
# same as Modem::connect_socket except with longer timeout
7161
def connect_socket(self, host, port):
7262
at_command_val = "%d,\"%s\",%s" % (self.socket_identifier, host, port)

Hologram/Network/Modem/Nova_U201.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -134,3 +134,10 @@ def location(self):
134134
@property
135135
def description(self):
136136
return 'Hologram Nova Global 3G/2G Cellular USB Modem (U201)'
137+
138+
@property
139+
def operator(self):
140+
op = self._basic_set('+UDOPN','12')
141+
if op is not None:
142+
return op.strip('"')
143+
return op

Hologram/Network/Modem/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
__all__ = ['Modem', 'MockModem', 'MS2131', 'Nova', 'E303', 'E372']
1+
__all__ = ['Modem', 'MockModem', 'MS2131', 'Nova', 'E303', 'E372', 'EC21', 'BG96']
22

33
from .IModem import IModem
44
from .Modem import Modem

0 commit comments

Comments
 (0)