Skip to content

Commit

Permalink
Merge pull request #118 from hologram-io/feature/multi-modem-support
Browse files Browse the repository at this point in the history
Feature/multi modem support
  • Loading branch information
parker-hologram authored Sep 5, 2023
2 parents 71c4bde + 1ded340 commit 641acdb
Show file tree
Hide file tree
Showing 13 changed files with 110 additions and 57 deletions.
4 changes: 2 additions & 2 deletions .github/workflows/publish.yml
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,10 @@ jobs:

steps:
- uses: actions/checkout@v2
- name: Set up Python 3.7
- name: Set up Python 3.9
uses: actions/setup-python@v2
with:
python-version: '3.7'
python-version: '3.9'

- name: Install dependencies
run: |
Expand Down
8 changes: 8 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,13 @@
# What's New in Hologram Python SDK

## v0.10.0
2023-09-05 Hologram <support@hologram.io>
* targets python version 3.9
* 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:
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`
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.
* 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*

## v0.9.1
2021-04-30 Hologram <support@hologram.io>
includes the following bug fixes
Expand Down
10 changes: 6 additions & 4 deletions Hologram/Cloud.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@
import logging
from logging import NullHandler
from Hologram.Event import Event
from typing import Union
from Hologram.Network.Modem.Modem import Modem
from Hologram.Network import NetworkManager
from Hologram.Authentication import *

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

def __init__(self, credentials, send_host = '', send_port = 0,
receive_host = '', receive_port = 0, network = ''):
receive_host = '', receive_port = 0, network = '', modem: Union[None, Modem] = None):

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

self.initializeNetwork(network)
self.initializeNetwork(network, modem)

def __initialize_host_and_port(self, send_host, send_port, receive_host, receive_port):
self.send_host = send_host
self.send_port = send_port
self.receive_host = receive_host
self.receive_port = receive_port

def initializeNetwork(self, network):
def initializeNetwork(self, network, modem):

self.event = Event()
self.__message_buffer = []

# Network Configuration
self._networkManager = NetworkManager.NetworkManager(self.event, network)
self._networkManager = NetworkManager.NetworkManager(self.event, network, modem=modem)

# This registers the message buffering feature based on network availability.
self.event.subscribe('network.connected', self.__clear_payload_buffer)
Expand Down
7 changes: 5 additions & 2 deletions Hologram/CustomCloud.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@
import sys
import threading
import time
from typing import Union
from Hologram.Network.Modem.Modem import Modem
from Hologram.Cloud import Cloud
from Exceptions.HologramError import HologramError

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

def __init__(self, credentials, send_host='', send_port=0,
receive_host='', receive_port=0, enable_inbound=False,
network=''):
network='', modem: Union[None, Modem] = None):

super().__init__(credentials,
send_host=send_host,
send_port=send_port,
receive_host=receive_host,
receive_port=receive_port,
network=network)
network=network,
modem=modem)

# Enforce that the send and receive configs are set before using the class.
if enable_inbound and (receive_host == '' or receive_port == 0):
Expand Down
8 changes: 6 additions & 2 deletions Hologram/HologramCloud.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@
import binascii
import json
import sys
from typing import Union
from Hologram.Network.Modem.Modem import Modem
from Hologram.CustomCloud import CustomCloud
from HologramAuth import TOTPAuthentication, SIMOTPAuthentication
from Hologram.Authentication import CSRPSKAuthentication
Expand Down Expand Up @@ -60,14 +62,16 @@ class HologramCloud(CustomCloud):
}

def __init__(self, credentials, enable_inbound=False, network='',
authentication_type='totp'):
authentication_type='totp', modem: Union[None, Modem] = None):
super().__init__(credentials,
send_host=HOLOGRAM_HOST_SEND,
send_port=HOLOGRAM_PORT_SEND,
receive_host=HOLOGRAM_HOST_RECEIVE,
receive_port=HOLOGRAM_PORT_RECEIVE,
enable_inbound=enable_inbound,
network=network)
network=network,
modem=modem
)

self.setAuthenticationType(credentials, authentication_type=authentication_type)

Expand Down
48 changes: 28 additions & 20 deletions Hologram/Network/Cellular.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
from Hologram.Network.Modem import Modem, E303, MS2131, E372, BG96, EC21, Nova_U201, NovaM, DriverLoader
from Hologram.Network import Network, NetworkScope
import time
from typing import Union
from serial.tools import list_ports

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

def autodetect_modem(self):
# scan for a modem and set it if found
dev_devices = self._scan_for_modems()
if dev_devices is None:
first_modem_handler = Cellular._scan_and_select_first_supported_modem()
if first_modem_handler is None:
raise NetworkError('Modem not detected')
self.modem = dev_devices[0]
self.modem = first_modem_handler(event=self.event)

def load_modem_drivers(self):
self._load_modem_drivers()
Expand Down Expand Up @@ -208,27 +209,37 @@ def _load_modem_drivers(self):
dl.force_driver_for_device(syspath, vid_pid[0], vid_pid[1])



def _scan_for_modems(self):
res = None
for (modemName, modemHandler) in self._modemHandlers.items():
if self._scan_for_modem(modemHandler):
res = (modemName, modemHandler)
break
return res
@staticmethod
def _scan_and_select_first_supported_modem() -> Union[Modem, None]:
for (_, modemHandler) in Cellular._modemHandlers.items():
modem_exists = Cellular._does_modem_exist_for_handler(modemHandler)
if modem_exists:
return modemHandler
return None


def _scan_for_modem(self, modemHandler):
@staticmethod
def _does_modem_exist_for_handler(modemHandler):
usb_ids = modemHandler.usb_ids
for vid_pid in usb_ids:
if not vid_pid:
continue
self.logger.debug('checking for vid_pid: %s', str(vid_pid))
for dev in list_ports.grep("{0}:{1}".format(vid_pid[0], vid_pid[1])):
self.logger.info('Detected modem %s', modemHandler.__name__)
for _ in list_ports.grep("{0}:{1}".format(vid_pid[0], vid_pid[1])):
return True
return False

@staticmethod
def scan_for_all_usable_modems() -> list[Modem]:
modems = []
for (_, modemHandler) in Cellular._modemHandlers.items():
modem_exists = Cellular._does_modem_exist_for_handler(modemHandler)
if modem_exists:
test_handler = modemHandler()
usable_ports = test_handler.detect_usable_serial_port(stop_on_first=False)
for port in usable_ports:
modem = modemHandler(device_name=port)
modems.append(modem)
return modems



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

@modem.setter
def modem(self, modem):
if modem not in self._modemHandlers:
raise NetworkError('Invalid modem type: %s' % modem)
else:
self._modem = self._modemHandlers[modem](event=self.event)
def modem(self, modem: Union[None, Modem] = None):
self._modem = modem

@property
def localIPAddress(self):
Expand Down
61 changes: 44 additions & 17 deletions Hologram/Network/Modem/Modem.py
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@ class Modem(IModem):
0x2F: u'\\',
}

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

Expand Down Expand Up @@ -199,20 +200,23 @@ def __detect_all_serial_ports(self, stop_on_first=False, include_all_ports=True)
# since our usable serial devices usually start at 0.
udevices = [x for x in list_ports.grep("{0}:{1}".format(vid, pid))]
for udevice in reversed(udevices):
if include_all_ports == False:
self.logger.debug('checking port %s', udevice.name)
port_opened = self.openSerialPort(udevice.device)
if not port_opened:
continue

res = self.command('', timeout=1)
if res[0] != ModemResult.OK:
continue
self.logger.info('found working port at %s', udevice.name)

device_names.append(udevice.device)
if stop_on_first:
break
try:
if include_all_ports == False:
self.logger.debug('checking port %s', udevice.name)
port_opened = self.openSerialPort(udevice.device)
if not port_opened:
continue

res = self.command('', timeout=1)
if res[0] != ModemResult.OK:
continue
self.logger.info('found working port at %s', udevice.name)

device_names.append(udevice.device)
if stop_on_first:
break
except Exception as e:
self.logger.warning(f"Error attempting to connect to serial port: {e}")
if stop_on_first and device_names:
break
return device_names
Expand Down Expand Up @@ -271,6 +275,23 @@ def send_message(self, data, timeout=DEFAULT_SEND_TIMEOUT):

return self.read_socket()

def send_sms_message(self, phonenumber, message, timeout=DEFAULT_SEND_TIMEOUT):
self.command("+CMGF", "1")

ctrl_z = chr(26).encode('utf-8')
ok, r = self.command(
"+CMGS",
f"\"{phonenumber}\"",
prompt=b">",
data=f"{message}\r",
commit_cmd=ctrl_z,
timeout=timeout
)

self.command("+CMGF", "0")
return ok == ModemResult.OK


def pop_received_message(self):
self.checkURC()
data = None
Expand Down Expand Up @@ -497,7 +518,7 @@ def _command_result(self):

def __command_helper(self, cmd='', value=None, expected=None, timeout=None,
retries=DEFAULT_SERIAL_RETRIES, seteq=False, read=False,
prompt=None, data=None, hide=False):
prompt=None, data=None, hide=False, commit_cmd=None):
self.result = ModemResult.Timeout

if cmd.endswith('?'):
Expand Down Expand Up @@ -528,6 +549,8 @@ def __command_helper(self, cmd='', value=None, expected=None, timeout=None,
if prompt in p:
time.sleep(1)
self._write_to_serial_port_and_flush(data)
if commit_cmd:
self.debugwrite(commit_cmd, hide=True)

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

def command(self, cmd='', value=None, expected=None, timeout=None,
retries=DEFAULT_SERIAL_RETRIES, seteq=False, read=False,
prompt=None, data=None, hide=False):
prompt=None, data=None, hide=False, commit_cmd=None):
try:
return self.__command_helper(cmd, value, expected, timeout,
retries, seteq, read, prompt, data, hide)
retries, seteq, read, prompt, data, hide, commit_cmd)
except serial.serialutil.SerialTimeoutException as e:
self.logger.debug('unable to write to port')
self.result = ModemResult.Error
Expand Down Expand Up @@ -844,6 +867,10 @@ def disable_hex_mode(self):

def __set_hex_mode(self, enable_hex_mode):
self.command('+UDCONF', '1,%d' % enable_hex_mode)

@property
def details(self):
return f"{self.description} at port: {self.device_name}"

@property
def serial_port(self):
Expand Down
9 changes: 5 additions & 4 deletions Hologram/Network/NetworkManager.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@
#

from Hologram.Network import Wifi, Ethernet, BLE, Cellular
from typing import Union
from Hologram.Network.Modem.Modem import Modem
from Exceptions.HologramError import NetworkError
import logging
from logging import NullHandler
Expand All @@ -26,15 +28,15 @@ class NetworkManager:
'ethernet' : Ethernet.Ethernet,
}

def __init__(self, event, network):
def __init__(self, event, network_name, modem: Union[None, Modem] = None):

# Logging setup.
self.logger = logging.getLogger(__name__)
self.logger.addHandler(NullHandler())

self.event = event
self.networkActive = False
self.network = network
self.init_network(network_name, modem)

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

@network.setter
def network(self, network, modem=None):
def init_network(self, network, modem: Union[None, Modem] = None):
if not network: # non-network mode
self.networkConnected()
self._network = None
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ in the spirit of bringing connectivity to your devices.

### Requirements:

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

We wrote scripts to ease the installation process.

Expand Down
4 changes: 2 additions & 2 deletions install.sh
Original file line number Diff line number Diff line change
Expand Up @@ -61,8 +61,8 @@ function install_software() {
}

function check_python_version() {
if ! python3 -V | grep '3.[7-9].[0-9]' > /dev/null 2>&1; then
echo "An unsupported version of python 3 is installed. Must have python 3.7+ installed to use the Hologram SDK"
if ! python3 -V | grep '3.[9-11].[0-9]' > /dev/null 2>&1; then
echo "An unsupported version of python 3 is installed. Must have python 3.9+ installed to use the Hologram SDK"
exit 1
fi
}
Expand Down
2 changes: 1 addition & 1 deletion requirements.txt
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
pyroute2==0.5.*
pyserial~=3.5
python-pppd==1.0.4
python-sdk-auth~=0.3.0
python-sdk-auth==0.4.0
pyudev~=0.22.0
pyusb~=1.2.1
psutil~=5.8.0
Expand Down
2 changes: 1 addition & 1 deletion setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@
'Topic :: Internet',
'Topic :: Security :: Cryptography',
'Programming Language :: Python',
'Programming Language :: Python :: 3.7',
'Programming Language :: Python :: 3.9',
],
**kw
)
Loading

0 comments on commit 641acdb

Please sign in to comment.