Skip to content

Commit 5965489

Browse files
committed
Hologram Python SDK v0.8.2 release
1 parent 69710f9 commit 5965489

File tree

14 files changed

+163
-112
lines changed

14 files changed

+163
-112
lines changed

CHANGELOG.md

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

3+
## v0.8.2
4+
5+
2018-11-16 Hologram <[email protected]>
6+
* Added new commands to control cellular radio: `hologram modem radio-off`
7+
and `hologram modem radio-on`
8+
* Added new command to restart the modem: `hologram modem reset`
9+
* Deprecated `hologram modem connect` and `hologram modem disconnect` in
10+
favor of the network versions of those commands. They'll be removed
11+
in a future release
12+
* Fix issue with auto periodic message sending taking a long time to stop
13+
when killed
14+
* Fix issue where `hologram network connect` would print that it worked
15+
even when it didn't
16+
* Fix issue where a zombie pppd process would hang around in the background
17+
after a connection failure and tie up a serial port
18+
* General code cleanup around the PPP path
19+
320
## v0.8.1
21+
422
2018-09-12 Hologram <[email protected]>
523
* Fix issues with PPP (`hologram network connect`) on Nova R410
624
* Fix bugs with activating SIM cards via `hologram activate`

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.8.1'
16+
__version__ = '0.8.2'
1717

1818
class Cloud(object):
1919

Hologram/CustomCloud.py

