Skip to content

Commit c1af498

Browse files
committed
Hologram Python SDK v0.8.0 release
1 parent ceb64be commit c1af498

File tree

17 files changed

+272
-115
lines changed

17 files changed

+272
-115
lines changed

CHANGELOG.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,14 @@
11
# What's New in Hologram Python SDK
22

3+
## v0.8.0
4+
5+
2018-07-19 Hologram <[email protected]>
6+
* Add support for Nova R410 Cat-M modem
7+
* Drivers for R410 and R404 are loaded automatically
8+
* Revamp error handling on AT command connect so we don't keep
9+
moving the process forward if we've already failed
10+
* Fixes to the repeated message send feature
11+
312
## v0.7.6
413

514
2018-04-04 Hologram <[email protected]>

Exceptions/HologramError.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,9 @@ class AuthenticationError(HologramError):
1919
class NetworkError(HologramError):
2020
pass
2121

22+
class ModemError(HologramError):
23+
pass
24+
2225
class PPPError(NetworkError):
2326
pass
2427

Hologram/Cloud.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@
1313
from Network import NetworkManager
1414
from Authentication import *
1515

16-
__version__ = '0.7.6'
16+
__version__ = '0.8.0'
1717

1818
class Cloud(object):
1919

Hologram/CustomCloud.py

Lines changed: 34 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -99,25 +99,22 @@ def open_send_socket(self, timeout=SEND_TIMEOUT):
9999
if self._is_send_socket_open:
100100
return
101101

102-
try:
103-
self.__enforce_send_host_and_port()
104-
self.logger.info("Connecting to: %s", self.send_host)
105-
self.logger.info("Port: %s", self.send_port)
102+
self.__enforce_send_host_and_port()
103+
self.logger.info("Connecting to: %s", self.send_host)
104+
self.logger.info("Port: %s", self.send_port)
106105

107-
# Check if we're going to use the AT command version of sockets or the
108-
# native Python socket lib.
109-
if self.__to_use_at_sockets():
110-
self.network.create_socket()
111-
self.network.connect_socket(self.send_host, self.send_port)
112-
else:
113-
self.sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
114-
self.sock.settimeout(timeout)
115-
self.sock.connect((self.send_host, self.send_port))
106+
# Check if we're going to use the AT command version of sockets or the
107+
# native Python socket lib.
108+
if self.__to_use_at_sockets():
109+
self.network.create_socket()
110+
self.network.connect_socket(self.send_host, self.send_port)
111+
else:
112+
self.sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
113+
self.sock.settimeout(timeout)
114+
self.sock.connect((self.send_host, self.send_port))
115+
116+
self._is_send_socket_open = True
116117

117-
self._is_send_socket_open = True
118-
except (IOError):
119-
self.logger.error('An error occurred while attempting to send the message to the cloud')
120-
self.logger.error('Please try again.')
121118

122119
def close_send_socket(self):
123120
try:
@@ -177,7 +174,6 @@ def sendPeriodicMessage(self, interval, message, topics=None, timeout=5):
177174
self._periodic_msg = threading.Thread(target=self._periodic_job_thread,
178175
args=[interval, self.sendMessage,
179176
message, topics, timeout])
180-
self._periodic_msg.daemon = True
181177
self._periodic_msg.start()
182178

183179
def sendSMS(self, destination_number, message):
@@ -190,7 +186,6 @@ def __to_use_at_sockets(self):
190186
# EFFECTS: This threaded infinite loop shoud keep sending messages with the specified
191187
# interval.
192188
def _periodic_job_thread(self, interval, function, *args):
193-
194189
while True:
195190
self._periodic_msg_lock.acquire()
196191

@@ -199,8 +194,17 @@ def _periodic_job_thread(self, interval, function, *args):
199194
break
200195

201196
self.logger.info('Sending another periodic message...')
202-
response = function(*args)
203-
self.logger.info('DATA RECEIVED: %s', str(response))
197+
try:
198+
response = function(*args)
199+
except Exception as e:
200+
self.logger.info('Message function threw an exception: %s', str(e))
201+
self._periodic_msg_lock.release()
202+
break
203+
else:
204+
self.logger.info('RESPONSE MESSAGE: %s', self.getResultString(response))
205+
if not self.resultWasSuccess(response):
206+
self._periodic_msg_lock.release()
207+
break
204208

205209
self._periodic_msg_lock.release()
206210
time.sleep(interval)
@@ -215,6 +219,9 @@ def stopPeriodicMessage(self):
215219
self._periodic_msg.join()
216220
self.logger.info('Periodic job stopped')
217221

222+
def periodicMessageRunning(self):
223+
return self._periodic_msg and self._periodic_msg.isAlive()
224+
218225
def initializeReceiveSocket(self):
219226
return self.openReceiveSocket()
220227

@@ -386,3 +393,9 @@ def _enforce_minimum_periodic_interval(self, interval):
386393
def __enforce_network_disconnected(self):
387394
if self.network_type == 'Cellular':
388395
self.network.disconnect()
396+
397+
def getResultString(self, result_code):
398+
return str(response)
399+
400+
def resultWasSuccess(self, result_code):
401+
return True

