From d889dfc3755ed719defccf6308586202abdb4fb7 Mon Sep 17 00:00:00 2001 From: Dominic Amato Date: Tue, 3 Mar 2020 17:09:39 -0600 Subject: [PATCH 01/76] Add Github Actions --- .github/workflows/pythonpackage.yml | 41 +++++++++++++++++++++++++++++ .github/workflows/pythonpublish.yml | 26 ++++++++++++++++++ README.md | 3 +-- 3 files changed, 68 insertions(+), 2 deletions(-) create mode 100644 .github/workflows/pythonpackage.yml create mode 100644 .github/workflows/pythonpublish.yml diff --git a/.github/workflows/pythonpackage.yml b/.github/workflows/pythonpackage.yml new file mode 100644 index 00000000..c444a252 --- /dev/null +++ b/.github/workflows/pythonpackage.yml @@ -0,0 +1,41 @@ +name: Python package + +on: [push] + +jobs: + build: + + runs-on: ubuntu-latest + strategy: + matrix: + python-version: [3.7, 3.8] + + steps: + - uses: actions/checkout@v2 + - name: Set up Python ${{ matrix.python-version }} + uses: actions/setup-python@v1 + with: + python-version: ${{ matrix.python-version }} + + - name: Install dependencies + run: | + python -m pip install --upgrade pip + pip install -r requirements.txt + + - name: Lint with flake8 + run: | + pip install flake8 + # stop the build if there are Python syntax errors or undefined names + flake8 . --count --select=E9,F63,F7,F82 --show-source --statistics + # exit-zero treats all errors as warnings. The GitHub editor is 127 chars wide + flake8 . --count --exit-zero --max-complexity=10 --max-line-length=127 --statistics + + - name: Test with coverage and coveralls + run: | + pip install coveralls pytest + coverage run --source Hologram -m pytest + + - name: Coveralls + uses: coverallsapp/github-action@master + with: + github-token: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/pythonpublish.yml b/.github/workflows/pythonpublish.yml new file mode 100644 index 00000000..b143a530 --- /dev/null +++ b/.github/workflows/pythonpublish.yml @@ -0,0 +1,26 @@ +name: Upload Python Package + +on: + release: + types: [created] + +jobs: + deploy: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + - name: Set up Python + uses: actions/setup-python@v1 + with: + python-version: '3.x' + - name: Install dependencies + run: | + python -m pip install --upgrade pip + pip install setuptools wheel twine + - name: Build and publish + env: + TWINE_USERNAME: ${{ secrets.PYPI_USERNAME }} + TWINE_PASSWORD: ${{ secrets.PYPI_PASSWORD }} + run: | + python setup.py sdist bdist_wheel + twine upload dist/* diff --git a/README.md b/README.md index 8d58969f..c97b8275 100644 --- a/README.md +++ b/README.md @@ -1,8 +1,7 @@ # hologram-python [![PyPI version](https://badge.fury.io/py/hologram-python.svg)](https://badge.fury.io/py/hologram-python) - -[![Build Status](https://travis-ci.org/hologram-io/hologram-python.svg?branch=master)](https://travis-ci.org/hologram-io/hologram-python) +[![Python package](https://github.com/hologram-io/hologram-python/workflows/Python%20package/badge.svg)](https://github.com/hologram-io/hologram-python/actions) [![Coverage Status](https://coveralls.io/repos/github/hologram-io/hologram-python/badge.svg?branch=master)](https://coveralls.io/github/hologram-io/hologram-python?branch=master) ## Introduction From 889b1b9910a29fc8d3c3e6ae778223b99275a9d7 Mon Sep 17 00:00:00 2001 From: Dominic Amato Date: Wed, 18 Mar 2020 13:56:42 -0500 Subject: [PATCH 02/76] Update issue templates use comments to hide extra details when preparing a report --- .github/ISSUE_TEMPLATE/bug_report.md | 39 +++++++++++++++++++++++ .github/ISSUE_TEMPLATE/feature_request.md | 20 ++++++++++++ 2 files changed, 59 insertions(+) create mode 100644 .github/ISSUE_TEMPLATE/bug_report.md create mode 100644 .github/ISSUE_TEMPLATE/feature_request.md diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md new file mode 100644 index 00000000..bec632af --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -0,0 +1,39 @@ +--- +name: Bug report +about: Help us stomp out bugs! +title: '' +labels: bug +assignees: '' + +--- + + +### Describe the problem + + +### Expected behavior + + + +### Actual behavior + + + +### Steps to reproduce the behavior + + +### System information +- **OS Platform and Distribution (e.g., Linux Ubuntu 16.04)**: +- **Python SDK installed via PyPI or GitHub**: +- **SDK version (use command below)**: +- **Python version**: +- **Hardware (modem) model**: + + diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md new file mode 100644 index 00000000..6315abc6 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature_request.md @@ -0,0 +1,20 @@ +--- +name: Feature request +about: Suggest an idea for this project +title: '' +labels: enhancement +assignees: '' + +--- + +**Is your feature request related to a problem? Please describe.** + + +**Describe the solution you'd like** + + +**Describe alternatives you've considered** + + +**Additional context** + From ccc06fb6a70adb177a08c833ed7951aab119e57a Mon Sep 17 00:00:00 2001 From: Dominic Amato Date: Fri, 3 Jan 2020 11:42:50 -0600 Subject: [PATCH 03/76] wait for the process to be terminated (#17) --- Hologram/Network/Modem/ModemMode/PPP.py | 5 ++++- scripts/hologram_modem.py | 4 +++- scripts/hologram_network.py | 4 +++- 3 files changed, 10 insertions(+), 3 deletions(-) diff --git a/Hologram/Network/Modem/ModemMode/PPP.py b/Hologram/Network/Modem/ModemMode/PPP.py index 82b8cbde..f614a92d 100755 --- a/Hologram/Network/Modem/ModemMode/PPP.py +++ b/Hologram/Network/Modem/ModemMode/PPP.py @@ -78,7 +78,10 @@ def __shut_down_existing_ppp_session(self): for pid in pid_list: self.logger.info('Killing pid %s that currently have an active PPP session', pid) - psutil.Process(pid).terminate() + process = psutil.Process(pid) + process.terminate() + # Wait at least 10 seconds for the process to terminate + process.wait(10) def __check_for_existing_ppp_sessions(self): diff --git a/scripts/hologram_modem.py b/scripts/hologram_modem.py index a69c5aa9..c918a3e6 100644 --- a/scripts/hologram_modem.py +++ b/scripts/hologram_modem.py @@ -73,7 +73,9 @@ def run_modem_disconnect(args): if 'pppd' in pinfo['name']: print('Found existing PPP session on pid: %s' % pinfo['pid']) print('Killing pid %s now' % pinfo['pid']) - psutil.Process(pinfo['pid']).terminate() + process = psutil.Process(pinfo['pid']) + process.terminate() + process.wait() def run_modem_signal(args): cloud = CustomCloud(None, network='cellular') diff --git a/scripts/hologram_network.py b/scripts/hologram_network.py index 9dd07cc9..6eec9e3e 100644 --- a/scripts/hologram_network.py +++ b/scripts/hologram_network.py @@ -40,7 +40,9 @@ def run_network_disconnect(args): if 'pppd' in pinfo['name']: print('Found existing PPP session on pid: %s' % pinfo['pid']) print('Killing pid %s now' % pinfo['pid']) - psutil.Process(pinfo['pid']).terminate() + process = psutil.Process(pinfo['pid']) + process.terminate() + process.wait() _run_handlers = { 'network_connect': run_network_connect, From 50e7d2b796f00caaa180071cf7270f20803cf1d8 Mon Sep 17 00:00:00 2001 From: Dominic Amato Date: Fri, 3 Jan 2020 11:43:29 -0600 Subject: [PATCH 04/76] Properly handle when parsing other types of SMS PDUs (#16) --- Hologram/Network/Modem/Modem.py | 6 +++--- tests/Modem/test_Modem.py | 19 +++++++++++++++++++ 2 files changed, 22 insertions(+), 3 deletions(-) diff --git a/Hologram/Network/Modem/Modem.py b/Hologram/Network/Modem/Modem.py index dadb8eaf..5b19d56d 100644 --- a/Hologram/Network/Modem/Modem.py +++ b/Hologram/Network/Modem/Modem.py @@ -550,9 +550,10 @@ def _parsePDU(self, header, pdu): # EFFECTS: Parses the rest of the sms pdu (sender). def _parse_sender(self, pdu, offset): - sms_deliver = int(pdu[offset],16) - if sms_deliver & 0x03 != 0: return None + # options are SMS-SUBMIT, SMS-DELIVER or SMS-STATUS-REPORT + # we are looking for a deliver, return none for other types + if sms_deliver & 0x03 != 0: return None, offset offset += 1 sender_len = int(pdu[offset:offset+2],16) offset += 2 @@ -889,4 +890,3 @@ def version(self): @property def imei(self): return self._basic_command('+GSN') - diff --git a/tests/Modem/test_Modem.py b/tests/Modem/test_Modem.py index 4d18b588..dc6cf2df 100644 --- a/tests/Modem/test_Modem.py +++ b/tests/Modem/test_Modem.py @@ -8,6 +8,7 @@ import pytest import sys +from datetime import datetime sys.path.append(".") sys.path.append("..") @@ -33,6 +34,12 @@ def mock_close_serial_port(modem): def mock_detect_usable_serial_port(modem, stop_on_first=True): return '/dev/ttyUSB0' + +def mock_command_sms(modem, at_command): + return (ModemResult.OK, ['+CMGL: 2,1,,26', '0791447779071413040C9144977304250500007160421062944008D4F29C0E8AC966']) + +def mock_set_sms(modem, at_command, val): + return None @pytest.fixture def no_serial_port(monkeypatch): @@ -43,6 +50,10 @@ def no_serial_port(monkeypatch): monkeypatch.setattr(Modem, 'closeSerialPort', mock_close_serial_port) monkeypatch.setattr(Modem, 'detect_usable_serial_port', mock_detect_usable_serial_port) +@pytest.fixture +def get_sms(monkeypatch): + monkeypatch.setattr(Modem, 'command', mock_command_sms) + monkeypatch.setattr(Modem, 'set', mock_set_sms) # CONSTRUCTOR @@ -79,6 +90,14 @@ def test_get_location(no_serial_port): assert(modem.location == 'test location') assert('This modem does not support this property' in str(e)) +# SMS + +def test_get_sms(no_serial_port, get_sms): + modem = Modem() + res = modem.popReceivedSMS() + assert(res.sender == '447937405250') + assert(res.timestamp == datetime.utcfromtimestamp(1498264009)) + assert(res.message == 'Test 123') # DEBUGWRITE From 22074be8023c48da7fc2f9e94072ccd1ff454e10 Mon Sep 17 00:00:00 2001 From: Dominic Amato Date: Fri, 3 Jan 2020 12:05:13 -0600 Subject: [PATCH 05/76] Add chunking for messeages over 512 bytes (#15) --- Hologram/Network/Modem/Modem.py | 25 ++++++++++++++++--------- tests/Modem/test_Modem.py | 29 +++++++++++++++++++++-------- 2 files changed, 37 insertions(+), 17 deletions(-) diff --git a/Hologram/Network/Modem/Modem.py b/Hologram/Network/Modem/Modem.py index 5b19d56d..3f5d710b 100644 --- a/Hologram/Network/Modem/Modem.py +++ b/Hologram/Network/Modem/Modem.py @@ -289,7 +289,6 @@ def connect_socket(self, host, port): self.logger.info('Connect socket is successful') def listen_socket(self, port): - at_command_val = "%d,%s" % (self.socket_identifier, port) self.listen_socket_identifier = self.socket_identifier ok, _ = self.set('+USOLI', at_command_val, timeout=5) @@ -298,17 +297,25 @@ def listen_socket(self, port): raise NetworkError('Failed to listen socket') def write_socket(self, data): - self.enable_hex_mode() - value = b'%d,%d,\"%s\"' % (self.socket_identifier, - len(data), - binascii.hexlify(data)) - ok, _ = self.set('+USOWR', value, timeout=10) - if ok != ModemResult.OK: - self.logger.error('Failed to write to socket') - raise NetworkError('Failed to write socket') + hexdata = binascii.hexlify(data) + # We have to do it in chunks of 510 since 512 is actually too long (CMEE error) + # and we need 2n chars for hexified data + for chunk in self._chunks(hexdata, 510): + value = b'%d,%d,\"%s\"' % (self.socket_identifier, + len(binascii.unhexlify(chunk)), + chunk) + ok, _ = self.set('+USOWR', value, timeout=10) + if ok != ModemResult.OK: + self.logger.error('Failed to write to socket') + raise NetworkError('Failed to write socket') self.disable_hex_mode() + def _chunks(self, data, n): + """Yield successive n-sized chunks from lst.""" + for i in range(0, len(data), n): + yield data[i:i + n] + def read_socket(self, socket_identifier=None, payload_length=None): if socket_identifier is None: diff --git a/tests/Modem/test_Modem.py b/tests/Modem/test_Modem.py index dc6cf2df..9cd540af 100644 --- a/tests/Modem/test_Modem.py +++ b/tests/Modem/test_Modem.py @@ -32,6 +32,9 @@ def mock_open_serial_port(modem, device_name=None): def mock_close_serial_port(modem): return True +def mock_result(modem): + return (ModemResult.OK, None) + def mock_detect_usable_serial_port(modem, stop_on_first=True): return '/dev/ttyUSB0' @@ -54,9 +57,14 @@ def no_serial_port(monkeypatch): def get_sms(monkeypatch): monkeypatch.setattr(Modem, 'command', mock_command_sms) monkeypatch.setattr(Modem, 'set', mock_set_sms) +def override_command_result(monkeypatch): + monkeypatch.setattr(Modem, '_command_result', mock_result) -# CONSTRUCTOR +@pytest.fixture +def override_command_result(monkeypatch): + monkeypatch.setattr(Modem, '_command_result', mock_result) +# CONSTRUCTOR def test_init_modem_no_args(no_serial_port): modem = Modem() @@ -80,10 +88,8 @@ def test_get_result_string(no_serial_port): assert(modem.getResultString(-3) == 'Modem response doesn\'t match expected return value') assert(modem.getResultString(-99) == 'Unknown response code') - # PROPERTIES - def test_get_location(no_serial_port): modem = Modem() with pytest.raises(NotImplementedError) as e: @@ -99,8 +105,19 @@ def test_get_sms(no_serial_port, get_sms): assert(res.timestamp == datetime.utcfromtimestamp(1498264009)) assert(res.message == 'Test 123') -# DEBUGWRITE +# WRITE SOCKET + +def test_socket_write_under_512(no_serial_port, override_command_result): + modem = Modem() + data = '{message:{fill}{align}{width}}'.format(message='Test-', fill='@', align='<', width=64) + modem.write_socket(data.encode()) + +def test_socket_write_over_512(no_serial_port, override_command_result): + modem = Modem() + data = '{message:{fill}{align}{width}}'.format(message='Test-', fill='@', align='<', width=600) + modem.write_socket(data.encode()) +# DEBUGWRITE def test_debugwrite(no_serial_port): modem = Modem() @@ -111,10 +128,8 @@ def test_debugwrite(no_serial_port): modem.debugwrite('test222', hide=True) assert(modem.debug_out == 'test') # debug_out shouldn't change since hide is enabled. - # MODEMWRITE - def test_modemwrite(no_serial_port): modem = Modem() assert(modem.debug_out == '') @@ -136,7 +151,6 @@ def test_modemwrite(no_serial_port): modem.modemwrite('test5', start=True, at=True, seteq=True, read=True, end=True) assert(modem.debug_out == '[ATtest5=?]') - # COMMAND_RESULT def test_command_result(no_serial_port): @@ -193,7 +207,6 @@ def test_command_result(no_serial_port): # HANDLEURC - # These are static methods that can be tested independently. # We decided to wrap it all here under this test object class TestModemProtectedStaticMethods(): From 6b36f54e05142c3460c151150d6e2d153c4fbc9d Mon Sep 17 00:00:00 2001 From: Dominic Amato Date: Fri, 3 Jan 2020 12:08:30 -0600 Subject: [PATCH 06/76] Update test_Modem.py --- tests/Modem/test_Modem.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/tests/Modem/test_Modem.py b/tests/Modem/test_Modem.py index 9cd540af..413dcdb5 100644 --- a/tests/Modem/test_Modem.py +++ b/tests/Modem/test_Modem.py @@ -57,8 +57,6 @@ def no_serial_port(monkeypatch): def get_sms(monkeypatch): monkeypatch.setattr(Modem, 'command', mock_command_sms) monkeypatch.setattr(Modem, 'set', mock_set_sms) -def override_command_result(monkeypatch): - monkeypatch.setattr(Modem, '_command_result', mock_result) @pytest.fixture def override_command_result(monkeypatch): From eeca8f90091e2c0a8b4c0ef011ddefc8d87d4b3e Mon Sep 17 00:00:00 2001 From: Dominic Amato Date: Tue, 7 Jan 2020 12:28:06 -0600 Subject: [PATCH 07/76] check python and pip versions (#19) --- install.sh | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/install.sh b/install.sh index b7b1f246..331f243d 100755 --- a/install.sh +++ b/install.sh @@ -60,6 +60,14 @@ function install_software() { fi } +function check_python_version() { + python3 -V | grep '3.[7-9].[0-9]' &> /dev/null + if ! [ $? == 0 ]; then + echo "An unsupported version of python 3 is installed. Must have python 3.7+ installed to use the Hologram SDK" + exit 1 + fi +} + # EFFECTS: Returns true if the specified program is installed, false otherwise. function check_if_installed() { if command -v "$*" >/dev/null 2>&1; then @@ -99,6 +107,8 @@ function verify_installation() { echo 'You are now ready to use the Hologram Python SDK!' } +check_python_version + update_repository # Iterate over all programs to see if they are installed @@ -115,6 +125,11 @@ do pause "Installing $program. Press [Enter] key to continue..."; install_software 'python3-pip' fi + pip3 -V | grep '3.[7-9]' &> /dev/null + if ! [ $? == 0 ]; then + echo "pip3 is installed for an unsupported version of python." + exit 1 + fi elif check_if_installed "$program"; then echo "$program is already installed." else From 1ee018930ace35495e0b7db3169f45594db13572 Mon Sep 17 00:00:00 2001 From: Dominic Amato Date: Tue, 7 Jan 2020 12:53:50 -0600 Subject: [PATCH 08/76] simplify version check --- install.sh | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/install.sh b/install.sh index 331f243d..191387ca 100755 --- a/install.sh +++ b/install.sh @@ -61,8 +61,7 @@ function install_software() { } function check_python_version() { - python3 -V | grep '3.[7-9].[0-9]' &> /dev/null - if ! [ $? == 0 ]; then + 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" exit 1 fi @@ -125,8 +124,7 @@ do pause "Installing $program. Press [Enter] key to continue..."; install_software 'python3-pip' fi - pip3 -V | grep '3.[7-9]' &> /dev/null - if ! [ $? == 0 ]; then + if ! pip3 -V | grep '3.[7-9]' >/dev/null 2>&1; then echo "pip3 is installed for an unsupported version of python." exit 1 fi From cdddf6c1c11261b7ec4c3ef49ebdea96d6bb3a57 Mon Sep 17 00:00:00 2001 From: Dominic Amato Date: Tue, 7 Jan 2020 13:11:05 -0600 Subject: [PATCH 09/76] check python and pip version for update too --- update.sh | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/update.sh b/update.sh index ab3093bc..7b602f2b 100755 --- a/update.sh +++ b/update.sh @@ -57,6 +57,13 @@ function install_software() { fi } +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" + exit 1 + fi +} + # EFFECTS: Returns true if the specified program is installed, false otherwise. function check_if_installed() { if command -v "$*" >/dev/null 2>&1; then @@ -96,10 +103,12 @@ function verify_installation() { echo 'You are now ready to use the Hologram Python SDK!' } +check_python_version + update_repository # Check if an older version exists and uninstall it -if command hologram version | grep '0.8'; then +if command hologram version | grep '0.[0-8]'; then echo "Found a previous version of the SDK on Python 2. The new update uses" echo "Python 3 and is not compatible with Python 2. This script will uninstall" echo "the previous SDK version, install Python 3 and then install the new" @@ -123,6 +132,10 @@ do pause "Installing $program. Press [Enter] key to continue..."; install_software 'python3-pip' fi + if ! pip3 -V | grep '3.[7-9]' >/dev/null 2>&1; then + echo "pip3 is installed for an unsupported version of python." + exit 1 + fi elif check_if_installed "$program"; then echo "$program is already installed." else From 4b6e2a2ca302264c1446d8d572d879f51d999f8a Mon Sep 17 00:00:00 2001 From: akumlehn Date: Tue, 3 Mar 2020 22:22:29 +0100 Subject: [PATCH 10/76] call super.disconnect instead of connect (ensures event network.disconnected broadcast) (#22) --- Hologram/Network/Cellular.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Hologram/Network/Cellular.py b/Hologram/Network/Cellular.py index 84d36c88..1007a2de 100644 --- a/Hologram/Network/Cellular.py +++ b/Hologram/Network/Cellular.py @@ -90,7 +90,7 @@ def disconnect(self): self.logger.info('Successfully disconnected from cell network') self._connection_status = CLOUD_DISCONNECTED self.event.broadcast('cellular.disconnected') - super().connect() + super().disconnect() else: self.logger.info('Failed to disconnect from cell network') From cde545af8ea0e269824ea938052b8fc119656b47 Mon Sep 17 00:00:00 2001 From: Dominic Amato Date: Tue, 24 Mar 2020 17:40:51 -0500 Subject: [PATCH 11/76] remove extraneous parenthesis (#24) --- Hologram/Api/Api.py | 2 +- Hologram/Authentication/AES/AESCipher.py | 2 +- Hologram/Authentication/Authentication.py | 2 +- Hologram/Cloud.py | 2 +- Hologram/Event/Event.py | 2 +- Hologram/Network/Modem/DriverLoader.py | 7 +------ Hologram/Network/Modem/IModem.py | 2 +- Hologram/Network/Modem/ModemMode/ModemMode.py | 2 +- Hologram/Network/Modem/ModemMode/pppd.py | 2 +- Hologram/Network/Network.py | 2 +- Hologram/Network/NetworkManager.py | 2 +- Hologram/Network/Route.py | 2 +- UtilClasses/UtilClasses.py | 6 +++--- tests/Authentication/test_CSRPSKAuthentication.py | 2 +- tests/Authentication/test_HologramAuthentication.py | 2 +- tests/Event/test_Event.py | 2 +- tests/MessageMode/test_Cloud.py | 2 +- tests/MessageMode/test_CustomCloud.py | 2 +- tests/MessageMode/test_HologramCloud.py | 2 +- tests/Modem/test_Modem.py | 2 +- tests/ModemMode/test_ModemMode.py | 2 +- tests/ModemMode/test_PPP.py | 2 +- tests/Network/test_Cellular.py | 2 +- tests/Network/test_Ethernet.py | 2 +- tests/Network/test_Network.py | 2 +- tests/Network/test_NetworkManager.py | 2 +- 26 files changed, 28 insertions(+), 33 deletions(-) diff --git a/Hologram/Api/Api.py b/Hologram/Api/Api.py index dc49edb3..acb28e09 100644 --- a/Hologram/Api/Api.py +++ b/Hologram/Api/Api.py @@ -14,7 +14,7 @@ HOLOGRAM_REST_API_BASEURL = 'https://dashboard.hologram.io/api/1' -class Api(): +class Api: def __init__(self, apikey='', username='', password=''): # Logging setup. diff --git a/Hologram/Authentication/AES/AESCipher.py b/Hologram/Authentication/AES/AESCipher.py index 3ddccd4c..70321124 100644 --- a/Hologram/Authentication/AES/AESCipher.py +++ b/Hologram/Authentication/AES/AESCipher.py @@ -12,7 +12,7 @@ from cryptography.hazmat.primitives import padding from cryptography.hazmat.backends import default_backend -class AESCipher(): +class AESCipher: # EFFECTS: Constructor that sets the IV to def __init__(self, iv, key): diff --git a/Hologram/Authentication/Authentication.py b/Hologram/Authentication/Authentication.py index 02618158..ed324b5b 100644 --- a/Hologram/Authentication/Authentication.py +++ b/Hologram/Authentication/Authentication.py @@ -10,7 +10,7 @@ import logging from logging import NullHandler -class Authentication(): +class Authentication: def __init__(self, credentials): self.credentials = credentials diff --git a/Hologram/Cloud.py b/Hologram/Cloud.py index 9907444e..680dfcb3 100644 --- a/Hologram/Cloud.py +++ b/Hologram/Cloud.py @@ -15,7 +15,7 @@ __version__ = '0.9.0' -class Cloud(): +class Cloud: def __repr__(self): return type(self).__name__ diff --git a/Hologram/Event/Event.py b/Hologram/Event/Event.py index 1b871091..340ee3b7 100644 --- a/Hologram/Event/Event.py +++ b/Hologram/Event/Event.py @@ -8,7 +8,7 @@ # LICENSE: Distributed under the terms of the MIT License import logging -class Event(): +class Event: _funcLookupTable = {} def __init__(self): self.__dict__ = self._funcLookupTable diff --git a/Hologram/Network/Modem/DriverLoader.py b/Hologram/Network/Modem/DriverLoader.py index 68aaf67d..cd29212c 100644 --- a/Hologram/Network/Modem/DriverLoader.py +++ b/Hologram/Network/Modem/DriverLoader.py @@ -13,7 +13,7 @@ import subprocess -class DriverLoader(): +class DriverLoader: # I would much rather use python-kmod for all this # but it doesn't seem to build properly on the Pi and # hasn't been updated in years. It's possible we need to update @@ -37,8 +37,3 @@ def load_module(self, module): def force_driver_for_device(self, syspath, vid, pid): with open(syspath, "w") as f: f.write("%s %s"%(vid, pid)) - - - - - diff --git a/Hologram/Network/Modem/IModem.py b/Hologram/Network/Modem/IModem.py index 80b86d14..654dd751 100644 --- a/Hologram/Network/Modem/IModem.py +++ b/Hologram/Network/Modem/IModem.py @@ -17,7 +17,7 @@ MODEM_TIMEOUT = -1 MODEM_OK = 0 -class IModem(): +class IModem: usb_ids = [] # module needed by modem diff --git a/Hologram/Network/Modem/ModemMode/ModemMode.py b/Hologram/Network/Modem/ModemMode/ModemMode.py index 08489cde..7ce6f2b9 100644 --- a/Hologram/Network/Modem/ModemMode/ModemMode.py +++ b/Hologram/Network/Modem/ModemMode/ModemMode.py @@ -11,7 +11,7 @@ from logging import NullHandler from Hologram.Event import Event -class ModemMode(): +class ModemMode: def __repr__(self): return type(self).__name__ diff --git a/Hologram/Network/Modem/ModemMode/pppd.py b/Hologram/Network/Modem/ModemMode/pppd.py index 2cc65186..f33f7e8a 100644 --- a/Hologram/Network/Modem/ModemMode/pppd.py +++ b/Hologram/Network/Modem/ModemMode/pppd.py @@ -23,7 +23,7 @@ __version__ = '1.0.3' DEFAULT_CONNECT_TIMEOUT = 200 -class PPPConnection(): +class PPPConnection: def __repr__(self): return type(self).__name__ diff --git a/Hologram/Network/Network.py b/Hologram/Network/Network.py index b3b56346..9f99feea 100644 --- a/Hologram/Network/Network.py +++ b/Hologram/Network/Network.py @@ -19,7 +19,7 @@ class NetworkScope(Enum): HOLOGRAM = 2 -class Network(): +class Network: def __repr__(self): return type(self).__name__ diff --git a/Hologram/Network/NetworkManager.py b/Hologram/Network/NetworkManager.py index 28c845e6..e0866471 100644 --- a/Hologram/Network/NetworkManager.py +++ b/Hologram/Network/NetworkManager.py @@ -17,7 +17,7 @@ DEFAULT_NETWORK_TIMEOUT = 200 -class NetworkManager(): +class NetworkManager: _networkHandlers = { 'wifi' : Wifi.Wifi, diff --git a/Hologram/Network/Route.py b/Hologram/Network/Route.py index 892aaa13..b7338ba2 100644 --- a/Hologram/Network/Route.py +++ b/Hologram/Network/Route.py @@ -18,7 +18,7 @@ DEFAULT_DESTINATION = '0.0.0.0/0' -class Route(): +class Route: def __init__(self): self.ipr = IPRoute() self.logger = logging.getLogger(__name__) diff --git a/UtilClasses/UtilClasses.py b/UtilClasses/UtilClasses.py index 99c44fac..3c05c90e 100644 --- a/UtilClasses/UtilClasses.py +++ b/UtilClasses/UtilClasses.py @@ -11,7 +11,7 @@ import threading -class Location(): +class Location: def __init__(self, date=None, time=None, latitude=None, longitude=None, altitude=None, uncertainty=None): @@ -25,7 +25,7 @@ def __init__(self, date=None, time=None, latitude=None, longitude=None, def __repr__(self): return type(self).__name__ -class SMS(): +class SMS: def __init__(self, sender, timestamp, message): self.sender = sender @@ -48,7 +48,7 @@ class ModemResult: Timeout = 'Timeout' OK = 'OK' -class RWLock(): +class RWLock: def __init__(self): self.mutex = threading.Condition() diff --git a/tests/Authentication/test_CSRPSKAuthentication.py b/tests/Authentication/test_CSRPSKAuthentication.py index 34b0a8da..9f71a878 100644 --- a/tests/Authentication/test_CSRPSKAuthentication.py +++ b/tests/Authentication/test_CSRPSKAuthentication.py @@ -15,7 +15,7 @@ sys.path.append("../..") from Hologram.Authentication.CSRPSKAuthentication import CSRPSKAuthentication -class TestCSRPSKAuthentication(): +class TestCSRPSKAuthentication: def test_create(self): credentials = {'devicekey': '12345678'} diff --git a/tests/Authentication/test_HologramAuthentication.py b/tests/Authentication/test_HologramAuthentication.py index aeb0c5e6..a69bf4d0 100644 --- a/tests/Authentication/test_HologramAuthentication.py +++ b/tests/Authentication/test_HologramAuthentication.py @@ -17,7 +17,7 @@ credentials = {'devicekey': '12345678'} -class TestHologramAuthentication(): +class TestHologramAuthentication: def test_create(self): auth = HologramAuthentication(credentials) diff --git a/tests/Event/test_Event.py b/tests/Event/test_Event.py index 175b264f..8229117e 100644 --- a/tests/Event/test_Event.py +++ b/tests/Event/test_Event.py @@ -14,7 +14,7 @@ sys.path.append("../..") from Hologram.Event import Event -class TestEvent(): +class TestEvent: def test_create(self): event = Event() diff --git a/tests/MessageMode/test_Cloud.py b/tests/MessageMode/test_Cloud.py index 4b5b4ef0..c7dbda11 100644 --- a/tests/MessageMode/test_Cloud.py +++ b/tests/MessageMode/test_Cloud.py @@ -14,7 +14,7 @@ from Hologram.Authentication import * from Hologram.Cloud import Cloud -class TestCloud(): +class TestCloud: def test_create_send(self): cloud = Cloud(None, send_host = '127.0.0.1', send_port = 9999) diff --git a/tests/MessageMode/test_CustomCloud.py b/tests/MessageMode/test_CustomCloud.py index 14ffd2c9..d7cd9e45 100644 --- a/tests/MessageMode/test_CustomCloud.py +++ b/tests/MessageMode/test_CustomCloud.py @@ -14,7 +14,7 @@ from Hologram.Authentication import * from Hologram.CustomCloud import CustomCloud -class TestCustomCloud(): +class TestCustomCloud: def test_create_send(self): customCloud = CustomCloud(None, send_host='127.0.0.1', diff --git a/tests/MessageMode/test_HologramCloud.py b/tests/MessageMode/test_HologramCloud.py index 86e627f3..e2d328d8 100644 --- a/tests/MessageMode/test_HologramCloud.py +++ b/tests/MessageMode/test_HologramCloud.py @@ -16,7 +16,7 @@ credentials = {'devicekey':'12345678'} -class TestHologramCloud(): +class TestHologramCloud: def test_create(self): hologram = HologramCloud(credentials, enable_inbound = False) diff --git a/tests/Modem/test_Modem.py b/tests/Modem/test_Modem.py index 413dcdb5..cbf71f40 100644 --- a/tests/Modem/test_Modem.py +++ b/tests/Modem/test_Modem.py @@ -207,7 +207,7 @@ def test_command_result(no_serial_port): # These are static methods that can be tested independently. # We decided to wrap it all here under this test object -class TestModemProtectedStaticMethods(): +class TestModemProtectedStaticMethods: def test_check_registered_string(self): result = '+CREG: 2,5,"5585","404C790",6' diff --git a/tests/ModemMode/test_ModemMode.py b/tests/ModemMode/test_ModemMode.py index 5eebfd29..c942e022 100644 --- a/tests/ModemMode/test_ModemMode.py +++ b/tests/ModemMode/test_ModemMode.py @@ -12,7 +12,7 @@ sys.path.append("../..") from Hologram.Network.Modem.ModemMode.ModemMode import ModemMode -class TestModemMode(): +class TestModemMode: def test_modem_mode_create(self): modem_mode = ModemMode(device_name='/dev/ttyUSB0', baud_rate='9600') diff --git a/tests/ModemMode/test_PPP.py b/tests/ModemMode/test_PPP.py index a3932f46..1f68c8a8 100644 --- a/tests/ModemMode/test_PPP.py +++ b/tests/ModemMode/test_PPP.py @@ -13,7 +13,7 @@ sys.path.append("../..") from Hologram.Network.Modem.ModemMode.MockPPP import MockPPP -class TestPPP(): +class TestPPP: def test_ppp_create(self): ppp = MockPPP(chatscript_file='test') diff --git a/tests/Network/test_Cellular.py b/tests/Network/test_Cellular.py index a74d6d1f..52f6e953 100644 --- a/tests/Network/test_Cellular.py +++ b/tests/Network/test_Cellular.py @@ -14,7 +14,7 @@ sys.path.append("../..") from Hologram.Network import Cellular -class TestCellular(): +class TestCellular: def test_invalid_cellular_type(self): pass diff --git a/tests/Network/test_Ethernet.py b/tests/Network/test_Ethernet.py index 5fa053d8..f68634e8 100644 --- a/tests/Network/test_Ethernet.py +++ b/tests/Network/test_Ethernet.py @@ -14,7 +14,7 @@ sys.path.append("../..") from Hologram.Network import Ethernet -class TestEthernet(): +class TestEthernet: def test_Ethernet(self): ethernet = Ethernet.Ethernet() diff --git a/tests/Network/test_Network.py b/tests/Network/test_Network.py index 48980aba..ce36fdfc 100644 --- a/tests/Network/test_Network.py +++ b/tests/Network/test_Network.py @@ -14,7 +14,7 @@ sys.path.append("../..") from Hologram.Network import Network -class TestNetwork(): +class TestNetwork: def test_create_network(self): network = Network() diff --git a/tests/Network/test_NetworkManager.py b/tests/Network/test_NetworkManager.py index 07a3db22..d941a45b 100644 --- a/tests/Network/test_NetworkManager.py +++ b/tests/Network/test_NetworkManager.py @@ -14,7 +14,7 @@ sys.path.append("../..") from Hologram.Network import NetworkManager -class TestNetworkManager(): +class TestNetworkManager: def test_create_non_network(self): networkManager = NetworkManager.NetworkManager(None, '') From a63eabaec4673d1433644d64590938550fd272db Mon Sep 17 00:00:00 2001 From: Dom Amato Date: Tue, 24 Mar 2020 18:33:26 -0500 Subject: [PATCH 12/76] include token --- .github/workflows/pythonpackage.yml | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/.github/workflows/pythonpackage.yml b/.github/workflows/pythonpackage.yml index c444a252..d078039d 100644 --- a/.github/workflows/pythonpackage.yml +++ b/.github/workflows/pythonpackage.yml @@ -31,11 +31,9 @@ jobs: flake8 . --count --exit-zero --max-complexity=10 --max-line-length=127 --statistics - name: Test with coverage and coveralls + env: + COVERALLS_REPO_TOKEN: ${{ secrets.COVERALLS_REPO_TOKEN }} run: | pip install coveralls pytest coverage run --source Hologram -m pytest - - - name: Coveralls - uses: coverallsapp/github-action@master - with: - github-token: ${{ secrets.GITHUB_TOKEN }} + coveralls From b41028b0f55d357986f502b08dc1371a6168a0f7 Mon Sep 17 00:00:00 2001 From: Dominic Amato Date: Tue, 24 Mar 2020 19:06:46 -0500 Subject: [PATCH 13/76] remove extraneous parenthesis (#26) From e8fcaed58b6de3a9692102027f4ee3e8b75b05e3 Mon Sep 17 00:00:00 2001 From: Dom Amato Date: Tue, 24 Mar 2020 18:52:04 -0500 Subject: [PATCH 14/76] remove travis and rename actions --- .../{pythonpublish.yml => publish.yml} | 0 .../{pythonpackage.yml => testlint.yml} | 0 .travis.yml | 33 ------------------- 3 files changed, 33 deletions(-) rename .github/workflows/{pythonpublish.yml => publish.yml} (100%) rename .github/workflows/{pythonpackage.yml => testlint.yml} (100%) delete mode 100644 .travis.yml diff --git a/.github/workflows/pythonpublish.yml b/.github/workflows/publish.yml similarity index 100% rename from .github/workflows/pythonpublish.yml rename to .github/workflows/publish.yml diff --git a/.github/workflows/pythonpackage.yml b/.github/workflows/testlint.yml similarity index 100% rename from .github/workflows/pythonpackage.yml rename to .github/workflows/testlint.yml diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index 30f57b69..00000000 --- a/.travis.yml +++ /dev/null @@ -1,33 +0,0 @@ -sudo: required -language: python -os: linux -dist: xenial # required for Python >= 3.7 - -before_install: - - pip install --upgrade pip - -jobs: - include: - - stage: test - python: 3.7 - name: 'test' - script: - - pip install coveralls - - coverage run --source Hologram -m pytest - - coveralls - -# Linting just doesn't seem to work on travis -# - script: -# - pip install . --upgrade -# - pip install python-sdk-auth --upgrade -# - pip install pylint --upgrade -# - pip freeze -# - pylint -rn --errors-only Hologram -# python: 3.7 -# name: 'lint' - -notifications: - email: false - slack: - rooms: - secure: wzP86BJJEFjs3dtALZ5tfjM4FqGL7oucXRxhVvyaG8uDH6S01q5xn6d/8jnGWgLWZAdrbAiOVMKeyMiuPq7N3puW5fVaKniVPl9wkRCaoMG0aw+qkQaKP9V0bv7Iq3sAO0HufeB2AbwOekv+wCtjZfaFFzZQqIILOdvzQfeWL+uJC0Nx2hF3q1ahDm0Twit6U7h5SLm+H9aAx2/DFKPxzQNh/8XldkinbwLne86lT+DHOIb+ak9lPzqICkme1zO3Mf1NbQTNI8Ys3Yzw2XNr/MnySj2W4qqqtVoCIgDy6AFBpRFvyb8o1NUN7yKK3xHXp5RtAKBNXPSF7Zn4ywdnvqzvV3wzz5+xPHI36fa8tDR6/Nbe2wupevp8OhMbzKUXSVK4iLoWbx+zU6qM298GvMj0zOyQJE5OFMk0jfA2ZF3xt2Po1klLtLGKebSD3I6eGQh2Xvy3Ckw8kIvpgPKnNY6Dmdm8VZ2p1hUf7jv0CYnROyKxXjXo6JOSiolfH88F4iKPg/h1459wa9CSk3OWsQHmYoE1EtshRBwPv9PfaHF60hUdfmrR1N8I+oV0Ffm9a6Y26SZVv37s4k65cBRC4AfiLmnIITAS+wt2WPf3bXhMq+338wIL0f1djUMCIu4h3OWuwLfNtardOu1WR+eb15hrCd0Kf6rRYiD9mgeqtsE= From 4dbbb1496b8a5b6fdbe5cd66322f7943caf9c229 Mon Sep 17 00:00:00 2001 From: Dominic Amato Date: Tue, 24 Mar 2020 19:55:23 -0500 Subject: [PATCH 15/76] use pytest instead of coverage --- .github/workflows/testlint.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/testlint.yml b/.github/workflows/testlint.yml index d078039d..0f82b094 100644 --- a/.github/workflows/testlint.yml +++ b/.github/workflows/testlint.yml @@ -34,6 +34,6 @@ jobs: env: COVERALLS_REPO_TOKEN: ${{ secrets.COVERALLS_REPO_TOKEN }} run: | - pip install coveralls pytest - coverage run --source Hologram -m pytest + pip install coveralls pytest-cov + pytest --cov=Hologram tests/ coveralls From 39964637eba2c96291603c94a45472f9198dddf8 Mon Sep 17 00:00:00 2001 From: Dom Amato Date: Tue, 24 Mar 2020 20:15:55 -0500 Subject: [PATCH 16/76] only test on 3.7 for now --- .github/workflows/testlint.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/testlint.yml b/.github/workflows/testlint.yml index 0f82b094..41061064 100644 --- a/.github/workflows/testlint.yml +++ b/.github/workflows/testlint.yml @@ -8,7 +8,7 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - python-version: [3.7, 3.8] + python-version: [3.7] steps: - uses: actions/checkout@v2 From 535e359a7a36095008d1f4dd84a9c06c73848691 Mon Sep 17 00:00:00 2001 From: Dominic Amato Date: Fri, 3 Apr 2020 16:03:14 -0500 Subject: [PATCH 17/76] Allow sending raw AT commands to the modem (#28) --- scripts/hologram_modem.py | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/scripts/hologram_modem.py b/scripts/hologram_modem.py index c918a3e6..f64c54ab 100644 --- a/scripts/hologram_modem.py +++ b/scripts/hologram_modem.py @@ -17,6 +17,8 @@ import subprocess import time +DEFAULT_TIMEOUT = 5 + help_connect = '''This subcommand establishes a cellular connection.\n ''' @@ -40,6 +42,8 @@ help_location = '''Print the location of the modem based on cell towers if supported\n ''' +help_at_command = '''Send an AT command to the modem and print the result''' + help_reset = '''Restart the modem\n''' help_radio_off = '''Turn off the cellular radio on the modem\n''' @@ -87,6 +91,17 @@ def run_modem_signal(args): else: print('Signal strength: ' + str(cloud.network.signal_strength)) +def run_at_command(args): + cloud = CustomCloud(None, network='cellular') + cmd = '' + if args['command'] is not None: + cmd = args['command'].lstrip("AT") + val = None + if not cmd.endswith('?') and '=' in cmd: + cmd, val = cmd.split('=') + result, response = cloud.network.modem.command(cmd, val, timeout=args['timeout']) + print('Response: ' + ''.join(map(str, response)) + f'\n{result}') + def run_modem_version(args): cloud = CustomCloud(None, network='cellular') version = cloud.network.modem.version @@ -142,6 +157,7 @@ def run_modem_location(args): _run_handlers = { 'modem_connect': run_modem_connect, 'modem_disconnect': run_modem_disconnect, + 'modem_command': run_at_command, 'modem_sim': run_modem_sim, 'modem_operator': run_modem_operator, 'modem_signal': run_modem_signal, @@ -212,6 +228,14 @@ def parse_hologram_modem_args(parser): parser_radio_off.set_defaults(command_selected='modem_radio_off') parser_radio_off.add_argument('-v', nargs='?', action=VAction, dest='verbose', required=False) + # at-command + parser_command = subparsers.add_parser('command', help=help_at_command) + parser_command.set_defaults(command_selected='modem_command') + parser_command.add_argument('command', nargs='?', help='AT command to send to the modem') + parser_command.add_argument('-t', '--timeout', type=int, default=DEFAULT_TIMEOUT, nargs='?', + help='The period in seconds before the command exits if it doesn\'t receive a response') + parser_command.add_argument('-v', nargs='?', action=VAction, dest='verbose', required=False) + # version parser_version = subparsers.add_parser('version', help=help_version) parser_version.set_defaults(command_selected='modem_version') From ce138c84bc95226d095c513f45481cd1e4a6c206 Mon Sep 17 00:00:00 2001 From: Dominic Amato Date: Thu, 23 Apr 2020 10:54:30 -0500 Subject: [PATCH 18/76] Fix PPP Errors with disconnect and routing (#32) Fix #30 and #31 --- Hologram/Network/Cellular.py | 13 +++++++++++ Hologram/Network/Modem/Modem.py | 25 ++++++++++++++++----- Hologram/Network/Modem/ModemMode/PPP.py | 18 ++++++++------- Hologram/Network/Modem/ModemMode/pppd.py | 4 ++++ Hologram/Network/Route.py | 28 ++++++++++++++++++++---- 5 files changed, 71 insertions(+), 17 deletions(-) diff --git a/Hologram/Network/Cellular.py b/Hologram/Network/Cellular.py index 1007a2de..0aa8dcb6 100644 --- a/Hologram/Network/Cellular.py +++ b/Hologram/Network/Cellular.py @@ -73,6 +73,8 @@ def connect(self, timeout = DEFAULT_CELLULAR_TIMEOUT): self.logger.info('Successfully connected to cell network') # Disable at sockets mode since we're already establishing PPP. # This call is needed in certain modems that have limited interfaces to work with. + time.sleep(2) + # give the device a little time to enumerate self.disable_at_sockets_mode() self.__configure_routing() self._connection_status = CLOUD_CONNECTED @@ -85,6 +87,7 @@ def connect(self, timeout = DEFAULT_CELLULAR_TIMEOUT): def disconnect(self): self.logger.info('Disconnecting from cell network') + self.__remove_routing() success = self.modem.disconnect() if success: self.logger.info('Successfully disconnected from cell network') @@ -166,6 +169,7 @@ def __reconnect_after_forced_disconnect(self): self.logger.info('Ready to receive data on port %s', self.__receive_port) def __configure_routing(self): + # maybe we don't have to tear down the routes but we probably should self.logger.info('Adding routes to Hologram cloud') self._route.add('10.176.0.0/16', self.localIPAddress) self._route.add('10.254.0.0/16', self.localIPAddress) @@ -173,6 +177,15 @@ def __configure_routing(self): self.logger.info('Adding system-wide default route to cellular interface') self._route.add_default(self.localIPAddress) + def __remove_routing(self): + self.logger.info('Removing routes to Hologram cloud') + if self.localIPAddress: + self._route.delete('10.176.0.0/16', self.localIPAddress) + self._route.delete('10.254.0.0/16', self.localIPAddress) + if self.scope == NetworkScope.SYSTEM: + self.logger.info('Removing system-wide default route to cellular interface') + self._route.delete_default(self.localIPAddress) + def _load_modem_drivers(self): dl = DriverLoader.DriverLoader() for (modemName, modemHandler) in self._modemHandlers.items(): diff --git a/Hologram/Network/Modem/Modem.py b/Hologram/Network/Modem/Modem.py index 3f5d710b..63d34290 100644 --- a/Hologram/Network/Modem/Modem.py +++ b/Hologram/Network/Modem/Modem.py @@ -13,7 +13,8 @@ from UtilClasses import ModemResult from UtilClasses import SMS from Hologram.Event import Event -from Exceptions.HologramError import SerialError, HologramError, NetworkError +from Exceptions.HologramError import SerialError, HologramError, NetworkError, PPPError + from collections import deque import binascii @@ -100,8 +101,16 @@ def connect(self, timeout): def disconnect(self): if self._mode is not None: - return self._mode.disconnect() - return None + res = self._mode.disconnect() + self._mode = None + return res + else: + try: + PPP.shut_down_existing_ppp_session(self.logger) + return True + except PPPError as e: + self.logger.info('Got PPPError trying to disconnect open sessions') + return None def _initialize_device_name(self, device_name): if device_name is None: @@ -883,11 +892,17 @@ def modem_mode(self, mode): @property def localIPAddress(self): - return self._mode.localIPAddress + if self._mode: + return self._mode.localIPAddress + else: + return None @property def remoteIPAddress(self): - return self._mode.remoteIPAddress + if self._mode: + return self._mode.remoteIPAddress + else: + return None @property diff --git a/Hologram/Network/Modem/ModemMode/PPP.py b/Hologram/Network/Modem/ModemMode/PPP.py index f614a92d..28d629b9 100755 --- a/Hologram/Network/Modem/ModemMode/PPP.py +++ b/Hologram/Network/Modem/ModemMode/PPP.py @@ -58,35 +58,37 @@ def connect(self, timeout=DEFAULT_PPP_TIMEOUT): def disconnect(self): self._ppp.disconnect() - self.__shut_down_existing_ppp_session() + PPP.shut_down_existing_ppp_session(self.logger) return True # EFFECTS: Makes sure that there are no existing PPP instances on the same # device interface. def __enforce_no_existing_ppp_session(self): - pid_list = self.__check_for_existing_ppp_sessions() + pid_list = PPP.check_for_existing_ppp_sessions(self.logger) if len(pid_list) > 0: raise PPPError('Existing PPP session(s) are established by pid(s) %s. Please close/kill these processes first' % pid_list) - def __shut_down_existing_ppp_session(self): - pid_list = self.__check_for_existing_ppp_sessions() + @staticmethod + def shut_down_existing_ppp_session(logger): + pid_list = PPP.check_for_existing_ppp_sessions(logger) # Process this only if it is a valid PID integer. for pid in pid_list: - self.logger.info('Killing pid %s that currently have an active PPP session', + logger.info('Killing pid %s that currently have an active PPP session', pid) process = psutil.Process(pid) process.terminate() # Wait at least 10 seconds for the process to terminate process.wait(10) - def __check_for_existing_ppp_sessions(self): + @staticmethod + def check_for_existing_ppp_sessions(logger): existing_ppp_pids = [] - self.logger.info('Checking for existing PPP sessions') + logger.info('Checking for existing PPP sessions') for proc in psutil.process_iter(): try: @@ -95,7 +97,7 @@ def __check_for_existing_ppp_sessions(self): raise PPPError('Failed to check for existing PPP sessions') if 'pppd' in pinfo['name']: - self.logger.info('Found existing PPP session on pid: %s', pinfo['pid']) + logger.info('Found existing PPP session on pid: %s', pinfo['pid']) existing_ppp_pids.append(pinfo['pid']) return existing_ppp_pids diff --git a/Hologram/Network/Modem/ModemMode/pppd.py b/Hologram/Network/Modem/ModemMode/pppd.py index f33f7e8a..5c3323ee 100644 --- a/Hologram/Network/Modem/ModemMode/pppd.py +++ b/Hologram/Network/Modem/ModemMode/pppd.py @@ -118,6 +118,10 @@ def disconnect(self): if self.proc and self.proc.poll() is None: self.proc.send_signal(signal.SIGTERM) time.sleep(1) + # Reset the values when we disconnect + self._laddr = None + self._raddr = None + self.proc = None # EFFECTS: Returns true if a cellular connection is established. diff --git a/Hologram/Network/Route.py b/Hologram/Network/Route.py index b7338ba2..d41c7b47 100644 --- a/Hologram/Network/Route.py +++ b/Hologram/Network/Route.py @@ -20,7 +20,6 @@ class Route: def __init__(self): - self.ipr = IPRoute() self.logger = logging.getLogger(__name__) self.logger.addHandler(NullHandler()) @@ -56,13 +55,33 @@ def add_default(self, gateway): self.logger.debug('Could not set default route due to NetlinkError: %s', str(e)) def add(self, destination, gateway): - self.ipr.route('add', + self.logger.debug('Adding Route %s : %s', destination, gateway) + with IPRoute() as ipr: + ipr.route('add', dst=destination, gateway=gateway) + def delete_default(self, gateway): + try: + self.delete(DEFAULT_DESTINATION, gateway) + except NetlinkError as e: + self.logger.debug('Could not set default route due to NetlinkError: %s', str(e)) + + def delete(self, destination, gateway): + self.logger.debug('Removing Route %s : %s', destination, gateway) + try: + with IPRoute() as ipr: + ipr.route('del', + dst=destination, + gateway=gateway) + except NetlinkError as e: + self.logger.debug('Could not delete route due to NetlinkError: %s', str(e)) + + def __interface_index(self, interface): index = None - indexes = self.ipr.link_lookup(ifname=interface) + with IPRoute() as ipr: + indexes = ipr.link_lookup(ifname=interface) if len(indexes) == 1: index = indexes[0] return index @@ -71,7 +90,8 @@ def __get_interface_state(self, interface): if self.is_interface_available(interface): link_state = None ipr_index = self.__interface_index(interface) - links = self.ipr.get_links() + with IPRoute() as ipr: + links = ipr.get_links() for link in links: if link['index'] == ipr_index: From 477e71170ea3c34ebb3dc17f4bd6180105d367fc Mon Sep 17 00:00:00 2001 From: Dominic Amato Date: Mon, 4 May 2020 10:33:08 -0500 Subject: [PATCH 19/76] Concat to bytes not str (#33) --- Hologram/CustomCloud.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Hologram/CustomCloud.py b/Hologram/CustomCloud.py index 02b0c5e9..99dc3ae9 100644 --- a/Hologram/CustomCloud.py +++ b/Hologram/CustomCloud.py @@ -325,7 +325,7 @@ def __incoming_connection_thread(self, clientsocket): clientsocket.settimeout(RECEIVE_TIMEOUT) # Keep parsing the received data until timeout or receive no more data. - recv = '' + recv = b'' while True: try: result = clientsocket.recv(MAX_RECEIVE_BYTES) From bd529423396f5c4d4668eef6f13aec9a35db8f49 Mon Sep 17 00:00:00 2001 From: Dominic Amato Date: Sat, 9 May 2020 10:53:08 -0500 Subject: [PATCH 20/76] properly convert bytestring ascii value --- Hologram/HologramCloud.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/Hologram/HologramCloud.py b/Hologram/HologramCloud.py index f2bf44d9..a650e67a 100755 --- a/Hologram/HologramCloud.py +++ b/Hologram/HologramCloud.py @@ -186,7 +186,10 @@ def popReceivedSMS(self): def __parse_hologram_json_result(self, result): try: resultList = json.loads(result) - resultList[0] = int(resultList[0]) + if isinstance(resultList, bytes): + resultList[0] = int(chr(resultList[0])) + else: + resultList[0] = int(resultList[0]) except ValueError: self.logger.error('Server replied with invalid JSON [%s]', result) resultList = [ERR_UNKNOWN] From cfef0f5b681701fcb4730d972268ad3f5d62659b Mon Sep 17 00:00:00 2001 From: Dominic Amato Date: Wed, 20 May 2020 14:00:42 -0500 Subject: [PATCH 21/76] convert from ascii int value to int --- Hologram/HologramCloud.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/Hologram/HologramCloud.py b/Hologram/HologramCloud.py index a650e67a..6c730363 100755 --- a/Hologram/HologramCloud.py +++ b/Hologram/HologramCloud.py @@ -203,8 +203,12 @@ def __parse_hologram_compact_result(self, result): return [ERR_UNKNOWN] resultList = [] - for x in result: - resultList.append(int(x)) + if isinstance(result, bytes): + for x in result: + resultList.append(int(chr(x))) + else: + for x in result: + resultList.append(int(x)) if len(resultList) == 0: resultList = [ERR_UNKNOWN] From 05663142a11648f0115dead818e3361819e51885 Mon Sep 17 00:00:00 2001 From: Dominic Amato Date: Fri, 5 Jun 2020 20:28:25 -0500 Subject: [PATCH 22/76] make it possible to renable at sockets (#36) --- Hologram/Network/Cellular.py | 4 ++++ Hologram/Network/Modem/E303.py | 4 ---- Hologram/Network/Modem/MS2131.py | 4 ---- Hologram/Network/Modem/Modem.py | 5 ++++- Hologram/Network/Modem/Nova.py | 3 +++ 5 files changed, 11 insertions(+), 9 deletions(-) diff --git a/Hologram/Network/Cellular.py b/Hologram/Network/Cellular.py index 0aa8dcb6..b02ed451 100644 --- a/Hologram/Network/Cellular.py +++ b/Hologram/Network/Cellular.py @@ -91,6 +91,7 @@ def disconnect(self): success = self.modem.disconnect() if success: self.logger.info('Successfully disconnected from cell network') + self.enable_at_sockets_mode() self._connection_status = CLOUD_DISCONNECTED self.event.broadcast('cellular.disconnected') super().disconnect() @@ -140,6 +141,9 @@ def pop_received_message(self): def disable_at_sockets_mode(self): self.modem.disable_at_sockets_mode() + def enable_at_sockets_mode(self): + self.modem.enable_at_sockets_mode() + def enableSMS(self): return self.modem.enableSMS() diff --git a/Hologram/Network/Modem/E303.py b/Hologram/Network/Modem/E303.py index b7264948..2f066e9f 100644 --- a/Hologram/Network/Modem/E303.py +++ b/Hologram/Network/Modem/E303.py @@ -37,10 +37,6 @@ def init_serial_commands(self): self.command("+CREG", "2") self.command("+CGREG", "2") - # AT sockets mode is always disabled for E303. - def disable_at_sockets_mode(self): - pass - @property def iccid(self): return self._basic_command('^ICCID?').lstrip('^ICCID: ')[:-1] diff --git a/Hologram/Network/Modem/MS2131.py b/Hologram/Network/Modem/MS2131.py index a3b5e348..ecc80e28 100644 --- a/Hologram/Network/Modem/MS2131.py +++ b/Hologram/Network/Modem/MS2131.py @@ -38,10 +38,6 @@ def init_serial_commands(self): self.command("+CREG", "2") self.command("+CGREG", "2") - # AT sockets mode is always disabled for MS2131. - def disable_at_sockets_mode(self): - pass - @property def iccid(self): return self._basic_command('^ICCID?').lstrip('^ICCID: ')[:-1] diff --git a/Hologram/Network/Modem/Modem.py b/Hologram/Network/Modem/Modem.py index 63d34290..9416adaf 100644 --- a/Hologram/Network/Modem/Modem.py +++ b/Hologram/Network/Modem/Modem.py @@ -809,7 +809,10 @@ def _read_from_serial_port(self, timeout=None, size=DEFAULT_SERIAL_READ_SIZE): return r def disable_at_sockets_mode(self): - raise HologramError('Cannot disable AT command sockets on this Modem type') + pass + + def enable_at_sockets_mode(self): + pass def enable_hex_mode(self): self.__set_hex_mode(1) diff --git a/Hologram/Network/Modem/Nova.py b/Hologram/Network/Modem/Nova.py index eddd77e9..c3ddd031 100644 --- a/Hologram/Network/Modem/Nova.py +++ b/Hologram/Network/Modem/Nova.py @@ -24,6 +24,9 @@ def __init__(self, device_name=None, baud_rate='9600', def disable_at_sockets_mode(self): self._at_sockets_available = False + def enable_at_sockets_mode(self): + self._at_sockets_available = True + @property def version(self): return self._basic_command('I9') From 7c45b17020a32c250832a8fd7f1d136c4174f391 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A9ctor?= Date: Fri, 10 Jul 2020 15:09:33 -0500 Subject: [PATCH 23/76] include support for Huawei Modem E372 (#21) * include support for Huawei Modem E372 and actually works with Ubuntu and Raspbian10 * add support in class for E372 modem Co-authored-by: Hector Mendez --- Hologram/Network/Cellular.py | 3 +- Hologram/Network/Modem/E372.py | 45 ++++++++++++++++++++++++++++++ Hologram/Network/Modem/__init__.py | 2 +- 3 files changed, 48 insertions(+), 2 deletions(-) create mode 100644 Hologram/Network/Modem/E372.py diff --git a/Hologram/Network/Cellular.py b/Hologram/Network/Cellular.py index b02ed451..b6431d77 100644 --- a/Hologram/Network/Cellular.py +++ b/Hologram/Network/Cellular.py @@ -11,7 +11,7 @@ from Hologram.Event import Event from Exceptions.HologramError import NetworkError from Hologram.Network.Route import Route -from Hologram.Network.Modem import Modem, E303, MS2131, Nova_U201, NovaM, DriverLoader +from Hologram.Network.Modem import Modem, E303, MS2131, E372, Nova_U201, NovaM, DriverLoader from Hologram.Network import Network, NetworkScope import time from serial.tools import list_ports @@ -30,6 +30,7 @@ class Cellular(Network): _modemHandlers = { 'e303': E303.E303, 'ms2131': MS2131.MS2131, + 'e372': E372.E372, 'nova': Nova_U201.Nova_U201, 'novam': NovaM.NovaM, '': Modem diff --git a/Hologram/Network/Modem/E372.py b/Hologram/Network/Modem/E372.py new file mode 100644 index 00000000..cfed7a31 --- /dev/null +++ b/Hologram/Network/Modem/E372.py @@ -0,0 +1,45 @@ +# E372.py - Based on Hologram Python SDK Huawei MS2131 modem interface +# +# +# +# +# +# LICENSE: Distributed under the terms of the MIT License +# + +from Hologram.Network.Modem import Modem +from Hologram.Event import Event + +DEFAULT_E372_TIMEOUT = 200 + +class E372(Modem): + + usb_ids = [('12d1', '14c6')] + + def __init__(self, device_name=None, baud_rate='9600', + chatscript_file=None, event=Event()): + + super().__init__(device_name=device_name, baud_rate=baud_rate, + chatscript_file=chatscript_file, event=event) + + def connect(self, timeout = DEFAULT_E372_TIMEOUT): + return super().connect(timeout) + + def init_serial_commands(self): + self.command("E0") #echo off + self.command("+CMEE", "2") #set verbose error codes + self.command("+CPIN?") + self.command("+CTZU", "1") #time/zone sync + self.command("+CTZR", "1") #time/zone URC + #self.command("+CPIN", "") #set SIM PIN + self.command("+CPMS", "\"ME\",\"ME\",\"ME\"") + self.set_sms_configs() + self.command("+CREG", "2") + self.command("+CGREG", "2") + + def disable_at_sockets_mode(self): + pass + + @property + def iccid(self): + return self._basic_command('^ICCID?').lstrip('^ICCID: ')[:-1] diff --git a/Hologram/Network/Modem/__init__.py b/Hologram/Network/Modem/__init__.py index 1112da98..fe44d41c 100644 --- a/Hologram/Network/Modem/__init__.py +++ b/Hologram/Network/Modem/__init__.py @@ -1,4 +1,4 @@ -__all__ = ['Modem', 'MockModem', 'MS2131', 'Nova', 'E303'] +__all__ = ['Modem', 'MockModem', 'MS2131', 'Nova', 'E303', 'E372'] from .IModem import IModem from .Modem import Modem From 653481d7172ded44edbb257aa7e6b4d5e1e82a00 Mon Sep 17 00:00:00 2001 From: Dominic Amato Date: Fri, 10 Jul 2020 15:40:53 -0500 Subject: [PATCH 24/76] Add BG96 Support (#37) * add bg96 to modems * update description and enable at sockets * Update BG96.py * check if active * send the right permutation * decode and use command * wait for socket to open and handle other actions * better urc handling * more logging * handle attribute errors too * handle urc types better * better socket state handling * bg96 is send ok not ok * log more info * use set instead of command * add send ok as possible results * handle send ok in process response * close properly and update read_socket * handle recv response from urc * fix missing imports * clean up logging a bit * remove another logging instance * fix merge conflict * fix copy pasta --- Hologram/Network/Cellular.py | 3 +- Hologram/Network/Modem/BG96.py | 189 ++++++++++++++++++++++++++++++++ Hologram/Network/Modem/Modem.py | 3 + 3 files changed, 194 insertions(+), 1 deletion(-) create mode 100644 Hologram/Network/Modem/BG96.py diff --git a/Hologram/Network/Cellular.py b/Hologram/Network/Cellular.py index b6431d77..928c69ff 100644 --- a/Hologram/Network/Cellular.py +++ b/Hologram/Network/Cellular.py @@ -11,7 +11,7 @@ from Hologram.Event import Event from Exceptions.HologramError import NetworkError from Hologram.Network.Route import Route -from Hologram.Network.Modem import Modem, E303, MS2131, E372, Nova_U201, NovaM, DriverLoader +from Hologram.Network.Modem import Modem, E303, MS2131, E372, BG96, Nova_U201, NovaM, DriverLoader from Hologram.Network import Network, NetworkScope import time from serial.tools import list_ports @@ -31,6 +31,7 @@ class Cellular(Network): 'e303': E303.E303, 'ms2131': MS2131.MS2131, 'e372': E372.E372, + 'bg96': BG96.BG96, 'nova': Nova_U201.Nova_U201, 'novam': NovaM.NovaM, '': Modem diff --git a/Hologram/Network/Modem/BG96.py b/Hologram/Network/Modem/BG96.py new file mode 100644 index 00000000..c3424c3b --- /dev/null +++ b/Hologram/Network/Modem/BG96.py @@ -0,0 +1,189 @@ +# BG96.py - Hologram Python SDK Quectel BG96 modem interface +# +# Author: Hologram +# +# Copyright 2016 - Hologram (Konekt, Inc.) +# +# +# LICENSE: Distributed under the terms of the MIT License +# +import binascii +import time + +from serial.serialutil import Timeout + +from Hologram.Network.Modem import Modem +from Hologram.Event import Event +from UtilClasses import ModemResult +from Exceptions.HologramError import SerialError, NetworkError + +DEFAULT_BG96_TIMEOUT = 200 + +class BG96(Modem): + usb_ids = [('2c7c', '0296')] + + def __init__(self, device_name=None, baud_rate='9600', + chatscript_file=None, event=Event()): + + super().__init__(device_name=device_name, baud_rate=baud_rate, + chatscript_file=chatscript_file, event=event) + self._at_sockets_available = True + self.urc_response = '' + + def connect(self, timeout=DEFAULT_BG96_TIMEOUT): + + success = super().connect(timeout) + + # put serial mode on other port + # if success is True: + # # detect another open serial port to use for PPP + # devices = self.detect_usable_serial_port() + # if not devices: + # raise SerialError('Not enough serial ports detected for Nova') + # self.logger.debug('Moving connection to port %s', devices[0]) + # self.device_name = devices[0] + # super().initialize_serial_interface() + + return success + + def send_message(self, data, timeout=Modem.DEFAULT_SEND_TIMEOUT): + # Waiting for the open socket urc + while self.urc_state != Modem.SOCKET_WRITE_STATE: + self.checkURC() + + self.write_socket(data) + + loop_timeout = Timeout(timeout) + while self.urc_state != Modem.SOCKET_SEND_READ: + self.checkURC() + if self.urc_state != Modem.SOCKET_SEND_READ: + if loop_timeout.expired(): + raise SerialError('Timeout occurred waiting for message status') + time.sleep(self._RETRY_DELAY) + elif self.urc_state == Modem.SOCKET_CLOSED: + return '[1,0]' #this is connection closed for hologram cloud response + + return self.urc_response + + def create_socket(self): + self._set_up_pdp_context() + + def connect_socket(self, host, port): + self.command('+QIOPEN', '1,0,\"TCP\",\"%s\",%d,0,1' % (host, port)) + # According to the BG96 Docs + # Have to wait for URC response “+QIOPEN: ,” + + def close_socket(self, socket_identifier=None): + ok, _ = self.command('+QICLOSE', self.socket_identifier) + if ok != ModemResult.OK: + self.logger.error('Failed to close socket') + self.urc_state = Modem.SOCKET_CLOSED + + def write_socket(self, data): + hexdata = binascii.hexlify(data) + # We have to do it in chunks of 510 since 512 is actually too long (CMEE error) + # and we need 2n chars for hexified data + for chunk in self._chunks(hexdata, 510): + value = '%d,\"%s\"' % (self.socket_identifier, chunk.decode()) + ok, _ = self.set('+QISENDEX', value, timeout=10) + if ok != ModemResult.OK: + self.logger.error('Failed to write to socket') + raise NetworkError('Failed to write to socket') + + def read_socket(self, socket_identifier=None, payload_length=None): + + if socket_identifier is None: + socket_identifier = self.socket_identifier + + if payload_length is None: + payload_length = self.last_read_payload_length + + ok, resp = self.set('+QIRD', '%d,%d' % (socket_identifier, payload_length)) + if ok == ModemResult.OK: + resp = resp.lstrip('+QIRD: ') + if resp is not None: + resp = resp.strip('"') + try: + resp = resp.decode() + except: + # This is some sort of binary data that can't be decoded so just + # return the bytes. We might want to make this happen via parameter + # in the future so it is more deterministic + self.logger.debug('Could not decode recieved data') + + return resp + + def is_registered(self): + return self.check_registered('+CREG') or self.check_registered('+CGREG') + + # EFFECTS: Handles URC related AT command responses. + def handleURC(self, urc): + if urc.startswith('+QIOPEN: '): + response_list = urc.lstrip('+QIOPEN: ').split(',') + socket_identifier = int(response_list[0]) + err = int(response_list[-1]) + if err == 0: + self.urc_state = Modem.SOCKET_WRITE_STATE + self.socket_identifier = socket_identifier + else: + self.logger.error('Failed to open socket') + raise NetworkError('Failed to open socket') + return + if urc.startswith('+QIURC: '): + response_list = urc.lstrip('+QIURC: ').split(',') + urctype = response_list[0] + if urctype == '\"recv\"': + self.urc_state = Modem.SOCKET_SEND_READ + self.socket_identifier = int(response_list[1]) + self.last_read_payload_length = int(response_list[2]) + self.urc_response = self._readline_from_serial_port(5) + if urctype == '\"closed\"': + self.urc_state = Modem.SOCKET_CLOSED + self.socket_identifier = int(response_list[-1]) + return + super().handleURC(urc) + + def _is_pdp_context_active(self): + if not self.is_registered(): + return False + + ok, r = self.command('+QIACT?') + if ok == ModemResult.OK: + try: + pdpstatus = int(r.lstrip('+QIACT: ').split(',')[1]) + # 1: PDP active + return pdpstatus == 1 + except (IndexError, ValueError) as e: + self.logger.error(repr(e)) + except AttributeError as e: + self.logger.error(repr(e)) + return False + + def init_serial_commands(self): + self.command("E0") #echo off + self.command("+CMEE", "2") #set verbose error codes + self.command("+CPIN?") + self.set_timezone_configs() + #self.command("+CPIN", "") #set SIM PIN + self.command("+CPMS", "\"ME\",\"ME\",\"ME\"") + self.set_sms_configs() + self.set_network_registration_status() + + def set_network_registration_status(self): + self.command("+CREG", "2") + self.command("+CGREG", "2") + + def _set_up_pdp_context(self): + if self._is_pdp_context_active(): return True + self.logger.info('Setting up PDP context') + self.set('+QICSGP', '1,1,\"hologram\",\"\",\"\",1') + ok, _ = self.set('+QIACT', '1', timeout=30) + if ok != ModemResult.OK: + self.logger.error('PDP Context setup failed') + raise NetworkError('Failed PDP context setup') + else: + self.logger.info('PDP context active') + + @property + def description(self): + return 'Quecetel BG96' \ No newline at end of file diff --git a/Hologram/Network/Modem/Modem.py b/Hologram/Network/Modem/Modem.py index 9416adaf..c93fa35a 100644 --- a/Hologram/Network/Modem/Modem.py +++ b/Hologram/Network/Modem/Modem.py @@ -464,6 +464,9 @@ def process_response(self, cmd, timeout=None, hide=False): if response == 'OK': return ModemResult.OK + if response == 'SEND OK': + return ModemResult.OK + if response.startswith('+'): if response.lower().startswith(cmd.lower() + ': '): self.response.append(response) From 12f485ea1cba09213032be14e7ba88bba2546960 Mon Sep 17 00:00:00 2001 From: Dom Amato Date: Fri, 10 Jul 2020 18:12:01 -0500 Subject: [PATCH 25/76] try adding config on develop --- .github/changelog-drafter.yml | 30 ++++++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) create mode 100644 .github/changelog-drafter.yml diff --git a/.github/changelog-drafter.yml b/.github/changelog-drafter.yml new file mode 100644 index 00000000..d0b68cf7 --- /dev/null +++ b/.github/changelog-drafter.yml @@ -0,0 +1,30 @@ +name-template: 'v$RESOLVED_VERSION 🌈' +tag-template: 'v$RESOLVED_VERSION' +categories: + - title: '🚀 Features' + labels: + - 'feature' + - 'enhancement' + - title: '🐛 Bug Fixes' + labels: + - 'fix' + - 'bugfix' + - 'bug' + - title: '🧰 Maintenance' + label: 'chore' +change-template: '- $TITLE @$AUTHOR (#$NUMBER)' +version-resolver: + major: + labels: + - 'major' + minor: + labels: + - 'minor' + patch: + labels: + - 'patch' + default: patch +template: | + ## Changes + + $CHANGES \ No newline at end of file From 82affa95d368b3b1fa33e9fcb43321ce15afc75d Mon Sep 17 00:00:00 2001 From: Dominic Amato Date: Sat, 11 Jul 2020 00:41:01 -0500 Subject: [PATCH 26/76] add some more github actions (#41) --- .github/changelog-drafter.yml | 16 +++++++++++++++- .github/dependabot.yml | 19 +++++++++++++++++++ .github/workflows/changelog.yml | 19 +++++++++++++++++++ .github/workflows/testlint.yml | 2 +- 4 files changed, 54 insertions(+), 2 deletions(-) create mode 100644 .github/dependabot.yml create mode 100644 .github/workflows/changelog.yml diff --git a/.github/changelog-drafter.yml b/.github/changelog-drafter.yml index d0b68cf7..46ab5786 100644 --- a/.github/changelog-drafter.yml +++ b/.github/changelog-drafter.yml @@ -1,5 +1,7 @@ name-template: 'v$RESOLVED_VERSION 🌈' + tag-template: 'v$RESOLVED_VERSION' + categories: - title: '🚀 Features' labels: @@ -11,19 +13,31 @@ categories: - 'bugfix' - 'bug' - title: '🧰 Maintenance' - label: 'chore' + labels: + - 'chore' + - 'debt' + change-template: '- $TITLE @$AUTHOR (#$NUMBER)' + version-resolver: major: labels: - 'major' + - 'breaking' minor: labels: - 'minor' + - 'feature' patch: labels: - 'patch' + - 'bugfix' + - 'debt' default: patch + +exclude-labels: + - 'skip-changelog' + template: | ## Changes diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 00000000..ee015b10 --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,19 @@ +# Basic dependabot.yml file with +# minimum configuration for two package managers +# https://help.github.com/github/administering-a-repository/configuration-options-for-dependency-updates + +version: 2 +updates: + + # Maintain dependencies for GitHub Actions + - package-ecosystem: "github-actions" + directory: "/" + schedule: + interval: "daily" + + + # Enable version updates for python pip + - package-ecosystem: "pip" + directory: "/" + schedule: + interval: "daily" \ No newline at end of file diff --git a/.github/workflows/changelog.yml b/.github/workflows/changelog.yml new file mode 100644 index 00000000..1cd79285 --- /dev/null +++ b/.github/workflows/changelog.yml @@ -0,0 +1,19 @@ +name: Changelog Drafter + +on: + push: + # develop is our tracking branch + branches: + - develop + +jobs: + update_release_draft: + runs-on: ubuntu-latest + steps: + # Drafts your next Release notes as Pull Requests are merged into "master" + - uses: release-drafter/release-drafter@v5 + with: + # (Optional) specify config name to use, relative to .github/. Default: release-drafter.yml + config-name: changelog-drafter.yml + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} \ No newline at end of file diff --git a/.github/workflows/testlint.yml b/.github/workflows/testlint.yml index 41061064..05ab249c 100644 --- a/.github/workflows/testlint.yml +++ b/.github/workflows/testlint.yml @@ -1,4 +1,4 @@ -name: Python package +name: Python Test and Lint on: [push] From 0a53d695190bbbcb4bbcaadae526651755aece76 Mon Sep 17 00:00:00 2001 From: Dom Amato Date: Sat, 11 Jul 2020 00:45:23 -0500 Subject: [PATCH 27/76] add dependency label to config --- .github/changelog-drafter.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/changelog-drafter.yml b/.github/changelog-drafter.yml index 46ab5786..a0c58c0b 100644 --- a/.github/changelog-drafter.yml +++ b/.github/changelog-drafter.yml @@ -16,6 +16,7 @@ categories: labels: - 'chore' - 'debt' + - 'dependencies' change-template: '- $TITLE @$AUTHOR (#$NUMBER)' @@ -33,6 +34,7 @@ version-resolver: - 'patch' - 'bugfix' - 'debt' + - 'dependencies' default: patch exclude-labels: From a52ecaeff9ff762bd3cb4915eeeaed9e514f6940 Mon Sep 17 00:00:00 2001 From: Dominic Amato Date: Tue, 14 Jul 2020 13:17:01 -0500 Subject: [PATCH 28/76] Dont encode buffer (#39) --- Hologram/CustomCloud.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/Hologram/CustomCloud.py b/Hologram/CustomCloud.py index 99dc3ae9..58eb0aea 100644 --- a/Hologram/CustomCloud.py +++ b/Hologram/CustomCloud.py @@ -302,6 +302,7 @@ def acceptIncomingConnection(self): self._receive_cv.acquire() if self.socketClose: + self.logger.debug('Closing socket connection') self._receive_cv.release() break @@ -325,10 +326,11 @@ def __incoming_connection_thread(self, clientsocket): clientsocket.settimeout(RECEIVE_TIMEOUT) # Keep parsing the received data until timeout or receive no more data. - recv = b'' + recv = '' while True: try: result = clientsocket.recv(MAX_RECEIVE_BYTES) + self.logger.debug('Received message: %s', result) except socket.timeout: break if not result: From aa976566f6d7dbd5296780bd88c7c28816ab3779 Mon Sep 17 00:00:00 2001 From: Dominic Amato Date: Tue, 14 Jul 2020 14:36:45 -0500 Subject: [PATCH 29/76] add more info to release --- .github/changelog-drafter.yml | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/.github/changelog-drafter.yml b/.github/changelog-drafter.yml index a0c58c0b..c119deea 100644 --- a/.github/changelog-drafter.yml +++ b/.github/changelog-drafter.yml @@ -43,4 +43,8 @@ exclude-labels: template: | ## Changes - $CHANGES \ No newline at end of file + $CHANGES + + **Release Date:** + **Release Engineer:** $CONTRIBUTORS + **Previous Release:** $PREVIOUS_TAG \ No newline at end of file From 26e24210674a4423626075b438105347d9a41833 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 17 Jul 2020 21:07:31 -0500 Subject: [PATCH 30/76] Bump actions/setup-python from v1 to v2.0.2 (#51) Bumps [actions/setup-python](https://github.com/actions/setup-python) from v1 to v2.0.2. - [Release notes](https://github.com/actions/setup-python/releases) - [Commits](https://github.com/actions/setup-python/compare/v1...7a69c2bc7dc38832443a11bc7c2550ba96c6f45c) Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/publish.yml | 2 +- .github/workflows/testlint.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index b143a530..1e4424c8 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -10,7 +10,7 @@ jobs: steps: - uses: actions/checkout@v2 - name: Set up Python - uses: actions/setup-python@v1 + uses: actions/setup-python@v2.0.2 with: python-version: '3.x' - name: Install dependencies diff --git a/.github/workflows/testlint.yml b/.github/workflows/testlint.yml index 05ab249c..ef33d9aa 100644 --- a/.github/workflows/testlint.yml +++ b/.github/workflows/testlint.yml @@ -13,7 +13,7 @@ jobs: steps: - uses: actions/checkout@v2 - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@v1 + uses: actions/setup-python@v2.0.2 with: python-version: ${{ matrix.python-version }} From 52ae291f81d5d3a23aca06af89e31fac3d4e874f Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 17 Jul 2020 21:12:05 -0500 Subject: [PATCH 31/76] Update psutil requirement from ~=5.6.3 to ~=5.7.2 (#50) Updates the requirements on [psutil](https://github.com/giampaolo/psutil) to permit the latest version. - [Release notes](https://github.com/giampaolo/psutil/releases) - [Changelog](https://github.com/giampaolo/psutil/blob/master/HISTORY.rst) - [Commits](https://github.com/giampaolo/psutil/compare/release-5.6.3...release-5.7.2) Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 573eedcc..2b6af37c 100644 --- a/requirements.txt +++ b/requirements.txt @@ -6,5 +6,5 @@ python-pppd==1.0.3 python-sdk-auth~=0.3.0 pyudev~=0.21.0 pyusb~=1.0.2 -psutil~=5.6.3 +psutil~=5.7.2 requests~=2.22.0 From 56a8f03556771d2ff663ecca7762d77865f69f5f Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 17 Jul 2020 21:12:40 -0500 Subject: [PATCH 32/76] Update hjson requirement from ~=3.0.0 to ~=3.0.1 (#48) Updates the requirements on [hjson](https://github.com/hjson/hjson-py) to permit the latest version. - [Release notes](https://github.com/hjson/hjson-py/releases) - [Changelog](https://github.com/hjson/hjson-py/blob/master/history.md) - [Commits](https://github.com/hjson/hjson-py/compare/v3.0.0...v3.0.1) Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 2b6af37c..43087ee4 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,4 +1,4 @@ -hjson~=3.0.0 +hjson~=3.0.1 mock~=3.0.5 pyroute2==0.5.* pyserial~=3.4.0 From fe7c9a465117633f21265aa4f0220d71668a8674 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 17 Jul 2020 21:15:06 -0500 Subject: [PATCH 33/76] Update pyudev requirement from ~=0.21.0 to ~=0.22.0 (#45) Updates the requirements on [pyudev](https://github.com/pyudev/pyudev) to permit the latest version. - [Release notes](https://github.com/pyudev/pyudev/releases) - [Changelog](https://github.com/pyudev/pyudev/blob/develop-0.22/CHANGES.rst) - [Commits](https://github.com/pyudev/pyudev/compare/v0.21.0...v0.22) Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 43087ee4..c299871a 100644 --- a/requirements.txt +++ b/requirements.txt @@ -4,7 +4,7 @@ pyroute2==0.5.* pyserial~=3.4.0 python-pppd==1.0.3 python-sdk-auth~=0.3.0 -pyudev~=0.21.0 +pyudev~=0.22.0 pyusb~=1.0.2 psutil~=5.7.2 requests~=2.22.0 From 1cac5d5a3263b49f6cdea657ea7df6eef5a554de Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 17 Jul 2020 21:15:58 -0500 Subject: [PATCH 34/76] Update requests requirement from ~=2.22.0 to ~=2.24.0 (#44) Updates the requirements on [requests](https://github.com/psf/requests) to permit the latest version. - [Release notes](https://github.com/psf/requests/releases) - [Changelog](https://github.com/psf/requests/blob/master/HISTORY.md) - [Commits](https://github.com/psf/requests/compare/v2.22.0...v2.24.0) Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index c299871a..e880b7f3 100644 --- a/requirements.txt +++ b/requirements.txt @@ -7,4 +7,4 @@ python-sdk-auth~=0.3.0 pyudev~=0.22.0 pyusb~=1.0.2 psutil~=5.7.2 -requests~=2.22.0 +requests~=2.24.0 From 7d59380bc4e1f63340dd6ab5fd16911f1fc40444 Mon Sep 17 00:00:00 2001 From: Dom Amato Date: Fri, 17 Jul 2020 21:24:57 -0500 Subject: [PATCH 35/76] add section for dependencies --- .github/changelog-drafter.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/changelog-drafter.yml b/.github/changelog-drafter.yml index c119deea..bfdb4f01 100644 --- a/.github/changelog-drafter.yml +++ b/.github/changelog-drafter.yml @@ -16,6 +16,8 @@ categories: labels: - 'chore' - 'debt' + - title: '🛠 Dependency Updates' + labels: - 'dependencies' change-template: '- $TITLE @$AUTHOR (#$NUMBER)' From 94e0d59ee7220b27dd9e00639068091fb8142876 Mon Sep 17 00:00:00 2001 From: Dom Amato Date: Mon, 4 Jan 2021 21:49:06 -0600 Subject: [PATCH 36/76] fix broken python actions --- .github/workflows/testlint.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/testlint.yml b/.github/workflows/testlint.yml index ef33d9aa..ddb6505e 100644 --- a/.github/workflows/testlint.yml +++ b/.github/workflows/testlint.yml @@ -13,7 +13,7 @@ jobs: steps: - uses: actions/checkout@v2 - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@v2.0.2 + uses: actions/setup-python@v2 with: python-version: ${{ matrix.python-version }} From 19b1a045d5697cba1c59a90e7f04d5faa68fb8af Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 4 Jan 2021 21:52:17 -0600 Subject: [PATCH 37/76] Update psutil requirement from ~=5.7.2 to ~=5.8.0 (#70) Updates the requirements on [psutil](https://github.com/giampaolo/psutil) to permit the latest version. - [Release notes](https://github.com/giampaolo/psutil/releases) - [Changelog](https://github.com/giampaolo/psutil/blob/master/HISTORY.rst) - [Commits](https://github.com/giampaolo/psutil/compare/release-5.7.2...release-5.8.0) Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index e880b7f3..3be4fcc2 100644 --- a/requirements.txt +++ b/requirements.txt @@ -6,5 +6,5 @@ python-pppd==1.0.3 python-sdk-auth~=0.3.0 pyudev~=0.22.0 pyusb~=1.0.2 -psutil~=5.7.2 +psutil~=5.8.0 requests~=2.24.0 From 96c3468891d024f7dba4272186909212328cdf4d Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 4 Jan 2021 22:00:45 -0600 Subject: [PATCH 38/76] Update pyusb requirement from ~=1.0.2 to ~=1.1.0 (#59) Updates the requirements on [pyusb](https://github.com/pyusb/pyusb) to permit the latest version. - [Release notes](https://github.com/pyusb/pyusb/releases) - [Commits](https://github.com/pyusb/pyusb/compare/v1.0.2...v1.1.0) Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 3be4fcc2..e2e03cd6 100644 --- a/requirements.txt +++ b/requirements.txt @@ -5,6 +5,6 @@ pyserial~=3.4.0 python-pppd==1.0.3 python-sdk-auth~=0.3.0 pyudev~=0.22.0 -pyusb~=1.0.2 +pyusb~=1.1.0 psutil~=5.8.0 requests~=2.24.0 From 2f92ec8d082cc41215d81186bacef24a16bffd99 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 4 Jan 2021 22:00:56 -0600 Subject: [PATCH 39/76] Update requests requirement from ~=2.24.0 to ~=2.25.1 (#67) Updates the requirements on [requests](https://github.com/psf/requests) to permit the latest version. - [Release notes](https://github.com/psf/requests/releases) - [Changelog](https://github.com/psf/requests/blob/master/HISTORY.md) - [Commits](https://github.com/psf/requests/compare/v2.24.0...v2.25.1) Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index e2e03cd6..baa29a1f 100644 --- a/requirements.txt +++ b/requirements.txt @@ -7,4 +7,4 @@ python-sdk-auth~=0.3.0 pyudev~=0.22.0 pyusb~=1.1.0 psutil~=5.8.0 -requests~=2.24.0 +requests~=2.25.1 From 7157d820077f4571b5e46c362a395c8b8a9c82b9 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 4 Jan 2021 22:06:12 -0600 Subject: [PATCH 40/76] Update pyserial requirement from ~=3.4.0 to ~=3.5 (#72) Updates the requirements on [pyserial](https://github.com/pyserial/pyserial) to permit the latest version. - [Release notes](https://github.com/pyserial/pyserial/releases) - [Changelog](https://github.com/pyserial/pyserial/blob/master/CHANGES.rst) - [Commits](https://github.com/pyserial/pyserial/compare/v3.4...v3.5) Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index baa29a1f..4d28eb9a 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,7 +1,7 @@ hjson~=3.0.1 mock~=3.0.5 pyroute2==0.5.* -pyserial~=3.4.0 +pyserial~=3.5 python-pppd==1.0.3 python-sdk-auth~=0.3.0 pyudev~=0.22.0 From 99dde7af3f74889f167705d11592d11e8b80671a Mon Sep 17 00:00:00 2001 From: Tom Pethtel Date: Sat, 16 Jan 2021 23:25:02 -0500 Subject: [PATCH 41/76] Strip AT command result prefix when issued command ends with ? or % (#6) * Strip AT command result prefix when issued command ends with ? or % * Simplify strip of characters from AT command result --- Hologram/Network/Modem/Modem.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/Hologram/Network/Modem/Modem.py b/Hologram/Network/Modem/Modem.py index c93fa35a..424aaba3 100644 --- a/Hologram/Network/Modem/Modem.py +++ b/Hologram/Network/Modem/Modem.py @@ -737,11 +737,14 @@ def test(self, cmd, expected=None, timeout=None, retries=DEFAULT_SERIAL_RETRIES) #returns the raw result of a command, with the 'CMD: ' prefix stripped def _basic_command(self, cmd, prefix=True): + base_cmd = cmd.rstrip('?%') try: ok, r = self.command(cmd) if ok == ModemResult.OK: if prefix and r.startswith(cmd+': '): return r.lstrip(cmd + ': ') + elif prefix and r.startswith(base_cmd+': '): + return r.lstrip(base_cmd + ': ') else: return r except AttributeError as e: From e205937626708bf2de49a95bc45a9483d3d7f6ba Mon Sep 17 00:00:00 2001 From: Reuben Balik Date: Tue, 9 Mar 2021 15:22:21 -0600 Subject: [PATCH 42/76] Fix type for BG96 (#77) --- Hologram/Network/Modem/BG96.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Hologram/Network/Modem/BG96.py b/Hologram/Network/Modem/BG96.py index c3424c3b..2ff8e71b 100644 --- a/Hologram/Network/Modem/BG96.py +++ b/Hologram/Network/Modem/BG96.py @@ -186,4 +186,4 @@ def _set_up_pdp_context(self): @property def description(self): - return 'Quecetel BG96' \ No newline at end of file + return 'Quectel BG96' From bacbf38c0624a3cc763c7b4c1c7facf574ea5d6c Mon Sep 17 00:00:00 2001 From: Dominic Amato Date: Tue, 27 Apr 2021 16:00:22 -0500 Subject: [PATCH 43/76] allow setting APN (#79) --- Hologram/Network/Modem/BG96.py | 2 +- Hologram/Network/Modem/Modem.py | 13 +++++++++++-- 2 files changed, 12 insertions(+), 3 deletions(-) diff --git a/Hologram/Network/Modem/BG96.py b/Hologram/Network/Modem/BG96.py index 2ff8e71b..23ec9bf9 100644 --- a/Hologram/Network/Modem/BG96.py +++ b/Hologram/Network/Modem/BG96.py @@ -176,7 +176,7 @@ def set_network_registration_status(self): def _set_up_pdp_context(self): if self._is_pdp_context_active(): return True self.logger.info('Setting up PDP context') - self.set('+QICSGP', '1,1,\"hologram\",\"\",\"\",1') + self.set('+QICSGP', f'1,1,\"{self._apn}\",\"\",\"\",1') ok, _ = self.set('+QIACT', '1', timeout=30) if ok != ModemResult.OK: self.logger.error('PDP Context setup failed') diff --git a/Hologram/Network/Modem/Modem.py b/Hologram/Network/Modem/Modem.py index 424aaba3..a116bd8a 100644 --- a/Hologram/Network/Modem/Modem.py +++ b/Hologram/Network/Modem/Modem.py @@ -74,6 +74,7 @@ def __init__(self, device_name=None, baud_rate='9600', self.result = ModemResult.OK self.debug_out = '' self.in_ext = False + self._apn = 'hologram' self._initialize_device_name(device_name) @@ -711,7 +712,7 @@ def _is_pdp_context_active(self): def _set_up_pdp_context(self): if self._is_pdp_context_active(): return True self.logger.info('Setting up PDP context') - self.set('+UPSD', '0,1,\"hologram\"') + self.set('+UPSD', f'0,1,\"{self._apn}\"') self.set('+UPSD', '0,7,\"0.0.0.0\"') ok, _ = self.set('+UPSDA', '0,3', timeout=30) if ok != ModemResult.OK: @@ -913,7 +914,6 @@ def remoteIPAddress(self): else: return None - @property def version(self): raise NotImplementedError('This modem does not support this property') @@ -921,3 +921,12 @@ def version(self): @property def imei(self): return self._basic_command('+GSN') + + @property + def apn(self): + return self._apn + + @apn.setter + def apn(self, apn): + self._apn = apn + return self.set('+CGDCONT', f'1,"IP","{self._apn}"') From 582c8dca01dc333d03956868adbf53702900d9f3 Mon Sep 17 00:00:00 2001 From: Dominic Amato Date: Fri, 30 Apr 2021 16:14:08 -0500 Subject: [PATCH 44/76] properly handle bytes when reading from an open socket (#74) --- Hologram/CustomCloud.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Hologram/CustomCloud.py b/Hologram/CustomCloud.py index 58eb0aea..9cd3e269 100644 --- a/Hologram/CustomCloud.py +++ b/Hologram/CustomCloud.py @@ -326,7 +326,7 @@ def __incoming_connection_thread(self, clientsocket): clientsocket.settimeout(RECEIVE_TIMEOUT) # Keep parsing the received data until timeout or receive no more data. - recv = '' + recv = b'' while True: try: result = clientsocket.recv(MAX_RECEIVE_BYTES) @@ -337,12 +337,12 @@ def __incoming_connection_thread(self, clientsocket): break recv += result - self.logger.info('Received message: %s', recv) + self.logger.info('Received message: %s', recv.decode()) self._receive_buffer_lock.acquire() # Append received message into receive buffer - self._receive_buffer.append(recv) + self._receive_buffer.append(recv.decode()) self.logger.debug('Receive buffer: %s', self._receive_buffer) self._receive_buffer_lock.release() From a622ee1cdad2578bc05b647640ed52bebe95d722 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 30 Apr 2021 16:26:09 -0500 Subject: [PATCH 45/76] Update pyusb requirement from ~=1.1.0 to ~=1.1.1 (#75) Updates the requirements on [pyusb](https://github.com/pyusb/pyusb) to permit the latest version. - [Release notes](https://github.com/pyusb/pyusb/releases) - [Commits](https://github.com/pyusb/pyusb/compare/v1.1.0...v1.1.1) Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 4d28eb9a..f36e43a4 100644 --- a/requirements.txt +++ b/requirements.txt @@ -5,6 +5,6 @@ pyserial~=3.5 python-pppd==1.0.3 python-sdk-auth~=0.3.0 pyudev~=0.22.0 -pyusb~=1.1.0 +pyusb~=1.1.1 psutil~=5.8.0 requests~=2.25.1 From 1c40eeba639273ef57b7d61d4f0a03c0c607768b Mon Sep 17 00:00:00 2001 From: Dominic Amato Date: Wed, 30 Mar 2022 12:07:55 -0500 Subject: [PATCH 46/76] Update install script to check for python 3.9 new raspbian has python set to 3.9.2 --- install.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/install.sh b/install.sh index 191387ca..0e89d69e 100755 --- a/install.sh +++ b/install.sh @@ -13,7 +13,7 @@ set -euo pipefail # This script will install the Hologram SDK and the necessary software dependencies # for it to work. -required_programs=('python3' 'pip3' 'ps' 'kill' 'libpython3.7-dev') +required_programs=('python3' 'pip3' 'ps' 'kill' 'libpython3.9-dev') OS='' # Check OS. From 3990905a13020ebf468aded291117a597cbd303c Mon Sep 17 00:00:00 2001 From: Dominic Amato Date: Wed, 30 Mar 2022 13:15:57 -0500 Subject: [PATCH 47/76] Also update update script to use python 3.9 --- update.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/update.sh b/update.sh index 7b602f2b..f52a6366 100755 --- a/update.sh +++ b/update.sh @@ -10,7 +10,7 @@ set -euo pipefail -required_programs=('python3' 'pip3' 'ps' 'kill' 'libpython3.7-dev') +required_programs=('python3' 'pip3' 'ps' 'kill' 'libpython3.9-dev') OS='' # Check OS. From 5733bdf64c7c05e1710a33c8a1ee8c2116865bec Mon Sep 17 00:00:00 2001 From: Dominic Amato Date: Wed, 2 Nov 2022 13:28:48 -0500 Subject: [PATCH 48/76] Tear down PDP context after sending socket messages (#90) * Tear down PDP context on socket close * don't expect timeout call, simplify close socket call --- Hologram/Network/Modem/BG96.py | 10 ++++++++++ Hologram/Network/Modem/Modem.py | 9 +++++++++ Hologram/Network/Modem/NovaM.py | 9 --------- Hologram/Network/Modem/Nova_U201.py | 4 ++++ tests/Modem/test_NovaM.py | 4 ++-- 5 files changed, 25 insertions(+), 11 deletions(-) diff --git a/Hologram/Network/Modem/BG96.py b/Hologram/Network/Modem/BG96.py index 23ec9bf9..f4893cce 100644 --- a/Hologram/Network/Modem/BG96.py +++ b/Hologram/Network/Modem/BG96.py @@ -78,6 +78,7 @@ def close_socket(self, socket_identifier=None): if ok != ModemResult.OK: self.logger.error('Failed to close socket') self.urc_state = Modem.SOCKET_CLOSED + self._tear_down_pdp_context() def write_socket(self, data): hexdata = binascii.hexlify(data) @@ -184,6 +185,15 @@ def _set_up_pdp_context(self): else: self.logger.info('PDP context active') + def _tear_down_pdp_context(self): + if not self._is_pdp_context_active(): return True + self.logger.info('Tearing down PDP context') + ok, _ = self.set('+QIACT', '0', timeout=30) + if ok != ModemResult.OK: + self.logger.error('PDP Context tear down failed') + else: + self.logger.info('PDP context deactivated') + @property def description(self): return 'Quectel BG96' diff --git a/Hologram/Network/Modem/Modem.py b/Hologram/Network/Modem/Modem.py index a116bd8a..37896f1d 100644 --- a/Hologram/Network/Modem/Modem.py +++ b/Hologram/Network/Modem/Modem.py @@ -721,6 +721,15 @@ def _set_up_pdp_context(self): else: self.logger.info('PDP context active') + def _tear_down_pdp_context(self): + if not self._is_pdp_context_active(): return True + self.logger.info('Tearing down PDP context') + ok, _ = self.set('+UPSDA', '0,4', timeout=30) + if ok != ModemResult.OK: + self.logger.error('PDP Context tear down failed') + else: + self.logger.info('PDP context deactivated') + def __enforce_serial_port_open(self): if not (self.serial_port and self.serial_port.isOpen()): diff --git a/Hologram/Network/Modem/NovaM.py b/Hologram/Network/Modem/NovaM.py index b57c3a5e..af7ddf54 100644 --- a/Hologram/Network/Modem/NovaM.py +++ b/Hologram/Network/Modem/NovaM.py @@ -48,15 +48,6 @@ def set_network_registration_status(self): def is_registered(self): return self.check_registered('+CEREG') - def close_socket(self, socket_identifier=None): - - if socket_identifier is None: - socket_identifier = self.socket_identifier - - ok, r = self.set('+USOCL', "%s" % socket_identifier, timeout=40) - if ok != ModemResult.OK: - self.logger.error('Failed to close socket') - @property def description(self): modemtype = '(R410)' if self.is_r410 else '(R404)' diff --git a/Hologram/Network/Modem/Nova_U201.py b/Hologram/Network/Modem/Nova_U201.py index ad21c049..b2a315ad 100644 --- a/Hologram/Network/Modem/Nova_U201.py +++ b/Hologram/Network/Modem/Nova_U201.py @@ -50,6 +50,10 @@ def create_socket(self): self._set_up_pdp_context() super().create_socket() + def close_socket(self, socket_identifier=None): + super().close_socket(socket_identifier) + self._tear_down_pdp_context() + def is_registered(self): return self.check_registered('+CREG') or self.check_registered('+CGREG') diff --git a/tests/Modem/test_NovaM.py b/tests/Modem/test_NovaM.py index 41215325..bd1bb16d 100644 --- a/tests/Modem/test_NovaM.py +++ b/tests/Modem/test_NovaM.py @@ -73,7 +73,7 @@ def test_close_socket_no_args(mock_set, no_serial_port): mock_set.return_value = (0,0) mock_set.reset_mock() modem.close_socket() - mock_set.assert_called_once_with('+USOCL', '0', timeout=40) + mock_set.assert_called_once_with('+USOCL', '0') @patch.object(NovaM, 'set') def test_close_socket_with_socket_identifier(mock_set, no_serial_port): @@ -81,7 +81,7 @@ def test_close_socket_with_socket_identifier(mock_set, no_serial_port): mock_set.return_value = (0,0) mock_set.reset_mock() modem.close_socket(5) - mock_set.assert_called_once_with('+USOCL', '5', timeout=40) + mock_set.assert_called_once_with('+USOCL', '5') @patch.object(NovaM, 'command') def test_set_network_registration_status(mock_command, no_serial_port): From 01ffd919fe4e7389c9d5d793ed7f40cfb47c318c Mon Sep 17 00:00:00 2001 From: Dominic Amato Date: Wed, 2 Nov 2022 13:29:01 -0500 Subject: [PATCH 49/76] Clean up unused and test dependencies (#73) * move test dependency to own requirements file * update test file and dependency file * update mock version --- .github/workflows/testlint.yml | 2 +- requirements-dev.txt | 2 ++ requirements.txt | 2 -- setup.py | 1 + 4 files changed, 4 insertions(+), 3 deletions(-) create mode 100644 requirements-dev.txt diff --git a/.github/workflows/testlint.yml b/.github/workflows/testlint.yml index ddb6505e..19294da6 100644 --- a/.github/workflows/testlint.yml +++ b/.github/workflows/testlint.yml @@ -20,7 +20,7 @@ jobs: - name: Install dependencies run: | python -m pip install --upgrade pip - pip install -r requirements.txt + pip install -r requirements-dev.txt - name: Lint with flake8 run: | diff --git a/requirements-dev.txt b/requirements-dev.txt new file mode 100644 index 00000000..7e69507b --- /dev/null +++ b/requirements-dev.txt @@ -0,0 +1,2 @@ +-r requirements.txt +mock~=4.0.3 diff --git a/requirements.txt b/requirements.txt index f36e43a4..0d162d44 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,5 +1,3 @@ -hjson~=3.0.1 -mock~=3.0.5 pyroute2==0.5.* pyserial~=3.5 python-pppd==1.0.3 diff --git a/setup.py b/setup.py index 2d27cc9e..6fa2d6e7 100755 --- a/setup.py +++ b/setup.py @@ -33,6 +33,7 @@ url = 'https://github.com/hologram-io/hologram-python/', packages = find_packages(), include_package_data = True, + tests_require = open('requirements-dev.txt').read().split(), install_requires = open('requirements.txt').read().split(), scripts = ['scripts/hologram'], license = 'MIT', From 9d98eab495ad05ad3e7b268b824d3ca45e294743 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 2 Nov 2022 13:31:54 -0500 Subject: [PATCH 50/76] Update pyusb requirement from ~=1.1.1 to ~=1.2.1 (#88) Updates the requirements on [pyusb](https://github.com/pyusb/pyusb) to permit the latest version. - [Release notes](https://github.com/pyusb/pyusb/releases) - [Commits](https://github.com/pyusb/pyusb/compare/v1.1.1...v1.2.1) --- updated-dependencies: - dependency-name: pyusb dependency-type: direct:production ... Signed-off-by: dependabot[bot] Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 0d162d44..dbf5e16f 100644 --- a/requirements.txt +++ b/requirements.txt @@ -3,6 +3,6 @@ pyserial~=3.5 python-pppd==1.0.3 python-sdk-auth~=0.3.0 pyudev~=0.22.0 -pyusb~=1.1.1 +pyusb~=1.2.1 psutil~=5.8.0 requests~=2.25.1 From dacca2a86f142564108599612e75608089195de6 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 2 Nov 2022 13:34:11 -0500 Subject: [PATCH 51/76] Bump python-pppd from 1.0.3 to 1.0.4 (#83) Bumps [python-pppd](https://github.com/cour4g3/python-pppd) from 1.0.3 to 1.0.4. - [Release notes](https://github.com/cour4g3/python-pppd/releases) - [Commits](https://github.com/cour4g3/python-pppd/compare/v1.0.3...1.0.4) --- updated-dependencies: - dependency-name: python-pppd dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index dbf5e16f..f0c35e59 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,6 +1,6 @@ pyroute2==0.5.* pyserial~=3.5 -python-pppd==1.0.3 +python-pppd==1.0.4 python-sdk-auth~=0.3.0 pyudev~=0.22.0 pyusb~=1.2.1 From a281cd7712e09a0d9623545c210ace92caeebb7a Mon Sep 17 00:00:00 2001 From: Dom Amato Date: Wed, 2 Nov 2022 22:51:39 -0500 Subject: [PATCH 52/76] fix setup.py so that installing isnt broken --- requirements-dev.txt | 2 -- requirements-test.txt | 1 + setup.py | 2 +- 3 files changed, 2 insertions(+), 3 deletions(-) delete mode 100644 requirements-dev.txt create mode 100644 requirements-test.txt diff --git a/requirements-dev.txt b/requirements-dev.txt deleted file mode 100644 index 7e69507b..00000000 --- a/requirements-dev.txt +++ /dev/null @@ -1,2 +0,0 @@ --r requirements.txt -mock~=4.0.3 diff --git a/requirements-test.txt b/requirements-test.txt new file mode 100644 index 00000000..82800846 --- /dev/null +++ b/requirements-test.txt @@ -0,0 +1 @@ +mock~=4.0.3 diff --git a/setup.py b/setup.py index 6fa2d6e7..acbfbf50 100755 --- a/setup.py +++ b/setup.py @@ -33,7 +33,7 @@ url = 'https://github.com/hologram-io/hologram-python/', packages = find_packages(), include_package_data = True, - tests_require = open('requirements-dev.txt').read().split(), + tests_require = open('requirements-test.txt').read().split(), install_requires = open('requirements.txt').read().split(), scripts = ['scripts/hologram'], license = 'MIT', From a52de207117355ef039ba8a4fdf40971a23751c9 Mon Sep 17 00:00:00 2001 From: Dom Amato Date: Wed, 2 Nov 2022 23:10:16 -0500 Subject: [PATCH 53/76] update action file --- .github/workflows/testlint.yml | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/.github/workflows/testlint.yml b/.github/workflows/testlint.yml index 19294da6..0e61a748 100644 --- a/.github/workflows/testlint.yml +++ b/.github/workflows/testlint.yml @@ -8,7 +8,7 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - python-version: [3.7] + python-version: [3.9] steps: - uses: actions/checkout@v2 @@ -20,7 +20,8 @@ jobs: - name: Install dependencies run: | python -m pip install --upgrade pip - pip install -r requirements-dev.txt + pip install -r requirements-test.txt + pip install -r requirements.txt - name: Lint with flake8 run: | From a3bf1cb2bead0863ecf4314c0a5003788b01a9e7 Mon Sep 17 00:00:00 2001 From: Dominic Amato Date: Thu, 10 Nov 2022 12:26:32 -0600 Subject: [PATCH 54/76] Add EC21 to supported modems (#109) --- Hologram/Network/Cellular.py | 3 +- Hologram/Network/Modem/EC21.py | 191 ++++++++++++++++++++++++++++ Hologram/Network/Modem/Modem.py | 12 +- Hologram/Network/Modem/NovaM.py | 10 -- Hologram/Network/Modem/Nova_U201.py | 7 + Hologram/Network/Modem/__init__.py | 2 +- 6 files changed, 208 insertions(+), 17 deletions(-) create mode 100644 Hologram/Network/Modem/EC21.py diff --git a/Hologram/Network/Cellular.py b/Hologram/Network/Cellular.py index 928c69ff..dc4dbc1f 100644 --- a/Hologram/Network/Cellular.py +++ b/Hologram/Network/Cellular.py @@ -11,7 +11,7 @@ from Hologram.Event import Event from Exceptions.HologramError import NetworkError from Hologram.Network.Route import Route -from Hologram.Network.Modem import Modem, E303, MS2131, E372, BG96, Nova_U201, NovaM, DriverLoader +from Hologram.Network.Modem import Modem, E303, MS2131, E372, BG96, EC21, Nova_U201, NovaM, DriverLoader from Hologram.Network import Network, NetworkScope import time from serial.tools import list_ports @@ -32,6 +32,7 @@ class Cellular(Network): 'ms2131': MS2131.MS2131, 'e372': E372.E372, 'bg96': BG96.BG96, + 'ec21': EC21.EC21, 'nova': Nova_U201.Nova_U201, 'novam': NovaM.NovaM, '': Modem diff --git a/Hologram/Network/Modem/EC21.py b/Hologram/Network/Modem/EC21.py new file mode 100644 index 00000000..5bb0d8a7 --- /dev/null +++ b/Hologram/Network/Modem/EC21.py @@ -0,0 +1,191 @@ +# EC21.py - Hologram Python SDK Quectel EC21 modem interface +# +# Author: Hologram +# +# Copyright 2016 - Hologram (Konekt, Inc.) +# +# +# LICENSE: Distributed under the terms of the MIT License +# +import binascii +import time + +from serial.serialutil import Timeout + +from Hologram.Network.Modem import Modem +from Hologram.Event import Event +from UtilClasses import ModemResult +from Exceptions.HologramError import SerialError, NetworkError + +DEFAULT_EC21_TIMEOUT = 200 + +class EC21(Modem): + usb_ids = [('2c7c', '0121')] + + def __init__(self, device_name=None, baud_rate='9600', + chatscript_file=None, event=Event()): + + super().__init__(device_name=device_name, baud_rate=baud_rate, + chatscript_file=chatscript_file, event=event) + self._at_sockets_available = True + self.urc_response = '' + + def connect(self, timeout=DEFAULT_EC21_TIMEOUT): + success = super().connect(timeout) + return success + + def send_message(self, data, timeout=Modem.DEFAULT_SEND_TIMEOUT): + # Waiting for the open socket urc + while self.urc_state != Modem.SOCKET_WRITE_STATE: + self.checkURC() + + self.write_socket(data) + + loop_timeout = Timeout(timeout) + while self.urc_state != Modem.SOCKET_SEND_READ: + self.checkURC() + if self.urc_state != Modem.SOCKET_SEND_READ: + if loop_timeout.expired(): + raise SerialError('Timeout occurred waiting for message status') + time.sleep(self._RETRY_DELAY) + elif self.urc_state == Modem.SOCKET_CLOSED: + return '[1,0]' #this is connection closed for hologram cloud response + + return self.urc_response.rstrip('\r\n') + + def create_socket(self): + self._set_up_pdp_context() + + def connect_socket(self, host, port): + self.command('+QIOPEN', '1,0,\"TCP\",\"%s\",%d,0,1' % (host, port)) + # According to the EC21 Docs + # Have to wait for URC response “+QIOPEN: ,” + + def close_socket(self, socket_identifier=None): + ok, _ = self.command('+QICLOSE', self.socket_identifier) + if ok != ModemResult.OK: + self.logger.error('Failed to close socket') + self.urc_state = Modem.SOCKET_CLOSED + self._tear_down_pdp_context() + + def write_socket(self, data): + hexdata = binascii.hexlify(data) + # We have to do it in chunks of 510 since 512 is actually too long (CMEE error) + # and we need 2n chars for hexified data + for chunk in self._chunks(hexdata, 510): + value = '%d,\"%s\"' % (self.socket_identifier, chunk.decode()) + ok, _ = self.set('+QISENDEX', value, timeout=10) + if ok != ModemResult.OK: + self.logger.error('Failed to write to socket') + raise NetworkError('Failed to write to socket') + + def read_socket(self, socket_identifier=None, payload_length=None): + + if socket_identifier is None: + socket_identifier = self.socket_identifier + + if payload_length is None: + payload_length = self.last_read_payload_length + + ok, resp = self.set('+QIRD', '%d,%d' % (socket_identifier, payload_length)) + if ok == ModemResult.OK: + resp = resp.lstrip('+QIRD: ') + if resp is not None: + resp = resp.strip('"') + try: + resp = resp.decode() + except: + # This is some sort of binary data that can't be decoded so just + # return the bytes. We might want to make this happen via parameter + # in the future so it is more deterministic + self.logger.debug('Could not decode recieved data') + + return resp + + def listen_socket(self, port): + # No equivilent exists for quectel modems + pass + + def is_registered(self): + return self.check_registered('+CREG') or self.check_registered('+CEREG') + + # EFFECTS: Handles URC related AT command responses. + def handleURC(self, urc): + if urc.startswith('+QIOPEN: '): + response_list = urc.lstrip('+QIOPEN: ').split(',') + socket_identifier = int(response_list[0]) + err = int(response_list[-1]) + if err == 0: + self.urc_state = Modem.SOCKET_WRITE_STATE + self.socket_identifier = socket_identifier + else: + self.logger.error('Failed to open socket') + raise NetworkError('Failed to open socket') + return + if urc.startswith('+QIURC: '): + response_list = urc.lstrip('+QIURC: ').split(',') + urctype = response_list[0] + if urctype == '\"recv\"': + self.urc_state = Modem.SOCKET_SEND_READ + self.socket_identifier = int(response_list[1]) + self.last_read_payload_length = int(response_list[2]) + self.urc_response = self._readline_from_serial_port(5) + if urctype == '\"closed\"': + self.urc_state = Modem.SOCKET_CLOSED + self.socket_identifier = int(response_list[-1]) + return + super().handleURC(urc) + + def _is_pdp_context_active(self): + if not self.is_registered(): + return False + + ok, r = self.command('+QIACT?') + if ok == ModemResult.OK: + try: + pdpstatus = int(r.lstrip('+QIACT: ').split(',')[1]) + # 1: PDP active + return pdpstatus == 1 + except (IndexError, ValueError) as e: + self.logger.error(repr(e)) + except AttributeError as e: + self.logger.error(repr(e)) + return False + + def init_serial_commands(self): + self.command("E0") #echo off + self.command("+CMEE", "2") #set verbose error codes + self.command("+CPIN?") + self.set_timezone_configs() + #self.command("+CPIN", "") #set SIM PIN + self.command("+CPMS", "\"ME\",\"ME\",\"ME\"") + self.set_sms_configs() + self.set_network_registration_status() + + def set_network_registration_status(self): + self.command("+CREG", "2") + self.command("+CEREG", "2") + + def _set_up_pdp_context(self): + if self._is_pdp_context_active(): return True + self.logger.info('Setting up PDP context') + self.set('+QICSGP', f'1,1,\"{self._apn}\",\"\",\"\",1') + ok, _ = self.set('+QIACT', '1', timeout=30) + if ok != ModemResult.OK: + self.logger.error('PDP Context setup failed') + raise NetworkError('Failed PDP context setup') + else: + self.logger.info('PDP context active') + + def _tear_down_pdp_context(self): + if not self._is_pdp_context_active(): return True + self.logger.info('Tearing down PDP context') + ok, _ = self.set('+QIDEACT', '1', timeout=30) + if ok != ModemResult.OK: + self.logger.error('PDP Context tear down failed') + else: + self.logger.info('PDP context deactivated') + + @property + def description(self): + return 'Quectel EC21' diff --git a/Hologram/Network/Modem/Modem.py b/Hologram/Network/Modem/Modem.py index 37896f1d..c55468cc 100644 --- a/Hologram/Network/Modem/Modem.py +++ b/Hologram/Network/Modem/Modem.py @@ -865,14 +865,16 @@ def modem_id(self): @property def iccid(self): - return self._basic_command('+CCID') + return self._basic_command('+CCID').rstrip('F') @property def operator(self): - op = self._basic_set('+UDOPN','12') - if op is not None: - return op.strip('"') - return op + ret = self._basic_command('+COPS?') + if ret is not None: + parts = ret.split(',') + if len(parts) >= 3: + return parts[2].strip('"') + return None @property def location(self): diff --git a/Hologram/Network/Modem/NovaM.py b/Hologram/Network/Modem/NovaM.py index af7ddf54..9fbb95ec 100644 --- a/Hologram/Network/Modem/NovaM.py +++ b/Hologram/Network/Modem/NovaM.py @@ -57,16 +57,6 @@ def description(self): def location(self): raise NotImplementedError('The R404 and R410 do not support Cell Locate at this time') - @property - def operator(self): - # R4 series doesn't have UDOPN so need to override - ret = self._basic_command('+COPS?') - parts = ret.split(',') - if len(parts) >= 3: - return parts[2].strip('"') - return None - - # same as Modem::connect_socket except with longer timeout def connect_socket(self, host, port): at_command_val = "%d,\"%s\",%s" % (self.socket_identifier, host, port) diff --git a/Hologram/Network/Modem/Nova_U201.py b/Hologram/Network/Modem/Nova_U201.py index b2a315ad..b3e3ddc3 100644 --- a/Hologram/Network/Modem/Nova_U201.py +++ b/Hologram/Network/Modem/Nova_U201.py @@ -134,3 +134,10 @@ def location(self): @property def description(self): return 'Hologram Nova Global 3G/2G Cellular USB Modem (U201)' + + @property + def operator(self): + op = self._basic_set('+UDOPN','12') + if op is not None: + return op.strip('"') + return op \ No newline at end of file diff --git a/Hologram/Network/Modem/__init__.py b/Hologram/Network/Modem/__init__.py index fe44d41c..84491b79 100644 --- a/Hologram/Network/Modem/__init__.py +++ b/Hologram/Network/Modem/__init__.py @@ -1,4 +1,4 @@ -__all__ = ['Modem', 'MockModem', 'MS2131', 'Nova', 'E303', 'E372'] +__all__ = ['Modem', 'MockModem', 'MS2131', 'Nova', 'E303', 'E372', 'EC21', 'BG96'] from .IModem import IModem from .Modem import Modem From 71c4bdecb7d20a4223f6a8a8387bdcc97b897950 Mon Sep 17 00:00:00 2001 From: Hologram Interview <70453650+HologramInterview@users.noreply.github.com> Date: Wed, 16 Nov 2022 12:31:05 -0500 Subject: [PATCH 55/76] raise an authentication error if its unable to get keys, consolidate common code (#115) --- Hologram/HologramCloud.py | 3 +- Hologram/Network/Modem/BG96.py | 174 +----------------------- Hologram/Network/Modem/E303.py | 10 +- Hologram/Network/Modem/E372.py | 10 +- Hologram/Network/Modem/EC21.py | 161 +--------------------- Hologram/Network/Modem/MS2131.py | 10 +- Hologram/Network/Modem/Modem.py | 8 +- Hologram/Network/Modem/NovaM.py | 9 -- Hologram/Network/Modem/Nova_U201.py | 10 -- Hologram/Network/Modem/Quectel.py | 161 ++++++++++++++++++++++ Hologram/Network/Modem/__init__.py | 2 +- tests/API/test_API.py | 82 +++++++++++ tests/MessageMode/test_HologramCloud.py | 9 +- tests/Modem/test_BG96.py | 74 ++++++++++ tests/Modem/test_EC21.py | 74 ++++++++++ tests/Modem/test_Modem.py | 4 + tests/Modem/test_Quectel.py | 150 ++++++++++++++++++++ 17 files changed, 571 insertions(+), 380 deletions(-) create mode 100644 Hologram/Network/Modem/Quectel.py create mode 100644 tests/API/test_API.py create mode 100644 tests/Modem/test_BG96.py create mode 100644 tests/Modem/test_EC21.py create mode 100644 tests/Modem/test_Quectel.py diff --git a/Hologram/HologramCloud.py b/Hologram/HologramCloud.py index 6c730363..6489e960 100755 --- a/Hologram/HologramCloud.py +++ b/Hologram/HologramCloud.py @@ -14,7 +14,7 @@ from Hologram.CustomCloud import CustomCloud from HologramAuth import TOTPAuthentication, SIMOTPAuthentication from Hologram.Authentication import CSRPSKAuthentication -from Exceptions.HologramError import HologramError +from Exceptions.HologramError import HologramError, AuthenticationError DEFAULT_SEND_MESSAGE_TIMEOUT = 5 HOLOGRAM_HOST_SEND = 'cloudsocket.hologram.io' @@ -125,6 +125,7 @@ def __populate_totp_credentials(self): self.authentication.credentials['private_key'] = self.network.imsi except Exception as e: self.logger.error('Unable to fetch device id or private key') + raise AuthenticationError('Unable to fetch device id or private key for TOTP authenication') def __populate_sim_otp_credentials(self): nonce = self.request_hex_nonce() diff --git a/Hologram/Network/Modem/BG96.py b/Hologram/Network/Modem/BG96.py index f4893cce..fc1b4edf 100644 --- a/Hologram/Network/Modem/BG96.py +++ b/Hologram/Network/Modem/BG96.py @@ -7,183 +7,17 @@ # # LICENSE: Distributed under the terms of the MIT License # -import binascii -import time -from serial.serialutil import Timeout - -from Hologram.Network.Modem import Modem -from Hologram.Event import Event +from Hologram.Network.Modem.Quectel import Quectel from UtilClasses import ModemResult -from Exceptions.HologramError import SerialError, NetworkError DEFAULT_BG96_TIMEOUT = 200 -class BG96(Modem): +class BG96(Quectel): usb_ids = [('2c7c', '0296')] - - def __init__(self, device_name=None, baud_rate='9600', - chatscript_file=None, event=Event()): - - super().__init__(device_name=device_name, baud_rate=baud_rate, - chatscript_file=chatscript_file, event=event) - self._at_sockets_available = True - self.urc_response = '' - + def connect(self, timeout=DEFAULT_BG96_TIMEOUT): - - success = super().connect(timeout) - - # put serial mode on other port - # if success is True: - # # detect another open serial port to use for PPP - # devices = self.detect_usable_serial_port() - # if not devices: - # raise SerialError('Not enough serial ports detected for Nova') - # self.logger.debug('Moving connection to port %s', devices[0]) - # self.device_name = devices[0] - # super().initialize_serial_interface() - - return success - - def send_message(self, data, timeout=Modem.DEFAULT_SEND_TIMEOUT): - # Waiting for the open socket urc - while self.urc_state != Modem.SOCKET_WRITE_STATE: - self.checkURC() - - self.write_socket(data) - - loop_timeout = Timeout(timeout) - while self.urc_state != Modem.SOCKET_SEND_READ: - self.checkURC() - if self.urc_state != Modem.SOCKET_SEND_READ: - if loop_timeout.expired(): - raise SerialError('Timeout occurred waiting for message status') - time.sleep(self._RETRY_DELAY) - elif self.urc_state == Modem.SOCKET_CLOSED: - return '[1,0]' #this is connection closed for hologram cloud response - - return self.urc_response - - def create_socket(self): - self._set_up_pdp_context() - - def connect_socket(self, host, port): - self.command('+QIOPEN', '1,0,\"TCP\",\"%s\",%d,0,1' % (host, port)) - # According to the BG96 Docs - # Have to wait for URC response “+QIOPEN: ,” - - def close_socket(self, socket_identifier=None): - ok, _ = self.command('+QICLOSE', self.socket_identifier) - if ok != ModemResult.OK: - self.logger.error('Failed to close socket') - self.urc_state = Modem.SOCKET_CLOSED - self._tear_down_pdp_context() - - def write_socket(self, data): - hexdata = binascii.hexlify(data) - # We have to do it in chunks of 510 since 512 is actually too long (CMEE error) - # and we need 2n chars for hexified data - for chunk in self._chunks(hexdata, 510): - value = '%d,\"%s\"' % (self.socket_identifier, chunk.decode()) - ok, _ = self.set('+QISENDEX', value, timeout=10) - if ok != ModemResult.OK: - self.logger.error('Failed to write to socket') - raise NetworkError('Failed to write to socket') - - def read_socket(self, socket_identifier=None, payload_length=None): - - if socket_identifier is None: - socket_identifier = self.socket_identifier - - if payload_length is None: - payload_length = self.last_read_payload_length - - ok, resp = self.set('+QIRD', '%d,%d' % (socket_identifier, payload_length)) - if ok == ModemResult.OK: - resp = resp.lstrip('+QIRD: ') - if resp is not None: - resp = resp.strip('"') - try: - resp = resp.decode() - except: - # This is some sort of binary data that can't be decoded so just - # return the bytes. We might want to make this happen via parameter - # in the future so it is more deterministic - self.logger.debug('Could not decode recieved data') - - return resp - - def is_registered(self): - return self.check_registered('+CREG') or self.check_registered('+CGREG') - - # EFFECTS: Handles URC related AT command responses. - def handleURC(self, urc): - if urc.startswith('+QIOPEN: '): - response_list = urc.lstrip('+QIOPEN: ').split(',') - socket_identifier = int(response_list[0]) - err = int(response_list[-1]) - if err == 0: - self.urc_state = Modem.SOCKET_WRITE_STATE - self.socket_identifier = socket_identifier - else: - self.logger.error('Failed to open socket') - raise NetworkError('Failed to open socket') - return - if urc.startswith('+QIURC: '): - response_list = urc.lstrip('+QIURC: ').split(',') - urctype = response_list[0] - if urctype == '\"recv\"': - self.urc_state = Modem.SOCKET_SEND_READ - self.socket_identifier = int(response_list[1]) - self.last_read_payload_length = int(response_list[2]) - self.urc_response = self._readline_from_serial_port(5) - if urctype == '\"closed\"': - self.urc_state = Modem.SOCKET_CLOSED - self.socket_identifier = int(response_list[-1]) - return - super().handleURC(urc) - - def _is_pdp_context_active(self): - if not self.is_registered(): - return False - - ok, r = self.command('+QIACT?') - if ok == ModemResult.OK: - try: - pdpstatus = int(r.lstrip('+QIACT: ').split(',')[1]) - # 1: PDP active - return pdpstatus == 1 - except (IndexError, ValueError) as e: - self.logger.error(repr(e)) - except AttributeError as e: - self.logger.error(repr(e)) - return False - - def init_serial_commands(self): - self.command("E0") #echo off - self.command("+CMEE", "2") #set verbose error codes - self.command("+CPIN?") - self.set_timezone_configs() - #self.command("+CPIN", "") #set SIM PIN - self.command("+CPMS", "\"ME\",\"ME\",\"ME\"") - self.set_sms_configs() - self.set_network_registration_status() - - def set_network_registration_status(self): - self.command("+CREG", "2") - self.command("+CGREG", "2") - - def _set_up_pdp_context(self): - if self._is_pdp_context_active(): return True - self.logger.info('Setting up PDP context') - self.set('+QICSGP', f'1,1,\"{self._apn}\",\"\",\"\",1') - ok, _ = self.set('+QIACT', '1', timeout=30) - if ok != ModemResult.OK: - self.logger.error('PDP Context setup failed') - raise NetworkError('Failed PDP context setup') - else: - self.logger.info('PDP context active') + return super().connect(timeout) def _tear_down_pdp_context(self): if not self._is_pdp_context_active(): return True diff --git a/Hologram/Network/Modem/E303.py b/Hologram/Network/Modem/E303.py index 2f066e9f..48e7f458 100644 --- a/Hologram/Network/Modem/E303.py +++ b/Hologram/Network/Modem/E303.py @@ -25,15 +25,7 @@ def __init__(self, device_name=None, baud_rate='9600', def connect(self, timeout = DEFAULT_E303_TIMEOUT): return super().connect(timeout) - def init_serial_commands(self): - self.command("E0") #echo off - self.command("+CMEE", "2") #set verbose error codes - self.command("+CPIN?") - self.command("+CTZU", "1") #time/zone sync - self.command("+CTZR", "1") #time/zone URC - #self.command("+CPIN", "") #set SIM PIN - self.command("+CPMS", "\"ME\",\"ME\",\"ME\"") - self.set_sms_configs() + def set_network_registration_status(self): self.command("+CREG", "2") self.command("+CGREG", "2") diff --git a/Hologram/Network/Modem/E372.py b/Hologram/Network/Modem/E372.py index cfed7a31..195df12d 100644 --- a/Hologram/Network/Modem/E372.py +++ b/Hologram/Network/Modem/E372.py @@ -25,15 +25,7 @@ def __init__(self, device_name=None, baud_rate='9600', def connect(self, timeout = DEFAULT_E372_TIMEOUT): return super().connect(timeout) - def init_serial_commands(self): - self.command("E0") #echo off - self.command("+CMEE", "2") #set verbose error codes - self.command("+CPIN?") - self.command("+CTZU", "1") #time/zone sync - self.command("+CTZR", "1") #time/zone URC - #self.command("+CPIN", "") #set SIM PIN - self.command("+CPMS", "\"ME\",\"ME\",\"ME\"") - self.set_sms_configs() + def set_network_registration_status(self): self.command("+CREG", "2") self.command("+CGREG", "2") diff --git a/Hologram/Network/Modem/EC21.py b/Hologram/Network/Modem/EC21.py index 5bb0d8a7..45963636 100644 --- a/Hologram/Network/Modem/EC21.py +++ b/Hologram/Network/Modem/EC21.py @@ -7,176 +7,19 @@ # # LICENSE: Distributed under the terms of the MIT License # -import binascii -import time -from serial.serialutil import Timeout - -from Hologram.Network.Modem import Modem -from Hologram.Event import Event +from Hologram.Network.Modem.Quectel import Quectel from UtilClasses import ModemResult -from Exceptions.HologramError import SerialError, NetworkError DEFAULT_EC21_TIMEOUT = 200 -class EC21(Modem): +class EC21(Quectel): usb_ids = [('2c7c', '0121')] - def __init__(self, device_name=None, baud_rate='9600', - chatscript_file=None, event=Event()): - - super().__init__(device_name=device_name, baud_rate=baud_rate, - chatscript_file=chatscript_file, event=event) - self._at_sockets_available = True - self.urc_response = '' - def connect(self, timeout=DEFAULT_EC21_TIMEOUT): success = super().connect(timeout) return success - def send_message(self, data, timeout=Modem.DEFAULT_SEND_TIMEOUT): - # Waiting for the open socket urc - while self.urc_state != Modem.SOCKET_WRITE_STATE: - self.checkURC() - - self.write_socket(data) - - loop_timeout = Timeout(timeout) - while self.urc_state != Modem.SOCKET_SEND_READ: - self.checkURC() - if self.urc_state != Modem.SOCKET_SEND_READ: - if loop_timeout.expired(): - raise SerialError('Timeout occurred waiting for message status') - time.sleep(self._RETRY_DELAY) - elif self.urc_state == Modem.SOCKET_CLOSED: - return '[1,0]' #this is connection closed for hologram cloud response - - return self.urc_response.rstrip('\r\n') - - def create_socket(self): - self._set_up_pdp_context() - - def connect_socket(self, host, port): - self.command('+QIOPEN', '1,0,\"TCP\",\"%s\",%d,0,1' % (host, port)) - # According to the EC21 Docs - # Have to wait for URC response “+QIOPEN: ,” - - def close_socket(self, socket_identifier=None): - ok, _ = self.command('+QICLOSE', self.socket_identifier) - if ok != ModemResult.OK: - self.logger.error('Failed to close socket') - self.urc_state = Modem.SOCKET_CLOSED - self._tear_down_pdp_context() - - def write_socket(self, data): - hexdata = binascii.hexlify(data) - # We have to do it in chunks of 510 since 512 is actually too long (CMEE error) - # and we need 2n chars for hexified data - for chunk in self._chunks(hexdata, 510): - value = '%d,\"%s\"' % (self.socket_identifier, chunk.decode()) - ok, _ = self.set('+QISENDEX', value, timeout=10) - if ok != ModemResult.OK: - self.logger.error('Failed to write to socket') - raise NetworkError('Failed to write to socket') - - def read_socket(self, socket_identifier=None, payload_length=None): - - if socket_identifier is None: - socket_identifier = self.socket_identifier - - if payload_length is None: - payload_length = self.last_read_payload_length - - ok, resp = self.set('+QIRD', '%d,%d' % (socket_identifier, payload_length)) - if ok == ModemResult.OK: - resp = resp.lstrip('+QIRD: ') - if resp is not None: - resp = resp.strip('"') - try: - resp = resp.decode() - except: - # This is some sort of binary data that can't be decoded so just - # return the bytes. We might want to make this happen via parameter - # in the future so it is more deterministic - self.logger.debug('Could not decode recieved data') - - return resp - - def listen_socket(self, port): - # No equivilent exists for quectel modems - pass - - def is_registered(self): - return self.check_registered('+CREG') or self.check_registered('+CEREG') - - # EFFECTS: Handles URC related AT command responses. - def handleURC(self, urc): - if urc.startswith('+QIOPEN: '): - response_list = urc.lstrip('+QIOPEN: ').split(',') - socket_identifier = int(response_list[0]) - err = int(response_list[-1]) - if err == 0: - self.urc_state = Modem.SOCKET_WRITE_STATE - self.socket_identifier = socket_identifier - else: - self.logger.error('Failed to open socket') - raise NetworkError('Failed to open socket') - return - if urc.startswith('+QIURC: '): - response_list = urc.lstrip('+QIURC: ').split(',') - urctype = response_list[0] - if urctype == '\"recv\"': - self.urc_state = Modem.SOCKET_SEND_READ - self.socket_identifier = int(response_list[1]) - self.last_read_payload_length = int(response_list[2]) - self.urc_response = self._readline_from_serial_port(5) - if urctype == '\"closed\"': - self.urc_state = Modem.SOCKET_CLOSED - self.socket_identifier = int(response_list[-1]) - return - super().handleURC(urc) - - def _is_pdp_context_active(self): - if not self.is_registered(): - return False - - ok, r = self.command('+QIACT?') - if ok == ModemResult.OK: - try: - pdpstatus = int(r.lstrip('+QIACT: ').split(',')[1]) - # 1: PDP active - return pdpstatus == 1 - except (IndexError, ValueError) as e: - self.logger.error(repr(e)) - except AttributeError as e: - self.logger.error(repr(e)) - return False - - def init_serial_commands(self): - self.command("E0") #echo off - self.command("+CMEE", "2") #set verbose error codes - self.command("+CPIN?") - self.set_timezone_configs() - #self.command("+CPIN", "") #set SIM PIN - self.command("+CPMS", "\"ME\",\"ME\",\"ME\"") - self.set_sms_configs() - self.set_network_registration_status() - - def set_network_registration_status(self): - self.command("+CREG", "2") - self.command("+CEREG", "2") - - def _set_up_pdp_context(self): - if self._is_pdp_context_active(): return True - self.logger.info('Setting up PDP context') - self.set('+QICSGP', f'1,1,\"{self._apn}\",\"\",\"\",1') - ok, _ = self.set('+QIACT', '1', timeout=30) - if ok != ModemResult.OK: - self.logger.error('PDP Context setup failed') - raise NetworkError('Failed PDP context setup') - else: - self.logger.info('PDP context active') - def _tear_down_pdp_context(self): if not self._is_pdp_context_active(): return True self.logger.info('Tearing down PDP context') diff --git a/Hologram/Network/Modem/MS2131.py b/Hologram/Network/Modem/MS2131.py index ecc80e28..a738f314 100644 --- a/Hologram/Network/Modem/MS2131.py +++ b/Hologram/Network/Modem/MS2131.py @@ -26,15 +26,7 @@ def __init__(self, device_name=None, baud_rate='9600', def connect(self, timeout = DEFAULT_MS2131_TIMEOUT): return super().connect(timeout) - def init_serial_commands(self): - self.command("E0") #echo off - self.command("+CMEE", "2") #set verbose error codes - self.command("+CPIN?") - self.command("+CTZU", "1") #time/zone sync - self.command("+CTZR", "1") #time/zone URC - #self.command("+CPIN", "") #set SIM PIN - self.command("+CPMS", "\"ME\",\"ME\",\"ME\"") - self.set_sms_configs() + def set_network_registration_status(self): self.command("+CREG", "2") self.command("+CGREG", "2") diff --git a/Hologram/Network/Modem/Modem.py b/Hologram/Network/Modem/Modem.py index c55468cc..ca5222d4 100644 --- a/Hologram/Network/Modem/Modem.py +++ b/Hologram/Network/Modem/Modem.py @@ -226,7 +226,13 @@ def initialize_serial_interface(self): self.init_serial_commands() def init_serial_commands(self): - pass + self.command("E0") #echo off + self.command("+CMEE", "2") #set verbose error codes + self.command("+CPIN?") + self.set_timezone_configs() + self.command("+CPMS", "\"ME\",\"ME\",\"ME\"") + self.set_sms_configs() + self.set_network_registration_status() def set_sms_configs(self): self.command("+CMGF", "0") #SMS PDU format diff --git a/Hologram/Network/Modem/NovaM.py b/Hologram/Network/Modem/NovaM.py index 9fbb95ec..65e6750d 100644 --- a/Hologram/Network/Modem/NovaM.py +++ b/Hologram/Network/Modem/NovaM.py @@ -33,15 +33,6 @@ def __init__(self, device_name=None, baud_rate='9600', else: self.is_r410 = True - - def init_serial_commands(self): - self.command("E0") #echo off - self.command("+CMEE", "2") #set verbose error codes - self.command("+CPIN?") - self.command("+CPMS", "\"ME\",\"ME\",\"ME\"") - self.set_sms_configs() - self.set_network_registration_status() - def set_network_registration_status(self): self.command("+CEREG", "2") diff --git a/Hologram/Network/Modem/Nova_U201.py b/Hologram/Network/Modem/Nova_U201.py index b3e3ddc3..d6a23d3c 100644 --- a/Hologram/Network/Modem/Nova_U201.py +++ b/Hologram/Network/Modem/Nova_U201.py @@ -71,16 +71,6 @@ def enforce_nova_modem_mode(self): self.device_name = devices[0] super().initialize_serial_interface() - def init_serial_commands(self): - self.command("E0") #echo off - self.command("+CMEE", "2") #set verbose error codes - self.command("+CPIN?") - self.set_timezone_configs() - #self.command("+CPIN", "") #set SIM PIN - self.command("+CPMS", "\"ME\",\"ME\",\"ME\"") - self.set_sms_configs() - self.set_network_registration_status() - def set_network_registration_status(self): self.command("+CREG", "2") self.command("+CGREG", "2") diff --git a/Hologram/Network/Modem/Quectel.py b/Hologram/Network/Modem/Quectel.py new file mode 100644 index 00000000..d996dd03 --- /dev/null +++ b/Hologram/Network/Modem/Quectel.py @@ -0,0 +1,161 @@ +# Quectel.py - Hologram Python SDK Quectel modem interface +# +# Author: Hologram +# +# Copyright 2016 - Hologram (Konekt, Inc.) +# +# +# LICENSE: Distributed under the terms of the MIT License +# +import time +import binascii + +from serial.serialutil import Timeout + +from Hologram.Network.Modem import Modem +from Hologram.Event import Event +from UtilClasses import ModemResult +from Exceptions.HologramError import SerialError, NetworkError + +class Quectel(Modem): + + def __init__(self, device_name=None, baud_rate='9600', + chatscript_file=None, event=Event()): + + super().__init__(device_name=device_name, baud_rate=baud_rate, + chatscript_file=chatscript_file, event=event) + self._at_sockets_available = True + self.urc_response = '' + + def send_message(self, data, timeout=Modem.DEFAULT_SEND_TIMEOUT): + # Waiting for the open socket urc + while self.urc_state != Modem.SOCKET_WRITE_STATE: + self.checkURC() + + self.write_socket(data) + + loop_timeout = Timeout(timeout) + while self.urc_state != Modem.SOCKET_SEND_READ: + self.checkURC() + if self.urc_state != Modem.SOCKET_SEND_READ: + if loop_timeout.expired(): + raise SerialError('Timeout occurred waiting for message status') + time.sleep(self._RETRY_DELAY) + elif self.urc_state == Modem.SOCKET_CLOSED: + return '[1,0]' #this is connection closed for hologram cloud response + + return self.urc_response.rstrip('\r\n') + + def create_socket(self): + self._set_up_pdp_context() + + def connect_socket(self, host, port): + self.command('+QIOPEN', '1,0,\"TCP\",\"%s\",%d,0,1' % (host, port)) + # According to the Quectel Docs + # Have to wait for URC response “+QIOPEN: ,” + + def close_socket(self, socket_identifier=None): + ok, _ = self.command('+QICLOSE', self.socket_identifier) + if ok != ModemResult.OK: + self.logger.error('Failed to close socket') + self.urc_state = Modem.SOCKET_CLOSED + self._tear_down_pdp_context() + + def write_socket(self, data): + hexdata = binascii.hexlify(data) + # We have to do it in chunks of 510 since 512 is actually too long (CMEE error) + # and we need 2n chars for hexified data + for chunk in self._chunks(hexdata, 510): + value = '%d,\"%s\"' % (self.socket_identifier, chunk.decode()) + ok, _ = self.set('+QISENDEX', value, timeout=10) + if ok != ModemResult.OK: + self.logger.error('Failed to write to socket') + raise NetworkError('Failed to write to socket') + + def read_socket(self, socket_identifier=None, payload_length=None): + + if socket_identifier is None: + socket_identifier = self.socket_identifier + + if payload_length is None: + payload_length = self.last_read_payload_length + + ok, resp = self.set('+QIRD', '%d,%d' % (socket_identifier, payload_length)) + if ok == ModemResult.OK: + resp = resp.lstrip('+QIRD: ') + if resp is not None: + resp = resp.strip('"') + try: + resp = resp.decode() + except: + # This is some sort of binary data that can't be decoded so just + # return the bytes. We might want to make this happen via parameter + # in the future so it is more deterministic + self.logger.debug('Could not decode recieved data') + + return resp + + def listen_socket(self, port): + # No equivilent exists for quectel modems + pass + + def is_registered(self): + return self.check_registered('+CREG') or self.check_registered('+CEREG') + + # EFFECTS: Handles URC related AT command responses. + def handleURC(self, urc): + if urc.startswith('+QIOPEN: '): + response_list = urc.lstrip('+QIOPEN: ').split(',') + socket_identifier = int(response_list[0]) + err = int(response_list[-1]) + if err == 0: + self.urc_state = Modem.SOCKET_WRITE_STATE + self.socket_identifier = socket_identifier + else: + self.logger.error('Failed to open socket') + raise NetworkError('Failed to open socket') + return + if urc.startswith('+QIURC: '): + response_list = urc.lstrip('+QIURC: ').split(',') + urctype = response_list[0] + if urctype == '\"recv\"': + self.urc_state = Modem.SOCKET_SEND_READ + self.socket_identifier = int(response_list[1]) + self.last_read_payload_length = int(response_list[2]) + self.urc_response = self._readline_from_serial_port(5) + if urctype == '\"closed\"': + self.urc_state = Modem.SOCKET_CLOSED + self.socket_identifier = int(response_list[-1]) + return + super().handleURC(urc) + + def _is_pdp_context_active(self): + if not self.is_registered(): + return False + + ok, r = self.command('+QIACT?') + if ok == ModemResult.OK: + try: + pdpstatus = int(r.lstrip('+QIACT: ').split(',')[1]) + # 1: PDP active + return pdpstatus == 1 + except (IndexError, ValueError) as e: + self.logger.error(repr(e)) + except AttributeError as e: + self.logger.error(repr(e)) + return False + + def set_network_registration_status(self): + self.command("+CREG", "2") + self.command("+CEREG", "2") + + def _set_up_pdp_context(self): + if self._is_pdp_context_active(): return True + self.logger.info('Setting up PDP context') + self.set('+QICSGP', f'1,1,\"{self._apn}\",\"\",\"\",1') + ok, _ = self.set('+QIACT', '1', timeout=30) + if ok != ModemResult.OK: + self.logger.error('PDP Context setup failed') + raise NetworkError('Failed PDP context setup') + else: + self.logger.info('PDP context active') \ No newline at end of file diff --git a/Hologram/Network/Modem/__init__.py b/Hologram/Network/Modem/__init__.py index 84491b79..1e795a17 100644 --- a/Hologram/Network/Modem/__init__.py +++ b/Hologram/Network/Modem/__init__.py @@ -1,4 +1,4 @@ -__all__ = ['Modem', 'MockModem', 'MS2131', 'Nova', 'E303', 'E372', 'EC21', 'BG96'] +__all__ = ['Modem', 'MockModem', 'MS2131', 'Nova', 'E303', 'E372', 'Quectel'] from .IModem import IModem from .Modem import Modem diff --git a/tests/API/test_API.py b/tests/API/test_API.py new file mode 100644 index 00000000..6d7c9896 --- /dev/null +++ b/tests/API/test_API.py @@ -0,0 +1,82 @@ +import sys +import pytest +from unittest.mock import Mock, patch +sys.path.append(".") +sys.path.append("..") +sys.path.append("../..") +from Hologram.Api import Api +from Exceptions.HologramError import ApiError + +class TestHologramAPI: + + def test_create_no_creds(self): + with pytest.raises(ApiError, match = 'Must specify valid HTTP authentication credentials'): + Api() + + def test_create_missing_password(self): + with pytest.raises(ApiError, match = 'Must specify valid HTTP authentication credentials'): + Api(username='user') + + def test_create_missing_username(self): + with pytest.raises(ApiError, match = 'Must specify valid HTTP authentication credentials'): + Api(password='password') + + @patch('requests.post') + def test_activate(self, r_post): + api = Api(apikey='123apikey') + + r_post.return_value = Mock(status_code=200) + r_post.return_value.json = Mock(return_value={"success": True, 'order_data': {}}) + + success, response = api.activateSIM('iccid') + + assert success == True + assert response == {} + + @patch('requests.post') + def test_activate_failed(self, r_post): + api = Api(apikey='123apikey') + + r_post.return_value = Mock(status_code=200) + r_post.return_value.json = Mock(return_value={"success": False, 'data': {'iccid': 'Activation failed'}}) + + success, response = api.activateSIM('iccid') + + assert success == False + assert response == 'Activation failed' + + @patch('requests.post') + def test_activate_bad_status_code(self, r_post): + api = Api(apikey='123apikey') + + r_post.return_value = Mock( + status_code=429, + text = 'Too many requests') + + success, response = api.activateSIM('iccid') + + assert success == False + assert response == 'Too many requests' + + @patch('requests.get') + def test_get_plans(self, r_post): + api = Api(apikey='123apikey') + + r_post.return_value.json = Mock(return_value={"success": True, 'data': {'id': 1, 'orgid': 1}}) + + success, response = api.getPlans() + + assert success == True + assert response == {'id': 1, 'orgid': 1} + + @patch('requests.get') + def test_get_sim_state(self, r_post): + api = Api(apikey='123apikey') + + r_post.return_value.json = Mock(return_value={"success": True, 'data': [{'state': 'LIVE'}]}) + + success, response = api.getSIMState('iccid') + + assert success == True + assert response == 'LIVE' + diff --git a/tests/MessageMode/test_HologramCloud.py b/tests/MessageMode/test_HologramCloud.py index e2d328d8..89345c05 100644 --- a/tests/MessageMode/test_HologramCloud.py +++ b/tests/MessageMode/test_HologramCloud.py @@ -13,19 +13,24 @@ sys.path.append("../..") from Hologram.Authentication import * from Hologram.HologramCloud import HologramCloud +from Exceptions.HologramError import AuthenticationError credentials = {'devicekey':'12345678'} class TestHologramCloud: def test_create(self): - hologram = HologramCloud(credentials, enable_inbound = False) + hologram = HologramCloud(credentials, authentication_type='csrpsk', enable_inbound = False) assert hologram.send_host == 'cloudsocket.hologram.io' assert hologram.send_port == 9999 assert hologram.receive_host == '0.0.0.0' assert hologram.receive_port == 4010 + def test_create_bad_totp_keys(self): + with pytest.raises(AuthenticationError, match = 'Unable to fetch device id or private key for TOTP authenication'): + HologramCloud(credentials, enable_inbound = False) + def test_invalid_sms_length(self): hologram = HologramCloud(credentials, authentication_type='csrpsk', enable_inbound = False) @@ -36,7 +41,7 @@ def test_invalid_sms_length(self): def test_get_result_string(self): - hologram = HologramCloud(credentials, enable_inbound = False) + hologram = HologramCloud(credentials, authentication_type='csrpsk', enable_inbound = False) assert hologram.getResultString(-1) == 'Unknown error' assert hologram.getResultString(0) == 'Message sent successfully' diff --git a/tests/Modem/test_BG96.py b/tests/Modem/test_BG96.py new file mode 100644 index 00000000..681d1e09 --- /dev/null +++ b/tests/Modem/test_BG96.py @@ -0,0 +1,74 @@ +# Author: Hologram +# +# Copyright 2017 - Hologram (Konekt, Inc.) +# +# LICENSE: Distributed under the terms of the MIT License +# +# test_BG96.py - This file implements unit tests for the BG96 modem interface. + +from unittest.mock import patch +import pytest +import sys + +from Hologram.Network.Modem.BG96 import BG96 +from UtilClasses import ModemResult + +sys.path.append(".") +sys.path.append("..") +sys.path.append("../..") + + +def mock_write(modem, message): + return True + + +def mock_read(modem): + return True + + +def mock_readline(modem, timeout=None, hide=False): + return "" + + +def mock_open_serial_port(modem, device_name=None): + return True + + +def mock_close_serial_port(modem): + return True + + +def mock_detect_usable_serial_port(modem, stop_on_first=True): + return "/dev/ttyUSB0" + + +@pytest.fixture +def no_serial_port(monkeypatch): + monkeypatch.setattr(BG96, "_read_from_serial_port", mock_read) + monkeypatch.setattr(BG96, "_readline_from_serial_port", mock_readline) + monkeypatch.setattr(BG96, "_write_to_serial_port_and_flush", mock_write) + monkeypatch.setattr(BG96, "openSerialPort", mock_open_serial_port) + monkeypatch.setattr(BG96, "closeSerialPort", mock_close_serial_port) + monkeypatch.setattr(BG96, "detect_usable_serial_port", mock_detect_usable_serial_port) + + +def test_init_BG96_no_args(no_serial_port): + modem = BG96() + assert modem.timeout == 1 + assert modem.socket_identifier == 0 + assert modem.chatscript_file.endswith("/chatscripts/default-script") + assert modem._at_sockets_available + + +@patch.object(BG96, "set") +@patch.object(BG96, "command") +@patch.object(BG96, "_is_pdp_context_active") +def test_close_socket(mock_pdp, mock_command, mock_set, no_serial_port): + modem = BG96() + modem.socket_identifier = 1 + mock_set.return_value = (ModemResult.OK, None) + mock_command.return_value = (ModemResult.OK, None) + mock_pdp.return_value = True + modem.close_socket() + mock_set.assert_called_with("+QIACT", "0", timeout=30) + mock_command.assert_called_with("+QICLOSE", 1) diff --git a/tests/Modem/test_EC21.py b/tests/Modem/test_EC21.py new file mode 100644 index 00000000..b36e24e0 --- /dev/null +++ b/tests/Modem/test_EC21.py @@ -0,0 +1,74 @@ +# Author: Hologram +# +# Copyright 2017 - Hologram (Konekt, Inc.) +# +# LICENSE: Distributed under the terms of the MIT License +# +# test_EC21.py - This file implements unit tests for the EC21 modem interface. + +from unittest.mock import patch +import pytest +import sys + +from Hologram.Network.Modem.EC21 import EC21 +from UtilClasses import ModemResult + +sys.path.append(".") +sys.path.append("..") +sys.path.append("../..") + + +def mock_write(modem, message): + return True + + +def mock_read(modem): + return True + + +def mock_readline(modem, timeout=None, hide=False): + return "" + + +def mock_open_serial_port(modem, device_name=None): + return True + + +def mock_close_serial_port(modem): + return True + + +def mock_detect_usable_serial_port(modem, stop_on_first=True): + return "/dev/ttyUSB0" + + +@pytest.fixture +def no_serial_port(monkeypatch): + monkeypatch.setattr(EC21, "_read_from_serial_port", mock_read) + monkeypatch.setattr(EC21, "_readline_from_serial_port", mock_readline) + monkeypatch.setattr(EC21, "_write_to_serial_port_and_flush", mock_write) + monkeypatch.setattr(EC21, "openSerialPort", mock_open_serial_port) + monkeypatch.setattr(EC21, "closeSerialPort", mock_close_serial_port) + monkeypatch.setattr(EC21, "detect_usable_serial_port", mock_detect_usable_serial_port) + + +def test_init_EC21_no_args(no_serial_port): + modem = EC21() + assert modem.timeout == 1 + assert modem.socket_identifier == 0 + assert modem.chatscript_file.endswith("/chatscripts/default-script") + assert modem._at_sockets_available + + +@patch.object(EC21, "set") +@patch.object(EC21, "command") +@patch.object(EC21, "_is_pdp_context_active") +def test_close_socket(mock_pdp, mock_command, mock_set, no_serial_port): + modem = EC21() + modem.socket_identifier = 1 + mock_set.return_value = (ModemResult.OK, None) + mock_command.return_value = (ModemResult.OK, None) + mock_pdp.return_value = True + modem.close_socket() + mock_set.assert_called_with("+QIDEACT", "1", timeout=30) + mock_command.assert_called_with("+QICLOSE", 1) diff --git a/tests/Modem/test_Modem.py b/tests/Modem/test_Modem.py index cbf71f40..e290d148 100644 --- a/tests/Modem/test_Modem.py +++ b/tests/Modem/test_Modem.py @@ -23,6 +23,9 @@ def mock_write(modem, message): def mock_read(modem): return True +def mock_init_commands(modem): + return True + def mock_readline(modem, timeout=None, hide=False): return '' @@ -49,6 +52,7 @@ def no_serial_port(monkeypatch): monkeypatch.setattr(Modem, '_read_from_serial_port', mock_read) monkeypatch.setattr(Modem, '_readline_from_serial_port', mock_readline) monkeypatch.setattr(Modem, '_write_to_serial_port_and_flush', mock_write) + monkeypatch.setattr(Modem, 'init_serial_commands', mock_init_commands) monkeypatch.setattr(Modem, 'openSerialPort', mock_open_serial_port) monkeypatch.setattr(Modem, 'closeSerialPort', mock_close_serial_port) monkeypatch.setattr(Modem, 'detect_usable_serial_port', mock_detect_usable_serial_port) diff --git a/tests/Modem/test_Quectel.py b/tests/Modem/test_Quectel.py new file mode 100644 index 00000000..d7e956dd --- /dev/null +++ b/tests/Modem/test_Quectel.py @@ -0,0 +1,150 @@ +# Author: Hologram +# +# Copyright 2017 - Hologram (Konekt, Inc.) +# +# LICENSE: Distributed under the terms of the MIT License +# +# test_Quectel.py - This file implements unit tests for the Quectel modem interface. + +from unittest.mock import patch, call +import pytest +import sys + +from Hologram.Network.Modem.Quectel import Quectel +from Hologram.Network.Modem.Modem import Modem +from UtilClasses import ModemResult + +sys.path.append(".") +sys.path.append("..") +sys.path.append("../..") + + +def mock_write(modem, message): + return True + + +def mock_read(modem): + return True + + +def mock_readline(modem, timeout=None, hide=False): + return "" + + +def mock_open_serial_port(modem, device_name=None): + return True + + +def mock_close_serial_port(modem): + return True + + +def mock_detect_usable_serial_port(modem, stop_on_first=True): + return "/dev/ttyUSB0" + + +@pytest.fixture +def no_serial_port(monkeypatch): + monkeypatch.setattr(Quectel, "_read_from_serial_port", mock_read) + monkeypatch.setattr(Quectel, "_readline_from_serial_port", mock_readline) + monkeypatch.setattr(Quectel, "_write_to_serial_port_and_flush", mock_write) + monkeypatch.setattr(Quectel, "openSerialPort", mock_open_serial_port) + monkeypatch.setattr(Quectel, "closeSerialPort", mock_close_serial_port) + monkeypatch.setattr(Quectel, "detect_usable_serial_port", mock_detect_usable_serial_port) + + +def test_init_Quectel_no_args(no_serial_port): + modem = Quectel() + assert modem.timeout == 1 + assert modem.socket_identifier == 0 + assert modem.chatscript_file.endswith("/chatscripts/default-script") + assert modem._at_sockets_available + +@patch.object(Quectel, "check_registered") +@patch.object(Quectel, "set") +@patch.object(Quectel, "command") +def test_create_socket(mock_command, mock_set, mock_check, no_serial_port): + modem = Quectel() + modem.apn = 'test' + mock_check.return_value = True + # The PDP context is not active + mock_command.return_value = (ModemResult.OK, '+QIACT: 0,0') + mock_set.return_value = (ModemResult.OK, None) + modem.create_socket() + mock_command.assert_called_with("+QIACT?") + mock_set.assert_has_calls( + [ + call("+QICSGP", '1,1,\"test\",\"\",\"\",1'), + call("+QIACT", '1', timeout=30) + ], + any_order=True + ) + +@patch.object(Quectel, "command") +def test_connect_socket(mock_command, no_serial_port): + modem = Quectel() + modem.socket_identifier = 1 + host = "hologram.io" + port = 9999 + modem.connect_socket(host, port) + mock_command.assert_called_with("+QIOPEN", '1,0,"TCP","%s",%d,0,1' % (host, port)) + + +@patch.object(Quectel, "set") +def test_write_socket_small(mock_command, no_serial_port): + modem = Quectel() + modem.socket_identifier = 1 + data = b"Message smaller than 510 bytes" + mock_command.return_value = (ModemResult.OK, None) + modem.write_socket(data) + mock_command.assert_called_with( + "+QISENDEX", + '1,"4d65737361676520736d616c6c6572207468616e20353130206279746573"', + timeout=10, + ) + + +@patch.object(Quectel, "set") +def test_write_socket_large(mock_command, no_serial_port): + modem = Quectel() + modem.socket_identifier = 1 + data = b"a" * 300 + mock_command.return_value = (ModemResult.OK, None) + modem.write_socket(data) + mock_command.assert_has_calls( + [ + call("+QISENDEX", '1,"%s"' % ("61" * 255), timeout=10), + call("+QISENDEX", '1,"%s"' % ("61" * 45), timeout=10), + ], + any_order=True, + ) + +@patch.object(Quectel, "set") +def test_read_socket(mock_command, no_serial_port): + modem = Quectel() + modem.socket_identifier = 1 + mock_command.return_value = (ModemResult.OK, '+QIRD: "Some val"') + # Double quotes should be stripped from the reutrn value + assert (modem.read_socket(payload_length=10) == 'Some val') + mock_command.assert_called_with("+QIRD", '1,10') + +def test_handle_open_urc(no_serial_port): + modem = Quectel() + modem.handleURC('+QIOPEN: 1,0') + assert modem.urc_state == Modem.SOCKET_WRITE_STATE + assert modem.socket_identifier == 1 + +def test_handle_received_data_urc(no_serial_port): + modem = Quectel() + modem.handleURC('+QIURC: \"recv\",1,25') + assert modem.urc_state == Modem.SOCKET_SEND_READ + assert modem.socket_identifier == 1 + assert modem.last_read_payload_length == 25 + assert modem.urc_response == "" + +def test_handle_socket_closed_urc(no_serial_port): + modem = Quectel() + modem.handleURC('+QIURC: \"closed\",1') + assert modem.urc_state == Modem.SOCKET_CLOSED + assert modem.socket_identifier == 1 + From 053f31940471caa03c9edd6d14afa45ba7b3df13 Mon Sep 17 00:00:00 2001 From: Parker LeBlanc Date: Tue, 29 Aug 2023 09:40:47 -0700 Subject: [PATCH 56/76] Add support for providing a specific modem name --- Hologram/Cloud.py | 8 ++++---- Hologram/CustomCloud.py | 5 +++-- Hologram/HologramCloud.py | 6 ++++-- Hologram/Network/Cellular.py | 33 ++++++++++++++++-------------- Hologram/Network/NetworkManager.py | 7 +++---- requirements.txt | 2 +- version.txt | 2 +- 7 files changed, 34 insertions(+), 29 deletions(-) diff --git a/Hologram/Cloud.py b/Hologram/Cloud.py index 7fe82fcc..3fe89c1c 100644 --- a/Hologram/Cloud.py +++ b/Hologram/Cloud.py @@ -21,7 +21,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 = None): # Logging setup. self.logger = logging.getLogger(__name__) @@ -33,7 +33,7 @@ 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 @@ -41,13 +41,13 @@ def __initialize_host_and_port(self, send_host, send_port, receive_host, receive 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) diff --git a/Hologram/CustomCloud.py b/Hologram/CustomCloud.py index 9cd3e269..3127c155 100644 --- a/Hologram/CustomCloud.py +++ b/Hologram/CustomCloud.py @@ -25,14 +25,15 @@ class CustomCloud(Cloud): def __init__(self, credentials, send_host='', send_port=0, receive_host='', receive_port=0, enable_inbound=False, - network=''): + network='', 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): diff --git a/Hologram/HologramCloud.py b/Hologram/HologramCloud.py index 6489e960..ac2a1b46 100755 --- a/Hologram/HologramCloud.py +++ b/Hologram/HologramCloud.py @@ -60,14 +60,16 @@ class HologramCloud(CustomCloud): } def __init__(self, credentials, enable_inbound=False, network='', - authentication_type='totp'): + authentication_type='totp', 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) diff --git a/Hologram/Network/Cellular.py b/Hologram/Network/Cellular.py index dc4dbc1f..0fd6ad51 100644 --- a/Hologram/Network/Cellular.py +++ b/Hologram/Network/Cellular.py @@ -48,10 +48,12 @@ 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: + dev_devices = Cellular.scan_for_modems() + if len(dev_devices) == 0: raise NetworkError('Modem not detected') - self.modem = dev_devices[0] + first_modem = dev_devices[0] + modem_name = first_modem[0] + self.modem = modem_name def load_modem_drivers(self): self._load_modem_drivers() @@ -208,26 +210,27 @@ 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 + @staticmethod + def scan_for_modems() -> list[Modem]: + res = [] + for (modemName, modemHandler) in Cellular._modemHandlers.items(): + modems = Cellular._scan_for_modem(modemHandler) + if len(modems) > 0: + modem = (modemName, modemHandler, modems) + res.append(modem) return res - def _scan_for_modem(self, modemHandler): + @staticmethod + def _scan_for_modem(modemHandler): usb_ids = modemHandler.usb_ids + devices = [] 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__) - return True - return False + devices.append(dev) + return devices diff --git a/Hologram/Network/NetworkManager.py b/Hologram/Network/NetworkManager.py index e0866471..12766d14 100644 --- a/Hologram/Network/NetworkManager.py +++ b/Hologram/Network/NetworkManager.py @@ -26,7 +26,7 @@ class NetworkManager: 'ethernet' : Ethernet.Ethernet, } - def __init__(self, event, network): + def __init__(self, event, network_name, modem=None): # Logging setup. self.logger = logging.getLogger(__name__) @@ -34,7 +34,7 @@ def __init__(self, event, network): 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): @@ -50,8 +50,7 @@ def listAvailableInterfaces(self): def network(self): return self._network - @network.setter - def network(self, network, modem=None): + def init_network(self, network, modem=None): if not network: # non-network mode self.networkConnected() self._network = None diff --git a/requirements.txt b/requirements.txt index f0c35e59..23e91bf1 100644 --- a/requirements.txt +++ b/requirements.txt @@ -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 diff --git a/version.txt b/version.txt index f374f666..2003b639 100644 --- a/version.txt +++ b/version.txt @@ -1 +1 @@ -0.9.1 +0.9.2 From 6a1fcc994d59a9ffe57c08f5f5f3c8241f31275b Mon Sep 17 00:00:00 2001 From: "parker.leblanc" Date: Tue, 29 Aug 2023 16:57:22 -0700 Subject: [PATCH 57/76] Allow passing of modem from hologram cloud --- Hologram/Cloud.py | 4 +++- Hologram/CustomCloud.py | 4 +++- Hologram/HologramCloud.py | 4 +++- 3 files changed, 9 insertions(+), 3 deletions(-) diff --git a/Hologram/Cloud.py b/Hologram/Cloud.py index 3fe89c1c..7ee80e11 100644 --- a/Hologram/Cloud.py +++ b/Hologram/Cloud.py @@ -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 * @@ -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 = '', modem = None): + receive_host = '', receive_port = 0, network = '', modem: Union[None, Modem] = None): # Logging setup. self.logger = logging.getLogger(__name__) diff --git a/Hologram/CustomCloud.py b/Hologram/CustomCloud.py index 3127c155..a3b7fb9b 100644 --- a/Hologram/CustomCloud.py +++ b/Hologram/CustomCloud.py @@ -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 @@ -25,7 +27,7 @@ class CustomCloud(Cloud): def __init__(self, credentials, send_host='', send_port=0, receive_host='', receive_port=0, enable_inbound=False, - network='', modem=None): + network='', modem: Union[None, Modem] = None): super().__init__(credentials, send_host=send_host, diff --git a/Hologram/HologramCloud.py b/Hologram/HologramCloud.py index ac2a1b46..b8f9f0cf 100755 --- a/Hologram/HologramCloud.py +++ b/Hologram/HologramCloud.py @@ -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 @@ -60,7 +62,7 @@ class HologramCloud(CustomCloud): } def __init__(self, credentials, enable_inbound=False, network='', - authentication_type='totp', modem=None): + authentication_type='totp', modem: Union[None, Modem] = None): super().__init__(credentials, send_host=HOLOGRAM_HOST_SEND, send_port=HOLOGRAM_PORT_SEND, From 964ea053a786d37d1a1e8bd17cf226097f7c08ce Mon Sep 17 00:00:00 2001 From: "parker.leblanc" Date: Tue, 29 Aug 2023 17:00:08 -0700 Subject: [PATCH 58/76] Add in the ability to pass modem to network manager --- Hologram/Network/NetworkManager.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/Hologram/Network/NetworkManager.py b/Hologram/Network/NetworkManager.py index 12766d14..18f79818 100644 --- a/Hologram/Network/NetworkManager.py +++ b/Hologram/Network/NetworkManager.py @@ -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 @@ -26,7 +28,7 @@ class NetworkManager: 'ethernet' : Ethernet.Ethernet, } - def __init__(self, event, network_name, modem=None): + def __init__(self, event, network_name, modem: Union[None, Modem] = None): # Logging setup. self.logger = logging.getLogger(__name__) @@ -50,7 +52,7 @@ def listAvailableInterfaces(self): def network(self): return self._network - def init_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 From c53ed0030f665611ac57b89b7f5b8bb4c0f08d22 Mon Sep 17 00:00:00 2001 From: "parker.leblanc" Date: Wed, 30 Aug 2023 15:14:18 -0700 Subject: [PATCH 59/76] Allow users to scan for usable modems and set specific modems --- Hologram/Network/Cellular.py | 37 +++++++++++++++++++++------------ Hologram/Network/Modem/Modem.py | 36 +++++++++++++++++++------------- 2 files changed, 46 insertions(+), 27 deletions(-) diff --git a/Hologram/Network/Cellular.py b/Hologram/Network/Cellular.py index 0fd6ad51..ce4b78eb 100644 --- a/Hologram/Network/Cellular.py +++ b/Hologram/Network/Cellular.py @@ -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. @@ -53,7 +54,7 @@ def autodetect_modem(self): raise NetworkError('Modem not detected') first_modem = dev_devices[0] modem_name = first_modem[0] - self.modem = modem_name + self.modem = self._modemHandlers[modem_name](event=self.event) def load_modem_drivers(self): self._load_modem_drivers() @@ -214,9 +215,9 @@ def _load_modem_drivers(self): def scan_for_modems() -> list[Modem]: res = [] for (modemName, modemHandler) in Cellular._modemHandlers.items(): - modems = Cellular._scan_for_modem(modemHandler) - if len(modems) > 0: - modem = (modemName, modemHandler, modems) + modem_exists = Cellular._scan_for_modem(modemHandler) + if modem_exists: + modem = (modemName, modemHandler) res.append(modem) return res @@ -224,14 +225,27 @@ def scan_for_modems() -> list[Modem]: @staticmethod def _scan_for_modem(modemHandler): usb_ids = modemHandler.usb_ids - devices = [] for vid_pid in usb_ids: if not vid_pid: continue - for dev in list_ports.grep("{0}:{1}".format(vid_pid[0], vid_pid[1])): - devices.append(dev) - return devices + 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 (modemName, modemHandler) in Cellular._modemHandlers.items(): + modem_exists = Cellular._scan_for_modem(modemHandler) + if modem_exists: + test_handler = modemHandler() + # test_handler.closeSerialPort() + usable_ports = test_handler.detect_usable_serial_port(stop_on_first=False) + for port in usable_ports: + modem = modemHandler(device_name=port) + # modem.closeSerialPort() + modems.append(modem) + return modems @@ -240,11 +254,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): diff --git a/Hologram/Network/Modem/Modem.py b/Hologram/Network/Modem/Modem.py index ca5222d4..771401e5 100644 --- a/Hologram/Network/Modem/Modem.py +++ b/Hologram/Network/Modem/Modem.py @@ -57,6 +57,7 @@ class Modem(IModem): 0x2F: u'\\', } + # We use device_name instead of serial port, if device name is set we skip searching for a serial port def __init__(self, device_name=None, baud_rate='9600', chatscript_file=None, event=Event()): @@ -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 @@ -844,6 +848,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.__class__.__name__}, port: {self.device_name}" @property def serial_port(self): From ff6fa662266e9fc339e3d88ac2a97da62edd3d3d Mon Sep 17 00:00:00 2001 From: "parker.leblanc" Date: Wed, 30 Aug 2023 16:31:10 -0700 Subject: [PATCH 60/76] Change python sdk auth to match pattern --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 23e91bf1..b773c1f1 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,7 +1,7 @@ pyroute2==0.5.* pyserial~=3.5 python-pppd==1.0.4 -python_sdk_auth==0.4.0 +python-sdk-auth==0.4.0 pyudev~=0.22.0 pyusb~=1.2.1 psutil~=5.8.0 From 5e2aecd3925ba8dab536309c8cfbe1be152333c5 Mon Sep 17 00:00:00 2001 From: "parker.leblanc" Date: Thu, 31 Aug 2023 11:41:16 -0700 Subject: [PATCH 61/76] Add send sms functionality --- Hologram/Network/Modem/Modem.py | 27 +++++++++++++++++++++++---- 1 file changed, 23 insertions(+), 4 deletions(-) diff --git a/Hologram/Network/Modem/Modem.py b/Hologram/Network/Modem/Modem.py index 771401e5..746349f9 100644 --- a/Hologram/Network/Modem/Modem.py +++ b/Hologram/Network/Modem/Modem.py @@ -275,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=10 + ) + + self.command("+CMGF", "0") + return ok == ModemResult.OK + + def pop_received_message(self): self.checkURC() data = None @@ -501,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('?'): @@ -532,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: @@ -789,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 @@ -851,7 +870,7 @@ def __set_hex_mode(self, enable_hex_mode): @property def details(self): - return f"{self.__class__.__name__}, port: {self.device_name}" + return f"{self.description} at port: {self.device_name}" @property def serial_port(self): From 99e73a30eb6755a6cc6fda371739b539b3b8dcc4 Mon Sep 17 00:00:00 2001 From: "parker.leblanc" Date: Thu, 31 Aug 2023 11:45:04 -0700 Subject: [PATCH 62/76] Use the timeout provided in the function definition --- Hologram/Network/Modem/Modem.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Hologram/Network/Modem/Modem.py b/Hologram/Network/Modem/Modem.py index 746349f9..b9cdb4ed 100644 --- a/Hologram/Network/Modem/Modem.py +++ b/Hologram/Network/Modem/Modem.py @@ -285,7 +285,7 @@ def send_sms_message(self, phonenumber, message, timeout=DEFAULT_SEND_TIMEOUT): prompt=b">", data=f"{message}\r", commit_cmd=ctrl_z, - timeout=10 + timeout=timeout ) self.command("+CMGF", "0") From bcbb825e77264d06772555712912096b5b591bdf Mon Sep 17 00:00:00 2001 From: "parker.leblanc" Date: Thu, 31 Aug 2023 12:11:36 -0700 Subject: [PATCH 63/76] Refactor how to autodetect a modem --- Hologram/Network/Cellular.py | 28 +++++++++++----------------- 1 file changed, 11 insertions(+), 17 deletions(-) diff --git a/Hologram/Network/Cellular.py b/Hologram/Network/Cellular.py index ce4b78eb..eab5c8c5 100644 --- a/Hologram/Network/Cellular.py +++ b/Hologram/Network/Cellular.py @@ -49,12 +49,10 @@ def __init__(self, event=Event()): def autodetect_modem(self): # scan for a modem and set it if found - dev_devices = Cellular.scan_for_modems() - if len(dev_devices) == 0: + first_modem_handler = Cellular._scan_and_select_first_supported_modem() + if first_modem_handler is None: raise NetworkError('Modem not detected') - first_modem = dev_devices[0] - modem_name = first_modem[0] - self.modem = self._modemHandlers[modem_name](event=self.event) + self.modem = first_modem_handler(event=self.event) def load_modem_drivers(self): self._load_modem_drivers() @@ -212,18 +210,16 @@ def _load_modem_drivers(self): @staticmethod - def scan_for_modems() -> list[Modem]: - res = [] - for (modemName, modemHandler) in Cellular._modemHandlers.items(): - modem_exists = Cellular._scan_for_modem(modemHandler) + 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: - modem = (modemName, modemHandler) - res.append(modem) - return res + return modemHandler + return None @staticmethod - def _scan_for_modem(modemHandler): + def _does_modem_exist_for_handler(modemHandler): usb_ids = modemHandler.usb_ids for vid_pid in usb_ids: if not vid_pid: @@ -235,15 +231,13 @@ def _scan_for_modem(modemHandler): @staticmethod def scan_for_all_usable_modems() -> list[Modem]: modems = [] - for (modemName, modemHandler) in Cellular._modemHandlers.items(): - modem_exists = Cellular._scan_for_modem(modemHandler) + for (_, modemHandler) in Cellular._modemHandlers.items(): + modem_exists = Cellular._does_modem_exist_for_handler(modemHandler) if modem_exists: test_handler = modemHandler() - # test_handler.closeSerialPort() usable_ports = test_handler.detect_usable_serial_port(stop_on_first=False) for port in usable_ports: modem = modemHandler(device_name=port) - # modem.closeSerialPort() modems.append(modem) return modems From a8c19d3977caef5d8b688f5e6fae4552dcb8b63b Mon Sep 17 00:00:00 2001 From: "parker.leblanc" Date: Tue, 5 Sep 2023 12:14:12 -0700 Subject: [PATCH 64/76] Update comment at andrews request --- Hologram/Network/Modem/Modem.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Hologram/Network/Modem/Modem.py b/Hologram/Network/Modem/Modem.py index b9cdb4ed..45f0de75 100644 --- a/Hologram/Network/Modem/Modem.py +++ b/Hologram/Network/Modem/Modem.py @@ -57,7 +57,7 @@ class Modem(IModem): 0x2F: u'\\', } - # We use device_name instead of serial port, if device name is set we skip searching for a serial port + # 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()): From 1c8d9efc377ed2fc0d08085674ff220d83c56479 Mon Sep 17 00:00:00 2001 From: "parker.leblanc" Date: Tue, 5 Sep 2023 12:17:10 -0700 Subject: [PATCH 65/76] Update documentation to point to python 3.9 instead of 3.7 --- .github/workflows/publish.yml | 4 ++-- CHANGELOG.md | 2 +- README.md | 2 +- install.sh | 4 ++-- setup.py | 2 +- version.txt | 2 +- 6 files changed, 8 insertions(+), 8 deletions(-) diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 715661e3..83342f1b 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -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: | diff --git a/CHANGELOG.md b/CHANGELOG.md index 579a1808..beec0697 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,7 +6,7 @@ includes the following bug fixes * PPP process is waited on for termination * SMS parser doesn't break for non SMS-RECEIVE messages * Add chunking for messages over 512 bytes long - * Install script checks for python versions >= 3.7 + * Install script checks for python versions >= 3.9 * Fix bug in disconnect (thanks @akumlehn ) * Send AT Commands from the CLI * Fix PPP errors related to routing and reconnection diff --git a/README.md b/README.md index c97b8275..9933e326 100644 --- a/README.md +++ b/README.md @@ -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. diff --git a/install.sh b/install.sh index 0e89d69e..35b1a331 100755 --- a/install.sh +++ b/install.sh @@ -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 } diff --git a/setup.py b/setup.py index acbfbf50..4367192e 100755 --- a/setup.py +++ b/setup.py @@ -46,7 +46,7 @@ 'Topic :: Internet', 'Topic :: Security :: Cryptography', 'Programming Language :: Python', - 'Programming Language :: Python :: 3.7', + 'Programming Language :: Python :: 3.9', ], **kw ) diff --git a/version.txt b/version.txt index 2003b639..78bc1abd 100644 --- a/version.txt +++ b/version.txt @@ -1 +1 @@ -0.9.2 +0.10.0 From 5cd1788ac3a9259c539912b4b3977b7af32fc662 Mon Sep 17 00:00:00 2001 From: "parker.leblanc" Date: Tue, 5 Sep 2023 12:19:12 -0700 Subject: [PATCH 66/76] Add updates to changelog --- CHANGELOG.md | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index beec0697..cd0808c8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,13 @@ # What's New in Hologram Python SDK +## v0.10.0 +2023-09-05 Hologram +* 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 modem objects +* Allow modems to send SMS messages through the modem interface + ## v0.9.1 2021-04-30 Hologram includes the following bug fixes From 63b2ca14a8d2c58304eb63e3827a42c254704f3f Mon Sep 17 00:00:00 2001 From: "parker.leblanc" Date: Tue, 5 Sep 2023 12:20:06 -0700 Subject: [PATCH 67/76] Fix 3.9 to 3.7 --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index cd0808c8..4afd68ba 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,7 +14,7 @@ includes the following bug fixes * PPP process is waited on for termination * SMS parser doesn't break for non SMS-RECEIVE messages * Add chunking for messages over 512 bytes long - * Install script checks for python versions >= 3.9 + * Install script checks for python versions >= 3.7 * Fix bug in disconnect (thanks @akumlehn ) * Send AT Commands from the CLI * Fix PPP errors related to routing and reconnection From d2e40595282729454f84b2be0dacbc2696e9d236 Mon Sep 17 00:00:00 2001 From: "parker.leblanc" Date: Tue, 5 Sep 2023 12:22:36 -0700 Subject: [PATCH 68/76] Add more concrete example to the changelog --- CHANGELOG.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4afd68ba..0bd359a1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,8 +5,8 @@ * 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 modem objects -* Allow modems to send SMS messages through the modem interface + 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!")` ## v0.9.1 2021-04-30 Hologram From 1ded340f2596e5b658c66884a639e02042c055bb Mon Sep 17 00:00:00 2001 From: "parker.leblanc" Date: Tue, 5 Sep 2023 12:24:52 -0700 Subject: [PATCH 69/76] Make users aware that sending smss may result in extra charges --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0bd359a1..5e53c303 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,7 +6,7 @@ * 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!")` +* 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 From e5f5770d8651bb386389cc92e94181fa58e6b2ff Mon Sep 17 00:00:00 2001 From: Parker LeBlanc Date: Tue, 5 Sep 2023 15:56:20 -0700 Subject: [PATCH 70/76] Fix the regex that breaks the install script --- install.sh | 4 ++-- update.sh | 6 +++--- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/install.sh b/install.sh index 35b1a331..771a8e82 100755 --- a/install.sh +++ b/install.sh @@ -61,7 +61,7 @@ function install_software() { } function check_python_version() { - if ! python3 -V | grep '3.[9-11].[0-9]' > /dev/null 2>&1; then + if ! python3 -V | grep -E '3.(9|1[012]).[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 @@ -124,7 +124,7 @@ do pause "Installing $program. Press [Enter] key to continue..."; install_software 'python3-pip' fi - if ! pip3 -V | grep '3.[7-9]' >/dev/null 2>&1; then + if ! pip3 -V | grep -E '3.(9|1[012])' >/dev/null 2>&1; then echo "pip3 is installed for an unsupported version of python." exit 1 fi diff --git a/update.sh b/update.sh index f52a6366..23d4c5dd 100755 --- a/update.sh +++ b/update.sh @@ -58,8 +58,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 -E '3.(9|1[012]).[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 } @@ -132,7 +132,7 @@ do pause "Installing $program. Press [Enter] key to continue..."; install_software 'python3-pip' fi - if ! pip3 -V | grep '3.[7-9]' >/dev/null 2>&1; then + if ! pip3 -V | grep -E '3.(9|1[012])' >/dev/null 2>&1; then echo "pip3 is installed for an unsupported version of python." exit 1 fi From 2d69457f2ad830aa838dcd69b6088b8a4fc8a559 Mon Sep 17 00:00:00 2001 From: Parker LeBlanc Date: Tue, 5 Sep 2023 16:05:27 -0700 Subject: [PATCH 71/76] Update version number --- version.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/version.txt b/version.txt index 78bc1abd..57121573 100644 --- a/version.txt +++ b/version.txt @@ -1 +1 @@ -0.10.0 +0.10.1 From 4fafac3501d424cdde7c89d889acbcb3953f90a3 Mon Sep 17 00:00:00 2001 From: Parker LeBlanc Date: Fri, 15 Sep 2023 09:39:06 -0700 Subject: [PATCH 72/76] Widen supported requests packages --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index b773c1f1..8229d290 100644 --- a/requirements.txt +++ b/requirements.txt @@ -5,4 +5,4 @@ python-sdk-auth==0.4.0 pyudev~=0.22.0 pyusb~=1.2.1 psutil~=5.8.0 -requests~=2.25.1 +requests>=2.25.1 From fe9b6194cb28aac20b31011815e7e82e230d1b29 Mon Sep 17 00:00:00 2001 From: "parker.leblanc" Date: Fri, 15 Sep 2023 15:14:22 -0700 Subject: [PATCH 73/76] Add a imei check around modem detection loop --- Hologram/Network/Cellular.py | 18 +++++++++++++----- 1 file changed, 13 insertions(+), 5 deletions(-) diff --git a/Hologram/Network/Cellular.py b/Hologram/Network/Cellular.py index eab5c8c5..b9c1d96e 100644 --- a/Hologram/Network/Cellular.py +++ b/Hologram/Network/Cellular.py @@ -231,14 +231,22 @@ def _does_modem_exist_for_handler(modemHandler): @staticmethod def scan_for_all_usable_modems() -> list[Modem]: modems = [] + unique_imeis = set() 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) + try: + 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) + imei = modem.imei + if imei not in unique_imeis: + unique_imeis.add(imei) + modems.append(modem) + except Exception: + # Any exception already logged up the chain + pass return modems From a9b935986c09a97883658277d530e9e415cadb82 Mon Sep 17 00:00:00 2001 From: Lalo Teijeiro Date: Wed, 20 Dec 2023 14:39:22 -0800 Subject: [PATCH 74/76] Allow different PDP contexts --- Hologram/Network/Modem/Modem.py | 25 ++++++++++++++----- Hologram/Network/Modem/Quectel.py | 12 +++++----- tests/Modem/test_BG96.py | 24 ++++++++++++++++++- tests/Modem/test_Modem.py | 40 +++++++++++++++++++++++++++++++ 4 files changed, 88 insertions(+), 13 deletions(-) diff --git a/Hologram/Network/Modem/Modem.py b/Hologram/Network/Modem/Modem.py index 45f0de75..43a194ee 100644 --- a/Hologram/Network/Modem/Modem.py +++ b/Hologram/Network/Modem/Modem.py @@ -13,13 +13,12 @@ from UtilClasses import ModemResult from UtilClasses import SMS from Hologram.Event import Event -from Exceptions.HologramError import SerialError, HologramError, NetworkError, PPPError +from Exceptions.HologramError import SerialError, NetworkError, PPPError from collections import deque import binascii import datetime -import logging import os import serial from serial.tools import list_ports @@ -35,6 +34,7 @@ class Modem(IModem): DEFAULT_SERIAL_TIMEOUT = 1 DEFAULT_SERIAL_RETRIES = 0 DEFAULT_SEND_TIMEOUT = 10 + DEFAULT_PDP_CONTEXT = 1 _RETRY_DELAY = 0.05 # 50 millisecond delay to avoid spinning loops @@ -58,8 +58,8 @@ class Modem(IModem): } # 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()): + def __init__(self, device_name=None, baud_rate='9600', chatscript_file=None, + event=Event(), apn='hologram', pdp_context=1): super().__init__(device_name=device_name, baud_rate=baud_rate, event=event) @@ -75,7 +75,8 @@ def __init__(self, device_name=None, baud_rate='9600', self.result = ModemResult.OK self.debug_out = '' self.in_ext = False - self._apn = 'hologram' + self._apn = apn + self._pdp_context = pdp_context self._initialize_device_name(device_name) @@ -741,6 +742,10 @@ def _is_pdp_context_active(self): def _set_up_pdp_context(self): if self._is_pdp_context_active(): return True self.logger.info('Setting up PDP context') + + if self._pdp_context != Modem.DEFAULT_PDP_CONTEXT: + self.set('+UPSD', f'0,100,{self._pdp_context}') + self.set('+UPSD', f'0,1,\"{self._apn}\"') self.set('+UPSD', '0,7,\"0.0.0.0\"') ok, _ = self.set('+UPSDA', '0,3', timeout=30) @@ -973,4 +978,12 @@ def apn(self): @apn.setter def apn(self, apn): self._apn = apn - return self.set('+CGDCONT', f'1,"IP","{self._apn}"') + return self.set('+CGDCONT', f'{self._pdp_context},"IP","{self._apn}"') + + @property + def pdp_context(self): + return self._pdp_context + + @pdp_context.setter + def pdp_context(self, pdp_context): + self._pdp_context = pdp_context diff --git a/Hologram/Network/Modem/Quectel.py b/Hologram/Network/Modem/Quectel.py index d996dd03..7b0f4c78 100644 --- a/Hologram/Network/Modem/Quectel.py +++ b/Hologram/Network/Modem/Quectel.py @@ -19,11 +19,11 @@ class Quectel(Modem): - def __init__(self, device_name=None, baud_rate='9600', - chatscript_file=None, event=Event()): + def __init__(self, device_name=None, baud_rate='9600', chatscript_file=None, + event=Event(), apn='hologram', pdp_context=1): - super().__init__(device_name=device_name, baud_rate=baud_rate, - chatscript_file=chatscript_file, event=event) + super().__init__(device_name=device_name, baud_rate=baud_rate, chatscript_file=chatscript_file, + event=event, apn=apn, pdp_context=pdp_context) self._at_sockets_available = True self.urc_response = '' @@ -152,8 +152,8 @@ def set_network_registration_status(self): def _set_up_pdp_context(self): if self._is_pdp_context_active(): return True self.logger.info('Setting up PDP context') - self.set('+QICSGP', f'1,1,\"{self._apn}\",\"\",\"\",1') - ok, _ = self.set('+QIACT', '1', timeout=30) + self.set('+QICSGP', f'{self._pdp_context},1,\"{self._apn}\",\"\",\"\",1') + ok, _ = self.set('+QIACT', f'{self._pdp_context}', timeout=30) if ok != ModemResult.OK: self.logger.error('PDP Context setup failed') raise NetworkError('Failed PDP context setup') diff --git a/tests/Modem/test_BG96.py b/tests/Modem/test_BG96.py index 681d1e09..bc61a641 100644 --- a/tests/Modem/test_BG96.py +++ b/tests/Modem/test_BG96.py @@ -6,7 +6,7 @@ # # test_BG96.py - This file implements unit tests for the BG96 modem interface. -from unittest.mock import patch +from unittest.mock import patch, call import pytest import sys @@ -72,3 +72,25 @@ def test_close_socket(mock_pdp, mock_command, mock_set, no_serial_port): modem.close_socket() mock_set.assert_called_with("+QIACT", "0", timeout=30) mock_command.assert_called_with("+QICLOSE", 1) + +@patch.object(BG96, "set") +def test_set_up_pdp_context_default(mock_set, no_serial_port): + modem = BG96() + mock_set.return_value = (ModemResult.OK, None) + + modem._set_up_pdp_context() + + expected_calls = [call('+QICSGP', '1,1,\"hologram\",\"\",\"\",1'), + call('+QIACT', '1', timeout=30)] + mock_set.assert_has_calls(expected_calls, any_order=True) + +@patch.object(BG96, "set") +def test_set_up_pdp_context_custom_apn_and_pdp_context(mock_set, no_serial_port): + modem = BG96(apn='hologram2', pdp_context=3) + mock_set.return_value = (ModemResult.OK, None) + + modem._set_up_pdp_context() + + expected_calls = [call('+QICSGP', '3,1,\"hologram2\",\"\",\"\",1'), + call('+QIACT', '3', timeout=30)] + mock_set.assert_has_calls(expected_calls, any_order=True) diff --git a/tests/Modem/test_Modem.py b/tests/Modem/test_Modem.py index e290d148..6c83b576 100644 --- a/tests/Modem/test_Modem.py +++ b/tests/Modem/test_Modem.py @@ -6,6 +6,7 @@ # # test_Modem.py - This file implements unit tests for the Modem class. +from unittest.mock import patch, call import pytest import sys from datetime import datetime @@ -47,6 +48,9 @@ def mock_command_sms(modem, at_command): def mock_set_sms(modem, at_command, val): return None +def mock_inactive_pdp_context(modem): + return False + @pytest.fixture def no_serial_port(monkeypatch): monkeypatch.setattr(Modem, '_read_from_serial_port', mock_read) @@ -56,6 +60,7 @@ def no_serial_port(monkeypatch): monkeypatch.setattr(Modem, 'openSerialPort', mock_open_serial_port) monkeypatch.setattr(Modem, 'closeSerialPort', mock_close_serial_port) monkeypatch.setattr(Modem, 'detect_usable_serial_port', mock_detect_usable_serial_port) + monkeypatch.setattr(Modem, '_is_pdp_context_active', mock_inactive_pdp_context) @pytest.fixture def get_sms(monkeypatch): @@ -75,6 +80,8 @@ def test_init_modem_no_args(no_serial_port): assert(modem.chatscript_file.endswith('/chatscripts/default-script')) assert(modem._at_sockets_available == False) assert(modem.description == 'Modem') + assert(modem.apn == 'hologram') + assert(modem.pdp_context == 1) def test_init_modem_chatscriptfileoverride(no_serial_port): modem = Modem(chatscript_file='test-chatscript') @@ -82,6 +89,14 @@ def test_init_modem_chatscriptfileoverride(no_serial_port): assert(modem.socket_identifier == 0) assert(modem.chatscript_file == 'test-chatscript') +def test_init_modem_apn(no_serial_port): + modem = Modem(apn='hologram2') + assert(modem.apn == 'hologram2') + +def test_init_modem_pdp_context(no_serial_port): + modem = Modem(pdp_context=3) + assert(modem.pdp_context == 3) + def test_get_result_string(no_serial_port): modem = Modem() assert(modem.getResultString(0) == 'Modem returned OK') @@ -98,6 +113,31 @@ def test_get_location(no_serial_port): assert(modem.location == 'test location') assert('This modem does not support this property' in str(e)) +@patch.object(Modem, "set") +def test_set_up_pdp_context_default(mock_set, no_serial_port): + modem = Modem() + mock_set.return_value = (ModemResult.OK, None) + + modem._set_up_pdp_context() + + expected_calls = [call('+UPSD', '0,1,\"hologram\"'), + call('+UPSD', '0,7,\"0.0.0.0\"'), + call('+UPSDA', '0,3', timeout=30)] + mock_set.assert_has_calls(expected_calls, any_order=True) + +@patch.object(Modem, "set") +def test_set_up_pdp_context_custom_apn_and_pdp_context(mock_set, no_serial_port): + modem = Modem(apn='hologram2', pdp_context=3) + mock_set.return_value = (ModemResult.OK, None) + + modem._set_up_pdp_context() + + expected_calls = [call('+UPSD', '0,100,3'), + call('+UPSD', '0,1,\"hologram2\"'), + call('+UPSD', '0,7,\"0.0.0.0\"'), + call('+UPSDA', '0,3', timeout=30)] + mock_set.assert_has_calls(expected_calls, any_order=True) + # SMS def test_get_sms(no_serial_port, get_sms): From 191e7e252cd087e8a427939644e9f79f15db90bb Mon Sep 17 00:00:00 2001 From: Parker LeBlanc Date: Tue, 7 Oct 2025 10:51:16 -0700 Subject: [PATCH 75/76] Add support for the EC25 and EG25 both are recognized as the same usb --- AUTHORS.md | 1 + CHANGELOG.md | 4 ++ Hologram/Network/Cellular.py | 108 +++++++++++++++++++-------------- Hologram/Network/Modem/EC25.py | 36 +++++++++++ version.txt | 2 +- 5 files changed, 103 insertions(+), 48 deletions(-) create mode 100644 Hologram/Network/Modem/EC25.py diff --git a/AUTHORS.md b/AUTHORS.md index eff33fa7..17fb3a83 100644 --- a/AUTHORS.md +++ b/AUTHORS.md @@ -4,3 +4,4 @@ * Reuben Balik * Erik Larson * Jeremy Tidemann +* Parker LeBlanc diff --git a/CHANGELOG.md b/CHANGELOG.md index 5e53c303..70eee246 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,9 @@ # What's New in Hologram Python SDK +## v0.10.2 +2025-10-07 Hologram +* Add support for Quectel EC-25 and EG25 + ## v0.10.0 2023-09-05 Hologram * targets python version 3.9 diff --git a/Hologram/Network/Cellular.py b/Hologram/Network/Cellular.py index b9c1d96e..fc63c163 100644 --- a/Hologram/Network/Cellular.py +++ b/Hologram/Network/Cellular.py @@ -11,7 +11,18 @@ from Hologram.Event import Event from Exceptions.HologramError import NetworkError from Hologram.Network.Route import Route -from Hologram.Network.Modem import Modem, E303, MS2131, E372, BG96, EC21, Nova_U201, NovaM, DriverLoader +from Hologram.Network.Modem import ( + Modem, + E303, + MS2131, + E372, + BG96, + EC21, + EC25, + Nova_U201, + NovaM, + DriverLoader, +) from Hologram.Network import Network, NetworkScope import time from typing import Union @@ -24,19 +35,21 @@ CLOUD_ERR_SIGNAL = 5 CLOUD_ERR_CONNECT = 12 -DEFAULT_CELLULAR_TIMEOUT = 200 # slightly more than 3 mins +DEFAULT_CELLULAR_TIMEOUT = 200 # slightly more than 3 mins + class Cellular(Network): _modemHandlers = { - 'e303': E303.E303, - 'ms2131': MS2131.MS2131, - 'e372': E372.E372, - 'bg96': BG96.BG96, - 'ec21': EC21.EC21, - 'nova': Nova_U201.Nova_U201, - 'novam': NovaM.NovaM, - '': Modem + "e303": E303.E303, + "ms2131": MS2131.MS2131, + "e372": E372.E372, + "bg96": BG96.BG96, + "ec21": EC21.EC21, + "ec25": EC25.EC25, + "nova": Nova_U201.Nova_U201, + "novam": NovaM.NovaM, + "": Modem, } def __init__(self, event=Event()): @@ -46,35 +59,34 @@ def __init__(self, event=Event()): self._route = Route() self.__receive_port = None - def autodetect_modem(self): # scan for a modem and set it if found first_modem_handler = Cellular._scan_and_select_first_supported_modem() if first_modem_handler is None: - raise NetworkError('Modem not detected') + raise NetworkError("Modem not detected") self.modem = first_modem_handler(event=self.event) def load_modem_drivers(self): self._load_modem_drivers() - def getConnectionStatus(self): return self._connection_status def is_connected(self): return self._connection_status == CLOUD_CONNECTED or self.modem.is_connected() - def connect(self, timeout = DEFAULT_CELLULAR_TIMEOUT): - self.logger.info('Connecting to cell network with timeout of %s seconds', timeout) + def connect(self, timeout=DEFAULT_CELLULAR_TIMEOUT): + self.logger.info( + "Connecting to cell network with timeout of %s seconds", timeout + ) success = False try: - success = self.modem.connect(timeout = timeout) + success = self.modem.connect(timeout=timeout) except KeyboardInterrupt as e: pass - if success: - self.logger.info('Successfully connected to cell network') + self.logger.info("Successfully connected to cell network") # Disable at sockets mode since we're already establishing PPP. # This call is needed in certain modems that have limited interfaces to work with. time.sleep(2) @@ -82,34 +94,34 @@ def connect(self, timeout = DEFAULT_CELLULAR_TIMEOUT): self.disable_at_sockets_mode() self.__configure_routing() self._connection_status = CLOUD_CONNECTED - self.event.broadcast('cellular.connected') + self.event.broadcast("cellular.connected") super().connect() else: - self.logger.info('Failed to connect to cell network') + self.logger.info("Failed to connect to cell network") return success def disconnect(self): - self.logger.info('Disconnecting from cell network') + self.logger.info("Disconnecting from cell network") self.__remove_routing() success = self.modem.disconnect() if success: - self.logger.info('Successfully disconnected from cell network') + self.logger.info("Successfully disconnected from cell network") self.enable_at_sockets_mode() self._connection_status = CLOUD_DISCONNECTED - self.event.broadcast('cellular.disconnected') + self.event.broadcast("cellular.disconnected") super().disconnect() else: - self.logger.info('Failed to disconnect from cell network') + self.logger.info("Failed to disconnect from cell network") return success def reconnect(self): - self.logger.info('Reconnecting to cell network') + self.logger.info("Reconnecting to cell network") success = self.disconnect() if success == False: - self.logger.info('Failed to disconnect from cell network') + self.logger.info("Failed to disconnect from cell network") return False return self.connect() @@ -136,7 +148,9 @@ def send_message(self, data): def open_receive_socket(self, receive_port): self.__receive_port = receive_port - self.event.subscribe('cellular.forced_disconnect', self.__reconnect_after_forced_disconnect) + self.event.subscribe( + "cellular.forced_disconnect", self.__reconnect_after_forced_disconnect + ) return self.modem.open_receive_socket(receive_port) def pop_received_message(self): @@ -167,40 +181,42 @@ def __reconnect_and_receive(self): self.open_receive_socket(self.__receive_port) def __reconnect_after_forced_disconnect(self): - self.logger.info('Reconnecting after forced disconnect...') + self.logger.info("Reconnecting after forced disconnect...") time.sleep(5) # uBlox takes some time to update internal state after disconnect self.__reconnect_and_receive() while not self.is_connected(): - self.logger.info('Reconnect failed. Retrying in 5 seconds...') + self.logger.info("Reconnect failed. Retrying in 5 seconds...") time.sleep(5) self.__reconnect_and_receive() - self.logger.info('Ready to receive data on port %s', self.__receive_port) + self.logger.info("Ready to receive data on port %s", self.__receive_port) def __configure_routing(self): # maybe we don't have to tear down the routes but we probably should - self.logger.info('Adding routes to Hologram cloud') - self._route.add('10.176.0.0/16', self.localIPAddress) - self._route.add('10.254.0.0/16', self.localIPAddress) + self.logger.info("Adding routes to Hologram cloud") + self._route.add("10.176.0.0/16", self.localIPAddress) + self._route.add("10.254.0.0/16", self.localIPAddress) if self.scope == NetworkScope.SYSTEM: - self.logger.info('Adding system-wide default route to cellular interface') + self.logger.info("Adding system-wide default route to cellular interface") self._route.add_default(self.localIPAddress) def __remove_routing(self): - self.logger.info('Removing routes to Hologram cloud') + self.logger.info("Removing routes to Hologram cloud") if self.localIPAddress: - self._route.delete('10.176.0.0/16', self.localIPAddress) - self._route.delete('10.254.0.0/16', self.localIPAddress) + self._route.delete("10.176.0.0/16", self.localIPAddress) + self._route.delete("10.254.0.0/16", self.localIPAddress) if self.scope == NetworkScope.SYSTEM: - self.logger.info('Removing system-wide default route to cellular interface') + self.logger.info( + "Removing system-wide default route to cellular interface" + ) self._route.delete_default(self.localIPAddress) def _load_modem_drivers(self): dl = DriverLoader.DriverLoader() - for (modemName, modemHandler) in self._modemHandlers.items(): + for modemName, modemHandler in self._modemHandlers.items(): module = modemHandler.module if module: if not dl.is_module_loaded(module): - self.logger.info('Loading module %s for %s', module, modemName) + self.logger.info("Loading module %s for %s", module, modemName) dl.load_module(module) syspath = modemHandler.syspath if syspath: @@ -208,16 +224,14 @@ def _load_modem_drivers(self): for vid_pid in usb_ids: dl.force_driver_for_device(syspath, vid_pid[0], vid_pid[1]) - @staticmethod def _scan_and_select_first_supported_modem() -> Union[Modem, None]: - for (_, modemHandler) in Cellular._modemHandlers.items(): + for _, modemHandler in Cellular._modemHandlers.items(): modem_exists = Cellular._does_modem_exist_for_handler(modemHandler) if modem_exists: return modemHandler return None - @staticmethod def _does_modem_exist_for_handler(modemHandler): usb_ids = modemHandler.usb_ids @@ -232,12 +246,14 @@ def _does_modem_exist_for_handler(modemHandler): def scan_for_all_usable_modems() -> list[Modem]: modems = [] unique_imeis = set() - for (_, modemHandler) in Cellular._modemHandlers.items(): + for _, modemHandler in Cellular._modemHandlers.items(): modem_exists = Cellular._does_modem_exist_for_handler(modemHandler) if modem_exists: try: test_handler = modemHandler() - usable_ports = test_handler.detect_usable_serial_port(stop_on_first=False) + usable_ports = test_handler.detect_usable_serial_port( + stop_on_first=False + ) for port in usable_ports: modem = modemHandler(device_name=port) imei = modem.imei @@ -249,8 +265,6 @@ def scan_for_all_usable_modems() -> list[Modem]: pass return modems - - @property def modem(self): return self._modem diff --git a/Hologram/Network/Modem/EC25.py b/Hologram/Network/Modem/EC25.py new file mode 100644 index 00000000..4f5e4f7a --- /dev/null +++ b/Hologram/Network/Modem/EC25.py @@ -0,0 +1,36 @@ +# EC25.py - Hologram Python SDK Quectel EC25 modem interface +# +# Author: Hologram +# +# Copyright 2025 - Hologram (Konekt, Inc.) +# +# +# LICENSE: Distributed under the terms of the MIT License +# + +from Hologram.Network.Modem.Quectel import Quectel +from UtilClasses import ModemResult + +DEFAULT_EC21_TIMEOUT = 200 + + +class EC25(Quectel): + usb_ids = [("2c7c", "0125")] + + def connect(self, timeout=DEFAULT_EC21_TIMEOUT): + success = super().connect(timeout) + return success + + def _tear_down_pdp_context(self): + if not self._is_pdp_context_active(): + return True + self.logger.info("Tearing down PDP context") + ok, _ = self.set("+QIDEACT", "1", timeout=30) + if ok != ModemResult.OK: + self.logger.error("PDP Context tear down failed") + else: + self.logger.info("PDP context deactivated") + + @property + def description(self): + return "Quectel EC25" diff --git a/version.txt b/version.txt index 57121573..5eef0f10 100644 --- a/version.txt +++ b/version.txt @@ -1 +1 @@ -0.10.1 +0.10.2 From 4b8156ec992d5f365b96cefb8817eef77dc5d842 Mon Sep 17 00:00:00 2001 From: Parker LeBlanc Date: Tue, 7 Oct 2025 11:51:32 -0700 Subject: [PATCH 76/76] Authenticate with twine via api token not username and password --- .github/workflows/publish.yml | 7 ++++--- version.txt | 2 +- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 83342f1b..a0ea2103 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -19,10 +19,11 @@ jobs: run: | python -m pip install --upgrade pip pip install setuptools wheel twine + - name: Build and publish env: - TWINE_USERNAME: ${{ secrets.PYPI_USERNAME }} - TWINE_PASSWORD: ${{ secrets.PYPI_PASSWORD }} + TWINE_USERNAME: "__token__" + TWINE_PASSWORD: ${{ secrets.PYPI_API_TOKEN }} run: | python setup.py sdist bdist_wheel - twine upload dist/* + twine upload dist/* \ No newline at end of file diff --git a/version.txt b/version.txt index 5eef0f10..a3f5a8ed 100644 --- a/version.txt +++ b/version.txt @@ -1 +1 @@ -0.10.2 +0.10.3