Skip to content

Commit 641acdb

Browse files
Merge pull request #118 from hologram-io/feature/multi-modem-support
Feature/multi modem support
2 parents 71c4bde + 1ded340 commit 641acdb

File tree

13 files changed

+110
-57
lines changed

13 files changed

+110
-57
lines changed

.github/workflows/publish.yml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,10 +10,10 @@ jobs:
1010

1111
steps:
1212
- uses: actions/checkout@v2
13-
- name: Set up Python 3.7
13+
- name: Set up Python 3.9
1414
uses: actions/setup-python@v2
1515
with:
16-
python-version: '3.7'
16+
python-version: '3.9'
1717

1818
- name: Install dependencies
1919
run: |

CHANGELOG.md

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

3+
## v0.10.0
4+
2023-09-05 Hologram <[email protected]>
5+
* targets python version 3.9
6+
* Allow setting a specific modem when initializing a network. This can be done by passing the modem into the `HologramCloud` intializing method for example: `HologramCloud({}, authentication_type='totp', network='cellular', modem=modem)`. Initialize a modem using one of the following methods:
7+
1. Initialize a modem object with a known good port using a supported modem class in `Hologram.Network.Modem` for example: `EC21(device_name="/dev/ttyUSB4")` This initializes a Quectel EC21 on port `/dev/ttyUSB4`
8+
2. Scan for all available modems through the new `Cellular.scan_for_all_usable_modems()` method at `Hologram.Network.Cellular`. This returns a list of accessible intialized modem objects. Just pass one of these in as a modem.
9+
* Allow modems to send SMS messages through the modem interface. For example: `hologram.network.modem.send_sms_message("+80112", "Hi dashboard!")`. *Note: Extra charges for sending SMS with this method may apply*
10+
311
## v0.9.1
412
2021-04-30 Hologram <[email protected]>
513
includes the following bug fixes

Hologram/Cloud.py

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,8 @@
1010
import logging
1111
from logging import NullHandler
1212
from Hologram.Event import Event
13+
from typing import Union
14+
from Hologram.Network.Modem.Modem import Modem
1315
from Hologram.Network import NetworkManager
1416
from Hologram.Authentication import *
1517

@@ -21,7 +23,7 @@ def __repr__(self):
2123
return type(self).__name__
2224

2325
def __init__(self, credentials, send_host = '', send_port = 0,
24-
receive_host = '', receive_port = 0, network = ''):
26+
receive_host = '', receive_port = 0, network = '', modem: Union[None, Modem] = None):
2527

2628
# Logging setup.
2729
self.logger = logging.getLogger(__name__)
@@ -33,21 +35,21 @@ def __init__(self, credentials, send_host = '', send_port = 0,
3335
self.__initialize_host_and_port(send_host, send_port,
3436
receive_host, receive_port)
3537

36-
self.initializeNetwork(network)
38+
self.initializeNetwork(network, modem)
3739

3840
def __initialize_host_and_port(self, send_host, send_port, receive_host, receive_port):
3941
self.send_host = send_host
4042
self.send_port = send_port
4143
self.receive_host = receive_host
4244
self.receive_port = receive_port
4345

44-
def initializeNetwork(self, network):
46+
def initializeNetwork(self, network, modem):
4547

4648
self.event = Event()
4749
self.__message_buffer = []
4850

4951
# Network Configuration
50-
self._networkManager = NetworkManager.NetworkManager(self.event, network)
52+
self._networkManager = NetworkManager.NetworkManager(self.event, network, modem=modem)
5153

5254
# This registers the message buffering feature based on network availability.
5355
self.event.subscribe('network.connected', self.__clear_payload_buffer)

Hologram/CustomCloud.py

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,8 @@
1212
import sys
1313
import threading
1414
import time
15+
from typing import Union
16+
from Hologram.Network.Modem.Modem import Modem
1517
from Hologram.Cloud import Cloud
1618
from Exceptions.HologramError import HologramError
1719

@@ -25,14 +27,15 @@ class CustomCloud(Cloud):
2527

2628
def __init__(self, credentials, send_host='', send_port=0,
2729
receive_host='', receive_port=0, enable_inbound=False,
28-
network=''):
30+
network='', modem: Union[None, Modem] = None):
2931