Hologram/HologramCloud.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,8 @@
2828

2929
# Hologram error codes
3030
ERR_OK = 0
31-
ERR_CONNCLOSED = 1 # Connection was closed so we couldn't read enough
31+
ERR_CONNCLOSED = 1 # Connection was closed before a terminating character
32+
# but message might be fine
3233
ERR_MSGINVALID = 2 # Couldn't parse the message
3334
ERR_AUTHINVALID = 3 # Auth section of message was invalid
3435
ERR_PAYLOADINVALID = 4 # Payload type was invalid
@@ -232,3 +233,6 @@ def getResultString(self, result_code):
232233
if result_code not in self._errorCodeDescription:
233234
return 'Unknown response code'
234235
return self._errorCodeDescription[result_code]
236+
237+
def resultWasSuccess(self, result_code):
238+
return result_code in (ERR_OK, ERR_CONNCLOSED)

Hologram/Network/Cellular.py

Lines changed: 23 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,8 @@
1515
from Modem import E303
1616
from Modem import MS2131
1717
from Modem import Nova_U201
18-
from Modem import NovaM_R404
18+
from Modem import NovaM
19+
from Modem import DriverLoader
1920
from Network import Network, NetworkScope
2021
import time
2122
from serial.tools import list_ports
@@ -35,7 +36,7 @@ class Cellular(Network):
3536
'e303': E303.E303,
3637
'ms2131': MS2131.MS2131,
3738
'nova': Nova_U201.Nova_U201,
38-
'r404': NovaM_R404.NovaM_R404,
39+
'novam': NovaM.NovaM,
3940
'': Modem
4041
}
4142

@@ -46,13 +47,17 @@ def __init__(self, event=Event()):
4647
self._route = Route()
4748
self.__receive_port = None
4849

50+
4951
def autodetect_modem(self):
5052
# scan for a modem and set it if found
5153
dev_devices = self._scan_for_modems()
5254
if dev_devices is None:
5355
raise NetworkError('Modem not detected')
5456
self.modem = dev_devices[0]
5557

58+
def load_modem_drivers(self):
59+
self._load_modem_drivers()
60+
5661

5762
def getConnectionStatus(self):
5863
return self._connection_status
@@ -173,6 +178,22 @@ def __configure_routing(self):
173178
self.logger.info('Adding system-wide default route to cellular interface')
174179
self._route.add_default(self.localIPAddress)
175180

181+
def _load_modem_drivers(self):
182+
dl = DriverLoader.DriverLoader()
183+
for (modemName, modemHandler) in self._modemHandlers.iteritems():
184+
module = modemHandler.module
185+
if module:
186+
if not dl.is_module_loaded(module):
187+
self.logger.info('Loading module %s for %s', module, modemName)
188+
dl.load_module(module)
189+
syspath = modemHandler.syspath
190+
if syspath:
191+
usb_ids = modemHandler.usb_ids
192+
for vid_pid in usb_ids:
193+
dl.force_driver_for_device(syspath, vid_pid[0], vid_pid[1])
194+
195+
196+
176197
def _scan_for_modems(self):
177198
res = None
178199
for (modemName, modemHandler) in self._modemHandlers.iteritems():
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
# DriverLoader.py - Class that wraps around different module loading
2+
# operations that are used for the R410 and maybe other modems in the
3+
# future
4+
# Author: Hologram <[email protected]>
5+
#
6+
#
7+
# Copyright 2018 - Hologram (Konekt, Inc.)
8+
#
9+
#
10+
# LICENSE: Distributed under the terms of the MIT License
11+
#
12+
13+
import subprocess
14+
15+
16+
class DriverLoader(object):
17+
# I would much rather use python-kmod for all this
18+
# but it doesn't seem to build properly on the Pi and
19+
# hasn't been updated in years. It's possible we need to update
20+
# to python 3 for it to work correctly
21+
22+
23+
def is_module_loaded(self, module):
24+
output = subprocess.check_output(['lsmod'])
25+
lines = output.splitlines()
26+
for line in lines:
27+
splitline = line.split()
28+
if splitline[0] == "option":
29+
return True
30+
return False
31+
32+
33+
def load_module(self, module):
34+
subprocess.call(['sudo', 'modprobe', module])
35+
36+
37+
def force_driver_for_device(self, syspath, vid, pid):
38+
with open(syspath, "w") as f:
39+
f.write("%s %s"%(vid, pid))
40+
41+
42+
43+
44+

Hologram/Network/Modem/IModem.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,10 @@
2020
class IModem(object):
2121

2222
usb_ids = []
23+
# module needed by modem
24+
module = ''
25+
# system path to write usb IDs to to force use of a driver
26+
syspath = ''
2327