Lines changed: 13 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -38,9 +38,12 @@ def __init__(self, credentials, send_host='', send_port=0,
3838
if enable_inbound and (receive_host == '' or receive_port == 0):
3939
raise HologramError('Must set receive host and port for inbound connection')
4040

41-
self._periodic_msg_lock = threading.Lock()
4241
self._periodic_msg = None
43-
self._periodic_msg_enabled = False
42+
# We start with the event set, clear it when running and then set when
43+
# shutting down. This way, the thread can wait on it and stop immediately
44+
# when the script is exiting
45+
self._periodic_msg_disabled = threading.Event()
46+
self._periodic_msg_disabled.set()
4447

4548
self._receive_buffer_lock = threading.Lock()
4649
self._receive_cv = threading.Lock()
@@ -158,14 +161,9 @@ def sendPeriodicMessage(self, interval, message, topics=None, timeout=5):
158161
try:
159162
self._enforce_minimum_periodic_interval(interval)
160163

161-
self._periodic_msg_lock.acquire()
162-
163-
if self._periodic_msg_enabled == True:
164+
if not self._periodic_msg_disabled.is_set():
164165
raise HologramError('Cannot have more than 1 periodic message job at once')
165-
166-
self._periodic_msg_enabled = True
167-
168-
self._periodic_msg_lock.release()
166+
self._periodic_msg_disabled.clear()
169167

170168
except Exception as e:
171169
self.__enforce_network_disconnected()
@@ -186,35 +184,27 @@ def __to_use_at_sockets(self):
186184
# EFFECTS: This threaded infinite loop shoud keep sending messages with the specified
187185
# interval.
188186
def _periodic_job_thread(self, interval, function, *args):
189-
while True:
190-
self._periodic_msg_lock.acquire()
191-
192-
if not self._periodic_msg_enabled:
193-
self._periodic_msg_lock.release()
194-
break
195-
187+
while not self._periodic_msg_disabled.is_set():
196188
self.logger.info('Sending another periodic message...')
197189
try:
198190
response = function(*args)
199191
except Exception as e:
200192
self.logger.info('Message function threw an exception: %s', str(e))
201-
self._periodic_msg_lock.release()
202193
break
203194
else:
204195
self.logger.info('RESPONSE MESSAGE: %s', self.getResultString(response))
205196
if not self.resultWasSuccess(response):
206-
self._periodic_msg_lock.release()
207197
break
208198

209-
self._periodic_msg_lock.release()
210-
time.sleep(interval)
199+
self._periodic_msg_disabled.wait(interval)
200+
self.logger.debug('Periodic job thread stopping')
201+
# in case we exited with an exception
202+
self._periodic_msg_disabled.set()
211203

212204
def stopPeriodicMessage(self):
213205
self.logger.info('Stopping periodic job...')
214206

215-
self._periodic_msg_lock.acquire()
216-
self._periodic_msg_enabled = False
217-
self._periodic_msg_lock.release()
207+
self._periodic_msg_disabled.set()
218208

219209
self._periodic_msg.join()
220210
self.logger.info('Periodic job stopped')

Hologram/Network/Modem/IModem.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,12 @@ def connect(self):
6161
def disconnect(self):
6262
raise NotImplementedError('Must instantiate a Modem type')
6363

64+
def reset(self):
65+
raise NotImplementedError('Must instantiate a Modem type')
66+
67+
def radio_power(self, power_mode):
68+
raise NotImplementedError('Must instantiate a Modem type')
69+
6470
def enableSMS(self):
6571
raise NotImplementedError('Must instantiate a Modem type')
6672

Hologram/Network/Modem/Modem.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -147,7 +147,6 @@ def closeSerialPort(self):
147147
except Exception:
148148
self.logger.error('Failed to close serial port')
149149

150-
151150
# EFFECTS: backwards compatibility only
152151
def enableSMS(self):
153152
self.checkURC()
@@ -235,6 +234,11 @@ def set_network_registration_status(self):
235234
def reset(self):
236235
self.set('+CFUN', '16') # restart the modem
237236

237+
def radio_power(self, power_mode):
238+
cfun_val = '1' if power_mode else '0'
239+
ok, r = self.command('+CFUN', cfun_val, timeout=5)
240+
return ok == ModemResult.OK
241+
238242
def send_message(self, data, timeout=DEFAULT_SEND_TIMEOUT):
239243

240244
self.urc_state = Modem.SOCKET_INIT

Hologram/Network/Modem/ModemMode/PPP.py

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ def __init__(self, device_name='/dev/ttyUSB0', all_attached_device_names=[],
3030
self.route = Route()
3131
self.all_attached_device_names = all_attached_device_names
3232
self._ppp = PPPConnection(self.device_name, self.baud_rate, 'noipdefault',
33-
'usepeerdns', 'defaultroute', 'persist', 'noauth',
33+
'usepeerdns', 'persist', 'noauth',
3434
connect=self.connect_script)
3535

3636
def isConnected(self):
@@ -44,15 +44,22 @@ def connect(self, timeout=DEFAULT_PPP_TIMEOUT):
4444

4545
result = self._ppp.connect(timeout=timeout)
4646

47-
if result == True and self.route.wait_for_interface(DEFAULT_PPP_INTERFACE,
48-
MAX_PPP_INTERFACE_UP_RETRIES):
47+
if result == True:
48+
if not self.route.wait_for_interface(DEFAULT_PPP_INTERFACE,
49+
MAX_PPP_INTERFACE_UP_RETRIES):
50+
self.logger.error('Unable to find interface %s. Disconnecting',
51+
DEFAULT_PPP_INTERFACE)
52+
self._ppp.disconnect()
53+
return False
4954
return True
5055
else:
5156
return False
5257

58+
5359
def disconnect(self):
60+
self._ppp.disconnect()
5461
self.__shut_down_existing_ppp_session()
55-
return self._ppp.disconnect()
62+
return True
5663

5764
# EFFECTS: Makes sure that there are no existing PPP instances on the same
5865
# device interface.
@@ -79,7 +86,6 @@ def __check_for_existing_ppp_sessions(self):
7986
self.logger.info('Checking for existing PPP sessions')
8087

8188
for proc in psutil.process_iter():
82-
8389
try:
8490
pinfo = proc.as_dict(attrs=['pid', 'name'])
8591
except:

Hologram/Network/Modem/ModemMode/pppd.py

Lines changed: 35 additions & 74 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
import threading
1818
from subprocess import Popen, PIPE, STDOUT
1919
from Exceptions.HologramError import PPPError, PPPConnectionError
20+
import errno
2021

2122
__version__ = '1.0.3'
2223
DEFAULT_CONNECT_TIMEOUT = 200
@@ -29,20 +30,18 @@ def __repr__(self):
2930
def __init__(self, *args, **kwargs):
3031
# Logging setup.
3132
self.logger = logging.getLogger(__name__)
32-
self.logger.addHandler(NullHandler())
3333

3434
self._laddr = None
3535
self._raddr = None
36-
self._connectThread = None
37-
self._outputRWLock = threading.Condition()
38-
self._outputReadyToRead = True
3936
self.proc = None
4037

4138
self.output = ''
4239

4340
self._commands = []
4441

45-
if kwargs.pop('sudo', True):
42+
# This makes it harder to kill pppd so we're defaulting to it off for now
43+
# It's redudant anyway for the CLI
44+
if kwargs.pop('sudo', False):
4645
sudo_path = kwargs.pop('sudo_path', '/usr/bin/sudo')
4746
if not os.path.isfile(sudo_path) or not os.access(sudo_path, os.X_OK):
4847
raise IOError('%s not found' % sudo_path)
@@ -66,73 +65,62 @@ def __init__(self, *args, **kwargs):
6665
# Returns true if successful, false otherwise.
6766
def connect(self, timeout = DEFAULT_CONNECT_TIMEOUT):
6867

68+
self.logger.info('Starting pppd')
6969
self.proc = Popen(self._commands, stdout=PIPE, stderr=STDOUT, universal_newlines=True)
7070

7171
# set stdout to non-blocking
7272
fd = self.proc.stdout.fileno()
7373
fl = fcntl.fcntl(fd, fcntl.F_GETFL)
7474
fcntl.fcntl(fd, fcntl.F_SETFL, fl | os.O_NONBLOCK)
7575

76-
result = [False]
77-
78-
self._connectThread = threading.Thread(target = self.connectThreadedFunc,
79-
args = [result])
80-
self._connectThread.daemon = True
81-
self._connectThread.start()
82-
self._connectThread.join(timeout)
76+
result = False
77+
try:
78+
result = self.waitForPPPSuccess(timeout)
79+
except Exception as e:
80+
self.logger.error(e)
8381

84-
# If thread is still alive, tear down pppd connection and kill the thread.
85-
if self._connectThread.is_alive():
86-
self._connectThread.join(1)
82+
if not result and self.proc and (self.proc.poll() is None):
83+
self.logger.debug('Killing pppd')
8784
self.proc.send_signal(signal.SIGTERM)
8885
time.sleep(1)
8986

90-
return result[0]
87+
return result
88+
89+
90+
def readFromPPP(self):
91+
try:
92+
self.output += self.proc.stdout.read()
93+
except IOError as e:
94+
if e.errno != errno.EAGAIN:
95+
raise
96+
time.sleep(1)
9197

92-
# EFFECTS: Establish a cellular connection. Returns true if successful,
93-
# false otherwise.
94-
def connectThreadedFunc(self, result):
9598

96-
while True:
97-
try:
98-
self.output += self.proc.stdout.read()
99-
except IOError as e:
100-
if e.errno != 11:
101-
raise
102-
time.sleep(1)
99+
def waitForPPPSuccess(self, timeout):
100+
starttime = time.time()
101+
while (time.time() - starttime) < timeout:
102+
self.readFromPPP()
103103

104-
if self.laddr != None and self.raddr != None:
105-
result[0] = True
106-
return
104+
if self.laddr is not None and self.raddr is not None:
105+
return True
107106

108107
if 'Modem hangup' in self.output:
109108
raise PPPError('Modem hangup - possibly due to an unregistered SIM')
110109
elif self.proc.poll():
111110
raise PPPConnectionError(self.proc.returncode, self.output)
111+
return False
112112

113113
# EFFECTS: Disconnects from the network.
114-
# Returns true if successful, false otherwise.
115114
def disconnect(self):
115+
if self.proc and self.proc.poll() is None:
116+
self.proc.send_signal(signal.SIGTERM)
117+
time.sleep(1)
116118

117-
try:
118-
if not self.connected():
119-
return False
120-
except PPPConnectionError:
121-
return False
122-
123-
self.proc.send_signal(signal.SIGTERM)
124-
time.sleep(1)
125-
126-
return True
127119

128120
# EFFECTS: Returns true if a cellular connection is established.
129121
def connected(self):
130122
if self.proc and self.proc.poll():
131-
try:
132-
self.output += self.proc.stdout.read()
133-
except IOError as e:
134-
if e.errno != 11:
135-
raise
123+
self.readFromPPP()
136124
if self.proc.returncode not in [0, 5]:
137125
raise PPPConnectionError(self.proc.returncode, self.output)
138126
return False
@@ -145,11 +133,7 @@ def connected(self):
145133
@property
146134
def laddr(self):
147135
if self.proc and not self._laddr:
148-
try:
149-
self.output += self.proc.stdout.read()
150-
except IOError as e:
151-
if e.errno != 11:
152-
raise
136+
self.readFromPPP()
153137
result = re.search(r'local IP address ([\d\.]+)', self.output)
154138
if result:
155139
self._laddr = result.group(1)
@@ -160,33 +144,10 @@ def laddr(self):
160144
@property
161145
def raddr(self):
162146
if self.proc and not self._raddr:
163-
try:
164-
self.output += self.proc.stdout.read()
165-
except IOError as e:
166-
if e.errno != 11:
167-
raise
147+
self.readFromPPP()
168148
result = re.search(r'remote IP address ([\d\.]+)', self.output)
169149
if result:
170150
self._raddr = result.group(1)
171151

172152
return self._raddr
173153

174-
@property
175-
def output(self):
176-
self._outputRWLock.acquire()
177-
178-
while not self._outputReadyToRead:
179-
self._outputRWLock.wait()
180-
181-
self._outputReadyToRead = True
182-
self._outputRWLock.release()
183-
return self._output
184-
185-
@output.setter
186-
def output(self, output):
187-
self._outputReadyToRead = False
188-
self._outputRWLock.acquire()
189-
self._output = output
190-
self._outputReadyToRead = True
191-
self._outputRWLock.notifyAll()
192-
self._outputRWLock.release()

Hologram/Network/Modem/NovaM.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -85,3 +85,6 @@ def connect_socket(self, host, port):
8585
raise NetworkError('Failed to connect socket')
8686
else:
8787
self.logger.info('Connect socket is successful')
88+
89+
def reset(self):
90+
self.set('+CFUN', '15') # restart the modem

Hologram/Network/Network.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ def __init__(self, event=Event()):
3333

3434
def connect(self):
3535
self.event.broadcast('network.connected')
36+
return True
3637

3738
def disconnect(self):
3839
self.event.broadcast('network.disconnected')

requirements.txt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,4 +10,4 @@ python-sdk-auth==0.2.1
1010
pyudev==0.21.0
1111
pyusb==1.0.0
1212
psutil==5.3.1
13-
requests==2.18.3
13+
requests==2.20.1

0 commit comments

Comments
 (0)