3032
super().__init__(credentials,
3133
send_host=send_host,
3234
send_port=send_port,
3335
receive_host=receive_host,
3436
receive_port=receive_port,
35-
network=network)
37+
network=network,
38+
modem=modem)
3639

3740
# Enforce that the send and receive configs are set before using the class.
3841
if enable_inbound and (receive_host == '' or receive_port == 0):

Hologram/HologramCloud.py

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,8 @@
1111
import binascii
1212
import json
1313
import sys
14+
from typing import Union
15+
from Hologram.Network.Modem.Modem import Modem
1416
from Hologram.CustomCloud import CustomCloud
1517
from HologramAuth import TOTPAuthentication, SIMOTPAuthentication
1618
from Hologram.Authentication import CSRPSKAuthentication
@@ -60,14 +62,16 @@ class HologramCloud(CustomCloud):
6062
}
6163

6264
def __init__(self, credentials, enable_inbound=False, network='',
63-
authentication_type='totp'):
65+
authentication_type='totp', modem: Union[None, Modem] = None):
6466
super().__init__(credentials,
6567
send_host=HOLOGRAM_HOST_SEND,
6668
send_port=HOLOGRAM_PORT_SEND,
6769
receive_host=HOLOGRAM_HOST_RECEIVE,
6870
receive_port=HOLOGRAM_PORT_RECEIVE,
6971
enable_inbound=enable_inbound,
70-
network=network)
72+
network=network,
73+
modem=modem
74+
)
7175

7276
self.setAuthenticationType(credentials, authentication_type=authentication_type)
7377

Hologram/Network/Cellular.py

Lines changed: 28 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
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
17+
from typing import Union
1718
from serial.tools import list_ports
1819

1920
# Cellular return codes.
@@ -48,10 +49,10 @@ def __init__(self, event=Event()):
4849

4950
def autodetect_modem(self):
5051
# scan for a modem and set it if found
51-
dev_devices = self._scan_for_modems()
52-
if dev_devices is None:
52+
first_modem_handler = Cellular._scan_and_select_first_supported_modem()
53+
if first_modem_handler is None:
5354
raise NetworkError('Modem not detected')
54-
self.modem = dev_devices[0]
55+
self.modem = first_modem_handler(event=self.event)
5556

5657
def load_modem_drivers(self):
5758
self._load_modem_drivers()
@@ -208,27 +209,37 @@ def _load_modem_drivers(self):
208209
dl.force_driver_for_device(syspath, vid_pid[0], vid_pid[1])
209210

210211

211-
212-
def _scan_for_modems(self):
213-
res = None
214-
for (modemName, modemHandler) in self._modemHandlers.items():
215-
if self._scan_for_modem(modemHandler):
216-
res = (modemName, modemHandler)
217-
break
218-
return res
212+
@staticmethod
213+
def _scan_and_select_first_supported_modem() -> Union[Modem, None]:
214+
for (_, modemHandler) in Cellular._modemHandlers.items():
215+
modem_exists = Cellular._does_modem_exist_for_handler(modemHandler)
216+
if modem_exists:
217+
return modemHandler
218+
return None
219219

220220

221-
def _scan_for_modem(self, modemHandler):
221+
@staticmethod
222+
def _does_modem_exist_for_handler(modemHandler):
222223
usb_ids = modemHandler.usb_ids
223224
for vid_pid in usb_ids:
224225
if not vid_pid:
225226
continue
226-
self.logger.debug('checking for vid_pid: %s', str(vid_pid))
227-
for dev in list_ports.grep("{0}:{1}".format(vid_pid[0], vid_pid[1])):
228-
self.logger.info('Detected modem %s', modemHandler.__name__)
227+
for _ in list_ports.grep("{0}:{1}".format(vid_pid[0], vid_pid[1])):
229228
return True
230229
return False
231230