2428
_error_code_description = {
2529

Hologram/Network/Modem/Modem.py

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
from Hologram.Event import Event
1616
from Exceptions.HologramError import SerialError
1717
from Exceptions.HologramError import HologramError
18+
from Exceptions.HologramError import NetworkError
1819

1920
from collections import deque
2021
import binascii
@@ -281,6 +282,7 @@ def connect_socket(self, host, port):
281282
ok, _ = self.set('+USOCO', at_command_val, timeout=20)
282283
if ok != ModemResult.OK:
283284
self.logger.error('Failed to connect socket')
285+
raise NetworkError('Failed to connect socket')
284286
else:
285287
self.logger.info('Connect socket is successful')
286288

@@ -291,6 +293,7 @@ def listen_socket(self, port):
291293
ok, _ = self.set('+USOLI', at_command_val, timeout=5)
292294
if ok != ModemResult.OK:
293295
self.logger.error('Failed to listen socket')
296+
raise NetworkError('Failed to listen socket')
294297

295298
def write_socket(self, data):
296299

@@ -299,6 +302,7 @@ def write_socket(self, data):
299302
ok, _ = self.set('+USOWR', value, timeout=10)
300303
if ok != ModemResult.OK:
301304
self.logger.error('Failed to write to socket')
305+
raise NetworkError('Failed to write socket')
302306
self.disable_hex_mode()
303307

304308
def read_socket(self, socket_identifier=None, payload_length=None):
@@ -681,9 +685,10 @@ def _set_up_pdp_context(self):
681685
ok, _ = self.set('+UPSDA', '0,3', timeout=30)
682686
if ok != ModemResult.OK:
683687
self.logger.error('PDP Context setup failed')
688+
raise NetworkError('Failed PDP context setup')
684689
else:
685690
self.logger.info('PDP context active')
686-
return ok == ModemResult.OK
691+
687692

688693
def __enforce_serial_port_open(self):
689694
if not (self.serial_port and self.serial_port.isOpen()):

Hologram/Network/Modem/NovaM.py

Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
# NovaM.py - Hologram Python SDK Hologram Nova R404/R410 modem interface
2+
#
3+
# Author: Hologram <[email protected]>
4+
#
5+
# Copyright 2016-2018 - Hologram, Inc
6+
#
7+
#
8+
# LICENSE: Distributed under the terms of the MIT License
9+
#
10+
11+
from Nova import Nova
12+
from Hologram.Event import Event
13+
from Exceptions.HologramError import NetworkError
14+
from UtilClasses import ModemResult
15+
16+
DEFAULT_NOVAM_TIMEOUT = 200
17+
18+
class NovaM(Nova):
19+
20+
usb_ids = [('05c6', '90b2')]
21+
module = 'option'
22+
syspath = '/sys/bus/usb-serial/drivers/option1/new_id'
23+
24+
def __init__(self, device_name=None, baud_rate='9600',
25+
chatscript_file=None, event=Event()):
26+
super(NovaM, self).__init__(device_name=device_name, baud_rate=baud_rate,
27+
chatscript_file=chatscript_file, event=event)
28+
self._at_sockets_available = True
29+
modem_id = self.modem_id
30+
if("R404" in modem_id):
31+
self.is_r410 = False
32+
else:
33+
self.is_r410 = True
34+
35+
36+
def init_serial_commands(self):
37+
self.command("E0") #echo off
38+
self.command("+CMEE", "2") #set verbose error codes
39+
self.command("+CPIN?")
40+
self.command("+CPMS", "\"ME\",\"ME\",\"ME\"")
41+
self.set_sms_configs()
42+
self.set_network_registration_status()
43+
44+
def set_network_registration_status(self):
45+
self.command("+CEREG", "2")
46+
47+
def is_registered(self):
48+
return self.check_registered('+CEREG')
49+
50+
def close_socket(self, socket_identifier=None):
51+
52+
if socket_identifier is None:
53+
socket_identifier = self.socket_identifier
54+
55+
ok, r = self.set('+USOCL', "%s" % socket_identifier, timeout=40)
56+
if ok != ModemResult.OK:
57+
self.logger.error('Failed to close socket')
58+
59+
@property
60+
def description(self):
61+
modemtype = '(R410)' if self.is_r410 else '(R404)'
62+
return 'Hologram Nova US 4G LTE Cat-M1 Cellular USB Modem ' + modemtype
63+
64+
@property
65+
def location(self):
66+
raise NotImplementedError('The R404 and R410 do not support Cell Locate at this time')
67+
68+
@property
69+
def operator(self):
70+
# R4 series doesn't have UDOPN so need to override
71+
ret = self._basic_command('+COPS?')
72+
parts = ret.split(',')
73+
if len(parts) >= 3:
74+
return parts[2].strip('"')
75+
return None
76+
77+
78+
# same as Modem::connect_socket except with longer timeout
79+
def connect_socket(self, host, port):
80+
at_command_val = "%d,\"%s\",%s" % (self.socket_identifier, host, port)
81+
ok, _ = self.set('+USOCO', at_command_val, timeout=122)
82+
if ok != ModemResult.OK:
83+
self.logger.error('Failed to connect socket')
84+
raise NetworkError('Failed to connect socket')
85+
else:
86+
self.logger.info('Connect socket is successful')

0 commit comments

Comments
 (0)