231+
@staticmethod
232+
def scan_for_all_usable_modems() -> list[Modem]:
233+
modems = []
234+
for (_, modemHandler) in Cellular._modemHandlers.items():
235+
modem_exists = Cellular._does_modem_exist_for_handler(modemHandler)
236+
if modem_exists:
237+
test_handler = modemHandler()
238+
usable_ports = test_handler.detect_usable_serial_port(stop_on_first=False)
239+
for port in usable_ports:
240+
modem = modemHandler(device_name=port)
241+
modems.append(modem)
242+
return modems
232243

233244

234245

@@ -237,11 +248,8 @@ def modem(self):
237248
return self._modem
238249

239250
@modem.setter
240-
def modem(self, modem):
241-
if modem not in self._modemHandlers:
242-
raise NetworkError('Invalid modem type: %s' % modem)
243-
else:
244-
self._modem = self._modemHandlers[modem](event=self.event)
251+
def modem(self, modem: Union[None, Modem] = None):
252+
self._modem = modem
245253

246254
@property
247255
def localIPAddress(self):

Hologram/Network/Modem/Modem.py

Lines changed: 44 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,7 @@ class Modem(IModem):
5757
0x2F: u'\\',
5858
}
5959

60+
# The device_name is the same as the serial port, only provide a device_name if you dont want it to be autodectected
6061
def __init__(self, device_name=None, baud_rate='9600',
6162
chatscript_file=None, event=Event()):
6263

@@ -199,20 +200,23 @@ def __detect_all_serial_ports(self, stop_on_first=False, include_all_ports=True)
199200
# since our usable serial devices usually start at 0.
200201
udevices = [x for x in list_ports.grep("{0}:{1}".format(vid, pid))]
201202
for udevice in reversed(udevices):
202-
if include_all_ports == False:
203-
self.logger.debug('checking port %s', udevice.name)
204-
port_opened = self.openSerialPort(udevice.device)
205-
if not port_opened:
206-
continue
207-
208-
res = self.command('', timeout=1)
209-
if res[0] != ModemResult.OK:
210-
continue
211-
self.logger.info('found working port at %s', udevice.name)
212-
213-
device_names.append(udevice.device)
214-
if stop_on_first:
215-
break
203+
try:
204+
if include_all_ports == False:
205+
self.logger.debug('checking port %s', udevice.name)
206+
port_opened = self.openSerialPort(udevice.device)
207+
if not port_opened:
208+
continue
209+
210+
res = self.command('', timeout=1)
211+
if res[0] != ModemResult.OK:
212+
continue
213+
self.logger.info('found working port at %s', udevice.name)
214+
215+
device_names.append(udevice.device)
216+
if stop_on_first:
217+
break
218+
except Exception as e:
219+
self.logger.warning(f"Error attempting to connect to serial port: {e}")
216220
if stop_on_first and device_names:
217221
break
218222
return device_names
@@ -271,6 +275,23 @@ def send_message(self, data, timeout=DEFAULT_SEND_TIMEOUT):
271275

272276
return self.read_socket()
273277

278+
def send_sms_message(self, phonenumber, message, timeout=DEFAULT_SEND_TIMEOUT):
279+
self.command("+CMGF", "1")
280+
281+
ctrl_z = chr(26).encode('utf-8')
282+
ok, r = self.command(
283+
"+CMGS",
284+
f"\"{phonenumber}\"",
285+
prompt=b">",
286+
data=f"{message}\r",
287+
commit_cmd=ctrl_z,
288+
timeout=timeout
289+
)
290+
291+
self.command("+CMGF", "0")
292+
return ok == ModemResult.OK
293+
294+
274295
def pop_received_message(self):
275296
self.checkURC()
276297
data = None
@@ -497,7 +518,7 @@ def _command_result(self):
497518

498519
def __command_helper(self, cmd='', value=None, expected=None, timeout=None,
499520
retries=DEFAULT_SERIAL_RETRIES, seteq=False, read=False,
500-
prompt=None, data=None, hide=False):
521+
prompt=None, data=None, hide=False, commit_cmd=None):
501522
self.result = ModemResult.Timeout
502523

503524
if cmd.endswith('?'):
@@ -528,6 +549,8 @@ def __command_helper(self, cmd='', value=None, expected=None, timeout=None,
528549
if prompt in p:
529550
time.sleep(1)
530551
self._write_to_serial_port_and_flush(data)
552+
if commit_cmd:
553+
self.debugwrite(commit_cmd, hide=True)
531554

532555
self.result = self.process_response(cmd, timeout, hide=hide)
533556
if self.result == ModemResult.OK:
@@ -785,10 +808,10 @@ def _basic_set(self, cmd, value, strip_val=True):
785808

786809
def command(self, cmd='', value=None, expected=None, timeout=None,
787810
retries=DEFAULT_SERIAL_RETRIES, seteq=False, read=False,
788-
prompt=None, data=None, hide=False):
811+
prompt=None, data=None, hide=False, commit_cmd=None):
789812
try:
790813
return self.__command_helper(cmd, value, expected, timeout,
791-
retries, seteq, read, prompt, data, hide)
814+
retries, seteq, read, prompt, data, hide, commit_cmd)
792815
except serial.serialutil.SerialTimeoutException as e:
793816
self.logger.debug('unable to write to port')
794817
self.result = ModemResult.Error
@@ -844,6 +867,10 @@ def disable_hex_mode(self):
844867

845868
def __set_hex_mode(self, enable_hex_mode):
846869
self.command('+UDCONF', '1,%d' % enable_hex_mode)
870+
871+
@property
872+
def details(self):
873+
return f"{self.description} at port: {self.device_name}"
847874

848875
@property
849876
def serial_port(self):

Hologram/Network/NetworkManager.py

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,8 @@
1010
#
1111

1212
from Hologram.Network import Wifi, Ethernet, BLE, Cellular
13+
from typing import Union
14+
from Hologram.Network.Modem.Modem import Modem
1315
from Exceptions.HologramError import NetworkError
1416
import logging
1517
from logging import NullHandler
@@ -26,15 +28,15 @@ class NetworkManager:
2628
'ethernet' : Ethernet.Ethernet,
2729
}
2830

29-
def __init__(self, event, network):
31+
def __init__(self, event, network_name, modem: Union[None, Modem] = None):
3032

3133
# Logging setup.
3234
self.logger = logging.getLogger(__name__)
3335
self.logger.addHandler(NullHandler())
3436

3537
self.event = event
3638
self.networkActive = False
37-
self.network = network
39+
self.init_network(network_name, modem)
3840

3941
# EFFECTS: Event handler function that sets the network disconnect flag.
4042
def networkDisconnected(self):
@@ -50,8 +52,7 @@ def listAvailableInterfaces(self):
5052
def network(self):
5153
return self._network
5254

53-
@network.setter
54-
def network(self, network, modem=None):
55+
def init_network(self, network, modem: Union[None, Modem] = None):
5556
if not network: # non-network mode
5657
self.networkConnected()
5758
self._network = None

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ in the spirit of bringing connectivity to your devices.
1717

1818
### Requirements:
1919

20-
You will need `ppp` and Python 3.7 installed on your system for the SDK to work.
20+
You will need `ppp` and Python 3.9 installed on your system for the SDK to work.
2121

2222
We wrote scripts to ease the installation process.
2323

install.sh

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -61,8 +61,8 @@ function install_software() {
6161
}
6262

6363
function check_python_version() {
64-
if ! python3 -V | grep '3.[7-9].[0-9]' > /dev/null 2>&1; then
65-
echo "An unsupported version of python 3 is installed. Must have python 3.7+ installed to use the Hologram SDK"
64+
if ! python3 -V | grep '3.[9-11].[0-9]' > /dev/null 2>&1; then
65+
echo "An unsupported version of python 3 is installed. Must have python 3.9+ installed to use the Hologram SDK"
6666
exit 1
6767
fi
6868
}

0 commit comments

Comments
 (0)