├── tests ├── __init__.py ├── test_utils_status.py ├── test_modules_peripherals.py ├── test_modules_config.py ├── conftest.py ├── test_modules_file.py ├── test_modules_gps.py ├── test_modules_auth.py ├── test_apps_google_sheets.py ├── test_modules_ssl.py └── test_modules_base.py ├── pico_lte ├── __init__.py ├── apps │ ├── __init__.py │ ├── slack.py │ ├── scriptr.py │ ├── telegram.py │ └── thingspeak.py ├── modules │ ├── __init__.py │ ├── config.py │ ├── peripherals.py │ ├── file.py │ ├── gps.py │ ├── auth.py │ ├── base.py │ └── ssl.py ├── utils │ ├── __init__.py │ ├── status.py │ ├── debug.py │ ├── helpers.py │ ├── manager.py │ └── atcom.py ├── common.py └── core.py ├── package.json ├── examples ├── __basic__ │ ├── blink_led.py │ ├── button_counter.py │ ├── button_controlled_led_toggle.py │ ├── led_brightness_with_pwm.py │ ├── neopixel_led_color_cycling.py │ ├── neopixel_led_rainbow_effect.py │ ├── qwiic.py │ └── monitor_network.py ├── scriptr │ └── send_data.py ├── slack │ └── send_message.py ├── telegram │ └── send_message.py ├── azure │ ├── mqtt_publish.py │ └── mqtt_subscribe.py ├── aws-iot │ ├── http_post.py │ ├── mqtt_publish.py │ └── mqtt_subscribe.py ├── google_sheets │ ├── create_sheet.py │ ├── add_row.py │ ├── delete_data.py │ ├── add_data.py │ └── get_data.py ├── thingspeak │ ├── mqtt_publish.py │ └── mqtt_subscribe.py ├── http │ ├── get.py │ ├── put.py │ ├── post.py │ ├── get_with_custom_header.py │ └── post_with_custom_header.py ├── mqtt │ ├── publish.py │ └── subscribe.py ├── gps │ ├── send_location_to_telegram.py │ └── send_location_to_server.py └── __sdk__ │ └── create_your_own_method.py ├── tools ├── deploy.sh ├── run.sh ├── upload.sh └── build.sh ├── CHANGELOG.md ├── LICENSE ├── CONTRIBUTING.md ├── .gitignore ├── README.md └── CONFIGURATIONS.md /tests/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /pico_lte/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /pico_lte/apps/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /pico_lte/modules/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /pico_lte/utils/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "urls": [], 3 | "deps": [], 4 | "version": "0.4.0", 5 | "license": "MIT" 6 | } 7 | -------------------------------------------------------------------------------- /pico_lte/utils/status.py: -------------------------------------------------------------------------------- 1 | """Data class for status.""" 2 | 3 | 4 | class Status: 5 | SUCCESS = 0 6 | ERROR = 1 7 | TIMEOUT = 2 8 | ONGOING = 3 9 | UNKNOWN = 99 10 | -------------------------------------------------------------------------------- /examples/__basic__/blink_led.py: -------------------------------------------------------------------------------- 1 | """ 2 | 3 | Example code for blinking the USER LED. 4 | 5 | """ 6 | 7 | import machine 8 | import utime 9 | 10 | led = machine.Pin(22, machine.Pin.OUT) 11 | 12 | while True: 13 | led.toggle() 14 | utime.sleep(1) -------------------------------------------------------------------------------- /tests/test_utils_status.py: -------------------------------------------------------------------------------- 1 | """ 2 | Test module for the utils.status module. 3 | """ 4 | 5 | from pico_lte.utils.status import Status 6 | 7 | 8 | class TestStatus: 9 | """ 10 | Test class for the utils.status module. 11 | """ 12 | 13 | def test_status_codes(self): 14 | """This method tests the attributes of Status class.""" 15 | assert Status.SUCCESS == 0 16 | assert Status.ERROR == 1 17 | assert Status.TIMEOUT == 2 18 | assert Status.ONGOING == 3 19 | assert Status.UNKNOWN == 99 20 | -------------------------------------------------------------------------------- /examples/__basic__/button_counter.py: -------------------------------------------------------------------------------- 1 | """ 2 | 3 | Example code for basic counter using USER button. 4 | Create a simple counter using a USER Button on the Pico LTE. 5 | When the button is pressed, the counter increases by one, and the updated value is printed in the terminal. 6 | 7 | """ 8 | 9 | import machine 10 | import utime 11 | 12 | button = machine.Pin(21, machine.Pin.IN, machine.Pin.PULL_DOWN) 13 | counter = 0 14 | 15 | while True: 16 | if button.value() == 0: 17 | counter += 1 18 | print("Counter:", counter) 19 | while button.value() == 0: 20 | pass # Wait for the button to be released 21 | utime.sleep(0.1) -------------------------------------------------------------------------------- /examples/scriptr/send_data.py: -------------------------------------------------------------------------------- 1 | """ 2 | Example Configuration 3 | --------------------- 4 | Create a config.json file in the root directory of the PicoLTE device. 5 | config.json file must include the following parameters for this example: 6 | config.json 7 | { 8 | "scriptr":{ 9 | "query": "[QUERY_OF_SCRIPT]", 10 | "authorization": "[YOUR_TOKEN]" 11 | } 12 | } 13 | """ 14 | import json 15 | from pico_lte.core import PicoLTE 16 | from pico_lte.common import debug 17 | 18 | picoLTE = PicoLTE() 19 | 20 | payload_json = {"temp": "25"} 21 | payload = json.dumps(payload_json) 22 | 23 | debug.info("Sending data to Scriptr.io script...") 24 | result = picoLTE.scriptr.send_data(payload) 25 | debug.info("Result:", result) 26 | -------------------------------------------------------------------------------- /examples/slack/send_message.py: -------------------------------------------------------------------------------- 1 | """ 2 | Example code for send message to slack channel by using 3 | incoming webhooks feature of Slack API. 4 | 5 | Example Configuration 6 | --------------------- 7 | Create a config.json file in the root directory of the PicoLTE device. 8 | config.json file must include the following parameters for this example: 9 | 10 | config.json 11 | { 12 | "slack":{ 13 | "webhook_url": "[INCOMING_WEBHOOK_URL]" 14 | } 15 | } 16 | """ 17 | 18 | from pico_lte.core import PicoLTE 19 | from pico_lte.common import debug 20 | 21 | picoLTE = PicoLTE() 22 | 23 | debug.info("Sending message to slack channel...") 24 | result = picoLTE.slack.send_message("It is test message from PicoLTE!") 25 | debug.info("Result:", result) 26 | -------------------------------------------------------------------------------- /examples/telegram/send_message.py: -------------------------------------------------------------------------------- 1 | """ 2 | Example code for sending message to group chat in Telegram with using its Bot API. 3 | 4 | Example Configuration 5 | --------------------- 6 | Create a config.json file in the root directory of the PicoLTE device. 7 | config.json file must include the following parameters for this example: 8 | 9 | config.json 10 | { 11 | "telegram": { 12 | "token": "[YOUR_BOT_TOKEN_ID]", 13 | "chat_id": "[YOUR_GROUP_CHAT_ID]" 14 | } 15 | } 16 | """ 17 | 18 | from pico_lte.core import PicoLTE 19 | from pico_lte.common import debug 20 | 21 | picoLTE = PicoLTE() 22 | 23 | debug.info("Sending message to Telegram channel...") 24 | result = picoLTE.telegram.send_message("PicoLTE Telegram Example") 25 | debug.info("Result:", result) 26 | -------------------------------------------------------------------------------- /examples/__basic__/button_controlled_led_toggle.py: -------------------------------------------------------------------------------- 1 | """ 2 | 3 | Example code for button controlled LED toggle. 4 | When the USER button is pressed, the light turns on. When the button is not pressed, the light turns off. 5 | 6 | """ 7 | 8 | import machine 9 | import utime 10 | 11 | led = machine.Pin(22, machine.Pin.OUT) # We set up the pin to control the light. 12 | button = machine.Pin(21, machine.Pin.IN, machine.Pin.PULL_DOWN) # We set up the pin for the button. 13 | 14 | while True: 15 | if button.value() == 0: # If the button is pressed (value is 0): 16 | led.on() # Turn on the light. 17 | else: # If the button is not pressed (value is 1): 18 | led.off() # Turn off the light. 19 | utime.sleep(0.1) # Wait for a short moment before checking again. -------------------------------------------------------------------------------- /examples/__basic__/led_brightness_with_pwm.py: -------------------------------------------------------------------------------- 1 | """ 2 | 3 | Example code for control USER LED brightness with PWM. 4 | 5 | """ 6 | 7 | import machine 8 | import utime 9 | 10 | led = machine.Pin(22, machine.Pin.OUT) # Set up a pin to control the LED. 11 | pwm_led = machine.PWM(led) # Initialize PWM (Pulse Width Modulation) for the LED. 12 | 13 | while True: 14 | for duty_cycle in range(0, 65535, 1024): # Increase LED brightness. 15 | pwm_led.duty_u16(duty_cycle) # Set the LED brightness level. 16 | utime.sleep(0.01) # Pause to observe the change. 17 | for duty_cycle in range(64511, 0, -1024): # Decrease LED brightness. 18 | pwm_led.duty_u16(duty_cycle) # Set the LED brightness level. 19 | utime.sleep(0.01) # Pause to observe the change. -------------------------------------------------------------------------------- /examples/azure/mqtt_publish.py: -------------------------------------------------------------------------------- 1 | """ 2 | Example code for publising data to Azure IoT Hub Device twin by using MQTT. 3 | 4 | Example Configuration 5 | --------------------- 6 | Create a config.json file in the root directory of the PicoLTE device. 7 | config.json file must include the following parameters for this example: 8 | 9 | config.json 10 | { 11 | "azure":{ 12 | "hub_name": "[YOUR_AZURE_IOT_HUB_NAME]", 13 | "device_id": "[YOUR_DEVICE_ID]" 14 | } 15 | } 16 | """ 17 | 18 | import json 19 | from pico_lte.core import PicoLTE 20 | from pico_lte.common import debug 21 | 22 | picoLTE = PicoLTE() 23 | 24 | payload_json = {"App": "Azure MQTT Example"} 25 | 26 | debug.info("Publishing data to Azure IoT Hub...") 27 | payload = json.dumps(payload_json) 28 | result = picoLTE.azure.publish_message(payload) 29 | debug.info("Result", result) 30 | -------------------------------------------------------------------------------- /examples/aws-iot/http_post.py: -------------------------------------------------------------------------------- 1 | """ 2 | Example code for publising data to AWS IoT using HTTP. 3 | 4 | Example Configuration 5 | --------------------- 6 | Create a config.json file in the root directory of the PicoLTE device. 7 | config.json file must include the following parameters for this example: 8 | 9 | config.json 10 | { 11 | "aws":{ 12 | "https":{ 13 | "endpoint":"[YOUR_AWS_IOT_ENDPOINT]" 14 | "topic":"[YOUR_DEVICE_TOPIC]" 15 | } 16 | } 17 | } 18 | """ 19 | import json 20 | from pico_lte.core import PicoLTE 21 | from pico_lte.common import debug 22 | 23 | picoLTE = PicoLTE() 24 | 25 | payload_json = {"state": {"reported": {"App": "AWS HTTP Example"}}} 26 | 27 | debug.info("Publishing data to AWS IoT...") 28 | payload = json.dumps(payload_json) 29 | result = picoLTE.aws.post_message(payload) 30 | debug.info("Result", result) 31 | -------------------------------------------------------------------------------- /tools/deploy.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # This script is used to embed the library and upload the firmware to the Pico board. 3 | 4 | # Internal variables 5 | PROJECT_DIR=$(pwd) 6 | GREEN='\033[0;32m' 7 | RED='\033[0;31m' 8 | NOCOLOR='\033[0m' 9 | 10 | print_the_status_of_command() { 11 | if [ $? -eq 0 ]; then 12 | echo -e " ${GREEN}OK${NOCOLOR}" 13 | else 14 | echo -e " ${RED}FAILED${NOCOLOR}" 15 | exit 1 16 | fi 17 | } 18 | 19 | build_the_firmware() { 20 | # Run the build script. 21 | source $PROJECT_DIR/tools/build.sh $1 22 | } 23 | 24 | upload_the_script() { 25 | # Run the upload script. 26 | cd $PROJECT_DIR 27 | chmod a+x $PROJECT_DIR/tools/upload.sh 28 | $PROJECT_DIR/tools/upload.sh $BUILD_ID 29 | } 30 | 31 | # Main function 32 | echo "[ BUILD ]" 33 | build_the_firmware $1 34 | echo "[ UPLOAD ]" 35 | upload_the_script -------------------------------------------------------------------------------- /examples/google_sheets/create_sheet.py: -------------------------------------------------------------------------------- 1 | """ 2 | Example code for creating a new Google Sheets spreadsheet and its sheets with using its API. 3 | 4 | Example Configuration 5 | --------------------- 6 | Create a config.json file in the root directory of the PicoLTE device. 7 | config.json file must include the following parameters for this example: 8 | 9 | config.json 10 | { 11 | "google_sheets":{ 12 | "api_key": "[API_KEY]", 13 | "client_id": "[CLIENT_ID]", 14 | "client_secret": "[CLIENT_SECRET]", 15 | "refresh_token": "[REFRESH_TOKEN]" 16 | } 17 | } 18 | """ 19 | from pico_lte.core import PicoLTE 20 | from pico_lte.common import debug 21 | 22 | picoLTE = PicoLTE() 23 | 24 | debug.info("Creating a new Google Sheets spreadsheet and its sheets...") 25 | result = picoLTE.google_sheets.create_sheet(sheets=["Sheet1", "Sheet2"]) 26 | debug.info("Result:", result) 27 | -------------------------------------------------------------------------------- /examples/google_sheets/add_row.py: -------------------------------------------------------------------------------- 1 | """ 2 | Example code for appending new row to a Google Sheets table with using its API. 3 | 4 | Example Configuration 5 | --------------------- 6 | Create a config.json file in the root directory of the PicoLTE device. 7 | config.json file must include the following parameters for this example: 8 | 9 | config.json 10 | { 11 | "google_sheets":{ 12 | "api_key": "[API_KEY]", 13 | "spreadsheetId": "[SPREAD_SHEET_ID]", 14 | "client_id": "[CLIENT_ID]", 15 | "client_secret": "[CLIENT_SECRET]", 16 | "refresh_token": "[REFRESH_TOKEN]" 17 | } 18 | } 19 | 20 | """ 21 | from pico_lte.core import PicoLTE 22 | from pico_lte.common import debug 23 | 24 | picoLTE = PicoLTE() 25 | 26 | debug.info("Appending new row to the Google Sheets table...") 27 | result = picoLTE.google_sheets.add_row(sheet="Sheet1", data=[[1, 2, 3, 4]]) 28 | debug.info("Result:", result) 29 | -------------------------------------------------------------------------------- /examples/google_sheets/delete_data.py: -------------------------------------------------------------------------------- 1 | """ 2 | Example code for deleting targeted datas of a Google Sheets table with using its API. 3 | 4 | Example Configuration 5 | --------------------- 6 | Create a config.json file in the root directory of the PicoLTE device. 7 | config.json file must include the following parameters for this example: 8 | 9 | config.json 10 | { 11 | "google_sheets":{ 12 | "api_key": "[API_KEY]", 13 | "spreadsheetId": "[SPREAD_SHEET_ID]", 14 | "client_id": "[CLIENT_ID]", 15 | "client_secret": "[CLIENT_SECRET]", 16 | "refresh_token": "[REFRESH_TOKEN]" 17 | } 18 | } 19 | """ 20 | from pico_lte.core import PicoLTE 21 | from pico_lte.common import debug 22 | 23 | picoLTE = PicoLTE() 24 | 25 | debug.info("Cleaning data from the Google Sheets table...") 26 | result = picoLTE.google_sheets.delete_data(sheet="Sheet1", data_range="A1:C2") 27 | debug.info("Result:", result) 28 | -------------------------------------------------------------------------------- /examples/aws-iot/mqtt_publish.py: -------------------------------------------------------------------------------- 1 | """ 2 | Example code for publising data to AWS IoT by using MQTT. 3 | 4 | Example Configuration 5 | --------------------- 6 | Create a config.json file in the root directory of the PicoLTE device. 7 | config.json file must include the following parameters for this example: 8 | 9 | config.json 10 | { 11 | "aws":{ 12 | "mqtts":{ 13 | "host":"[YOUR_AWSIOT_ENDPOINT]", 14 | "port":"[YOUR_AWSIOT_MQTT_PORT]", 15 | "pub_topic":"[YOUR_MQTT_TOPIC]", 16 | } 17 | } 18 | } 19 | """ 20 | import json 21 | from pico_lte.core import PicoLTE 22 | from pico_lte.common import debug 23 | 24 | picoLTE = PicoLTE() 25 | 26 | payload_json = {"state": {"reported": {"App": "AWS MQTT Example"}}} 27 | 28 | debug.info("Publishing data to AWS IoT...") 29 | payload = json.dumps(payload_json) 30 | result = picoLTE.aws.publish_message(payload) 31 | debug.info("Result", result) 32 | -------------------------------------------------------------------------------- /examples/__basic__/neopixel_led_color_cycling.py: -------------------------------------------------------------------------------- 1 | """ 2 | 3 | Example code for color cycling on NeoPixel LED. 4 | 5 | """ 6 | 7 | import machine 8 | import neopixel 9 | import utime 10 | 11 | NUM_LEDS = 8 12 | pin = machine.Pin(15) 13 | np = neopixel.NeoPixel(pin, NUM_LEDS) 14 | 15 | def color_cycle(wait): 16 | for i in range(NUM_LEDS): 17 | np[i] = (255, 0, 0) # Set LED color to red 18 | np.write() 19 | utime.sleep_ms(wait) # Wait for a short duration 20 | 21 | for i in range(NUM_LEDS): 22 | np[i] = (0, 255, 0) # Set LED color to green 23 | np.write() 24 | utime.sleep_ms(wait) # Wait for a short duration 25 | 26 | for i in range(NUM_LEDS): 27 | np[i] = (0, 0, 255) # Set LED color to blue 28 | np.write() 29 | utime.sleep_ms(wait) # Wait for a short duration 30 | 31 | while True: 32 | color_cycle(100) # Call the color cycle function with a delay of 100 milliseconds -------------------------------------------------------------------------------- /examples/google_sheets/add_data.py: -------------------------------------------------------------------------------- 1 | """ 2 | Example code for data adding or updating operations of a Google Sheets table with using its API. 3 | 4 | Example Configuration 5 | --------------------- 6 | Create a config.json file in the root directory of the PicoLTE device. 7 | config.json file must include the following parameters for this example: 8 | 9 | config.json 10 | { 11 | "google_sheets":{ 12 | "api_key": "[API_KEY]", 13 | "spreadsheetId": "[SPREAD_SHEET_ID]", 14 | "client_id": "[CLIENT_ID]", 15 | "client_secret": "[CLIENT_SECRET]", 16 | "refresh_token": "[REFRESH_TOKEN]" 17 | } 18 | } 19 | 20 | """ 21 | from pico_lte.core import PicoLTE 22 | from pico_lte.common import debug 23 | 24 | picoLTE = PicoLTE() 25 | 26 | debug.info("Adding data to the Google Sheets table...") 27 | result = picoLTE.google_sheets.add_data( 28 | sheet="Sheet1", data=[[1, 2, 3], [4, 5, 6]], data_range="A1:C2" 29 | ) 30 | debug.info("Result:", result) 31 | -------------------------------------------------------------------------------- /examples/google_sheets/get_data.py: -------------------------------------------------------------------------------- 1 | """ 2 | Example code for getting data from a Google Sheets table with using its API. 3 | 4 | Example Configuration 5 | --------------------- 6 | Create a config.json file in the root directory of the PicoLTE device. 7 | config.json file must include the following parameters for this example: 8 | 9 | config.json 10 | { 11 | "google_sheets":{ 12 | "api_key": "[API_KEY]", 13 | "spreadsheetId": "[SPREAD_SHEET_ID]", 14 | "client_id": "[CLIENT_ID]", 15 | "client_secret": "[CLIENT_SECRET]", 16 | "refresh_token": "[REFRESH_TOKEN]" 17 | } 18 | } 19 | 20 | """ 21 | from pico_lte.core import PicoLTE 22 | from pico_lte.common import debug 23 | 24 | picoLTE = PicoLTE() 25 | 26 | debug.info("Getting data from the Google Sheets table...") 27 | result = picoLTE.google_sheets.get_data(sheet="Sheet1", data_range="A1:C2") 28 | debug.info("Result:", result) 29 | 30 | # Values can be accessed as a list with "response" key of "result" dictionary: values = result["response"] 31 | -------------------------------------------------------------------------------- /examples/thingspeak/mqtt_publish.py: -------------------------------------------------------------------------------- 1 | """ 2 | Example code for publishing data to ThingSpeak channel by using SDK funtions. 3 | 4 | Example Configuration 5 | --------------------- 6 | Create a config.json file in the root directory of the PicoLTE device. 7 | config.json file must include the following parameters for this example: 8 | 9 | config.json 10 | { 11 | "thingspeak": { 12 | "channel_id": "[YOUR_CHANNEL_ID]", 13 | "mqtts": { 14 | "client_id": "[DEVICE_MQTT_CLIENT_ID]", 15 | "username": "[DEVICE_MQTT_USERNAME]", 16 | "password": "[DEVICE_MQTT_PASSWORD]", 17 | "pub_topic": "[YOUR_MQTT_TOPIC]" 18 | } 19 | } 20 | } 21 | """ 22 | from pico_lte.core import PicoLTE 23 | from pico_lte.common import debug 24 | 25 | picoLTE = PicoLTE() 26 | 27 | payload = {"field1": 30, "field2": 40, "status": "PicoLTE_THINGSPEAK_EXAMPLE"} 28 | 29 | debug.info("Publishing data to ThingSpeak...") 30 | result = picoLTE.thingspeak.publish_message(payload) 31 | debug.info("Result:", result) 32 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ## [0.4.0] - 2024-01-18 4 | 5 | ### Added 6 | 7 | - Add MQTT examples. 8 | 9 | ### Changed 10 | 11 | - Update MQTT module. 12 | 13 | ## [0.3.0] - 2023-10-21 14 | 15 | ### Added 16 | 17 | - Add google sheets app. 18 | 19 | ### Changed 20 | 21 | - Update gps output format as decimal. 22 | 23 | 24 | ## [0.2.1] - 2023-09-13 25 | 26 | ### Added 27 | 28 | - Add code examples for apps. 29 | 30 | ### Changed 31 | 32 | - Improve HTTP fault catching. 33 | - Update Azure app document for simple usage. 34 | - Update README.md file for last changes and fix link redirects. 35 | 36 | ### Removed 37 | 38 | - Delete I2C class initialization. 39 | - Remove ULP example. 40 | 41 | ### Fixed 42 | 43 | - Fix response related error of Scriptr app. 44 | - Fix memory allocation error while using Azure app. 45 | 46 | [0.2.1]: https://github.com/sixfab/pico_lte_micropython-sdk/tree/0.2.1 47 | [0.3.0]: https://github.com/sixfab/pico_lte_micropython-sdk/tree/0.3.0 48 | [0.4.0]: https://github.com/sixfab/pico_lte_micropython-sdk/tree//0.4.0 49 | -------------------------------------------------------------------------------- /examples/__basic__/neopixel_led_rainbow_effect.py: -------------------------------------------------------------------------------- 1 | """ 2 | 3 | Example code for rainbow effect on NeoPixel LED. 4 | 5 | """ 6 | 7 | 8 | import machine 9 | import neopixel 10 | import utime 11 | 12 | NUM_LEDS = 8 13 | pin = machine.Pin(15) 14 | np = neopixel.NeoPixel(pin, NUM_LEDS) 15 | 16 | # Function for rainbow effect 17 | def rainbow_cycle(wait): 18 | for j in range(255): 19 | for i in range(NUM_LEDS): 20 | rc_index = (i * 256 // NUM_LEDS) + j 21 | np[i] = wheel(rc_index & 255) # Set LED color using wheel function 22 | np.write() 23 | utime.sleep_ms(wait) 24 | 25 | def wheel(pos): 26 | if pos < 85: 27 | return (255 - pos * 3, pos * 3, 0) # Red to Green transition 28 | elif pos < 170: 29 | pos -= 85 30 | return (0, 255 - pos * 3, pos * 3) # Green to Blue transition 31 | else: 32 | pos -= 170 33 | return (pos * 3, 0, 255 - pos * 3) # Blue to Red transition 34 | 35 | while True: 36 | rainbow_cycle(20) # Call the rainbow effect function with a delay of 20 milliseconds -------------------------------------------------------------------------------- /examples/__basic__/qwiic.py: -------------------------------------------------------------------------------- 1 | """ 2 | 3 | Example code for SparkFun TMP102 Qwiic digital temperature sensor. 4 | 5 | """ 6 | 7 | import machine 8 | import time 9 | 10 | # Establishing the I2C connection. '0' corresponds to the I2C driver. 11 | # The 'SDA' and 'SCL' parameters respectively specify the SDA and SCL pins. 12 | # The 'freq' parameter determines the I2C communication speed. 13 | 14 | i2c = machine.I2C(0, scl=machine.Pin(13), sda=machine.Pin(12), freq=100000) 15 | tmp102_address = 0x48 # TMP102 I2C address (default set to 0x48). 16 | 17 | # Temperature reading function 18 | def read_temperature(): 19 | data = i2c.readfrom(tmp102_address, 2) # Read two bytes of data 20 | raw_temp = (data[0] << 8) | data[1] # Get the raw data combined 21 | temperature = (raw_temp >> 4) * 0.0625 # Convert raw data to temperature value 22 | return temperature 23 | 24 | while True: 25 | temperature = read_temperature() 26 | print("Temperature: {:.2f} °C".format(temperature)) # Print the temperature value 27 | time.sleep(1) # Slow down the loop by waiting 1 second -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Sixfab Inc. 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /pico_lte/common.py: -------------------------------------------------------------------------------- 1 | """ 2 | Basic module for using purpose of provining temporary memory 3 | for sharing data by different modules. 4 | """ 5 | 6 | from pico_lte.utils.debug import Debug 7 | 8 | 9 | class StateCache: 10 | """Data class for storing state data""" 11 | 12 | states = {} 13 | last_response = None 14 | 15 | def add_cache(self, function_name): 16 | """Gets cache for #function_name or adds new cache with #function_name key""" 17 | self.states[function_name] = None 18 | 19 | def get_state(self, function_name): 20 | """Returns state of function_name""" 21 | return self.states.get(function_name) 22 | 23 | def set_state(self, function_name, state): 24 | """Sets state of function_name""" 25 | self.states[function_name] = state 26 | 27 | def get_last_response(self): 28 | """Returns last response""" 29 | return self.last_response 30 | 31 | def set_last_response(self, response): 32 | """Sets last response""" 33 | self.last_response = response 34 | 35 | 36 | config = {} 37 | cache = StateCache() 38 | debug = Debug() 39 | config["cache"] = cache 40 | -------------------------------------------------------------------------------- /examples/http/get.py: -------------------------------------------------------------------------------- 1 | """ 2 | Example code for performing GET request to a server with using HTTP. 3 | 4 | Example Configuration 5 | --------------------- 6 | Create a config.json file in the root directory of the PicoLTE device. 7 | config.json file must include the following parameters for this example: 8 | 9 | config.json 10 | { 11 | "https":{ 12 | "server":"[HTTP_SERVER]", 13 | "username":"[YOUR_HTTP_USERNAME]", 14 | "password":"[YOUR_HTTP_PASSWORD]" 15 | }, 16 | } 17 | """ 18 | 19 | import time 20 | from pico_lte.utils.status import Status 21 | from pico_lte.core import PicoLTE 22 | from pico_lte.common import debug 23 | 24 | picoLTE = PicoLTE() 25 | 26 | picoLTE.network.register_network() 27 | picoLTE.http.set_context_id() 28 | picoLTE.network.get_pdp_ready() 29 | picoLTE.http.set_server_url() 30 | 31 | 32 | debug.info("Sending a GET request.") 33 | 34 | result = picoLTE.http.get() 35 | debug.info(result) 36 | 37 | # Read the response after 5 seconds. 38 | time.sleep(5) 39 | result = picoLTE.http.read_response() 40 | debug.info(result) 41 | if result["status"] == Status.SUCCESS: 42 | debug.info("Get request succeeded.") 43 | -------------------------------------------------------------------------------- /pico_lte/modules/config.py: -------------------------------------------------------------------------------- 1 | """ 2 | Module for including extended configuration function of PicoLTE module. 3 | """ 4 | 5 | from pico_lte.common import config 6 | from pico_lte.utils.helpers import read_json_file 7 | 8 | 9 | class Config: 10 | """ 11 | Class for including extended configuration functions. 12 | """ 13 | 14 | def set_parameters(self, parameters): 15 | """ 16 | Function for setting parameters in config.json file. 17 | 18 | Parameters 19 | ---------- 20 | parameters : dict 21 | Dictionary with parameters. 22 | 23 | Returns 24 | ------- 25 | dict 26 | Result that includes "status" and "response" keys 27 | """ 28 | config["params"] = parameters 29 | 30 | def read_parameters_from_json_file(self, path): 31 | """ 32 | Function for reading parameters from json file. 33 | 34 | Parameters 35 | ---------- 36 | path : str 37 | Path to json file. 38 | 39 | Returns 40 | ------- 41 | parameters : dict 42 | Dictionary with parameters. 43 | """ 44 | parameters = read_json_file(path) 45 | config["params"] = parameters 46 | -------------------------------------------------------------------------------- /examples/http/put.py: -------------------------------------------------------------------------------- 1 | """ 2 | Example code for performing PUT request to a server with using HTTP. 3 | 4 | Example Configuration 5 | --------------------- 6 | Create a config.json file in the root directory of the PicoLTE device. 7 | config.json file must include the following parameters for this example: 8 | 9 | config.json 10 | { 11 | "https":{ 12 | "server":"[HTTP_SERVER]", 13 | "username":"[YOUR_HTTP_USERNAME]", 14 | "password":"[YOUR_HTTP_PASSWORD]" 15 | }, 16 | } 17 | """ 18 | 19 | import json 20 | import time 21 | from pico_lte.utils.status import Status 22 | from pico_lte.core import PicoLTE 23 | from pico_lte.common import debug 24 | 25 | picoLTE = PicoLTE() 26 | 27 | picoLTE.network.register_network() 28 | picoLTE.http.set_context_id() 29 | picoLTE.network.get_pdp_ready() 30 | picoLTE.http.set_server_url() 31 | 32 | debug.info("Sending a PUT request.") 33 | 34 | payload_dict = {"message": "PicoLTE HTTP Put Example"} 35 | payload_json = json.dumps(payload_dict) 36 | result = picoLTE.http.put(data=payload_json) 37 | debug.info(result) 38 | 39 | # Read the response after 5 seconds. 40 | time.sleep(5) 41 | result = picoLTE.http.read_response() 42 | if result["status"] == Status.SUCCESS: 43 | debug.info("Put request succeeded.") 44 | debug.info(result) 45 | -------------------------------------------------------------------------------- /examples/http/post.py: -------------------------------------------------------------------------------- 1 | """ 2 | Example code for performing POST request to a server with using HTTP. 3 | 4 | Example Configuration 5 | --------------------- 6 | Create a config.json file in the root directory of the PicoLTE device. 7 | config.json file must include the following parameters for this example: 8 | 9 | config.json 10 | { 11 | "https":{ 12 | "server":"[HTTP_SERVER]", 13 | "username":"[YOUR_HTTP_USERNAME]", 14 | "password":"[YOUR_HTTP_PASSWORD]" 15 | }, 16 | } 17 | """ 18 | 19 | import json 20 | import time 21 | from pico_lte.utils.status import Status 22 | from pico_lte.core import PicoLTE 23 | from pico_lte.common import debug 24 | 25 | picoLTE = PicoLTE() 26 | 27 | picoLTE.network.register_network() 28 | picoLTE.http.set_context_id() 29 | picoLTE.network.get_pdp_ready() 30 | picoLTE.http.set_server_url() 31 | 32 | debug.info("Sending a POST request.") 33 | 34 | payload_dict = {"message": "PicoLTE HTTP Post Example"} 35 | payload_json = json.dumps(payload_dict) 36 | result = picoLTE.http.post(data=payload_json) 37 | debug.info(result) 38 | 39 | # Read the response after 5 seconds. 40 | time.sleep(5) 41 | result = picoLTE.http.read_response() 42 | if result["status"] == Status.SUCCESS: 43 | debug.info("Post request succeeded.") 44 | debug.info(result) 45 | -------------------------------------------------------------------------------- /examples/azure/mqtt_subscribe.py: -------------------------------------------------------------------------------- 1 | """ 2 | Example code for subscribing topics from Azure IoT Hub by using MQTT. 3 | 4 | Example Configuration 5 | --------------------- 6 | Create a config.json file in the root directory of the PicoLTE device. 7 | config.json file must include the following parameters for this example: 8 | 9 | config.json 10 | { 11 | "azure":{ 12 | "hub_name": "[YOUR_AZURE_IOT_HUB_NAME]", 13 | "device_id": "[YOUR_DEVICE_ID]", 14 | "mqtts": { 15 | "sub_topics": [ 16 | ["[SUB_TOPIC/1]", [QOS]], 17 | ["[SUB_TOPIC/2]", [QOS]], 18 | ] 19 | } 20 | } 21 | } 22 | """ 23 | 24 | import time 25 | from pico_lte.core import PicoLTE 26 | from pico_lte.common import debug 27 | from pico_lte.utils.status import Status 28 | 29 | picoLTE = PicoLTE() 30 | 31 | debug.info("Subscribing to topics...") 32 | result = picoLTE.azure.subscribe_topics() 33 | debug.info(result) 34 | 35 | if result.get("status") == Status.SUCCESS: 36 | debug.info("Reading messages from subscribed topics...") 37 | # Check is there any data in subscribed topics 38 | # in each 5 seconds for 5 times 39 | for _ in range(0, 5): 40 | result = picoLTE.azure.read_messages() 41 | debug.info(result.get("messages")) 42 | time.sleep(5) 43 | -------------------------------------------------------------------------------- /examples/aws-iot/mqtt_subscribe.py: -------------------------------------------------------------------------------- 1 | """ 2 | Example code for subscribing MQTT topics on AWS IoT 3 | and reading data from them. 4 | 5 | Example Configuration 6 | --------------------- 7 | Create a config.json file in the root directory of the PicoLTE device. 8 | config.json file must include the following parameters for this example: 9 | 10 | config.json 11 | { 12 | "aws":{ 13 | "mqtts":{ 14 | "host":"[YOUR_AWSIOT_ENDPOINT]", 15 | "port":"[YOUR_AWSIOT_MQTT_PORT]", 16 | "sub_topics":[ 17 | "[YOUR_MQTT_TOPIC/1]", 18 | "[YOUR_MQTT_TOPIC/2]" 19 | ] 20 | } 21 | } 22 | } 23 | """ 24 | 25 | import time 26 | from pico_lte.core import PicoLTE 27 | from pico_lte.common import debug 28 | from pico_lte.utils.status import Status 29 | 30 | picoLTE = PicoLTE() 31 | 32 | debug.info("Subscribing to topics...") 33 | result = picoLTE.aws.subscribe_topics() 34 | debug.info(result) 35 | 36 | if result.get("status") == Status.SUCCESS: 37 | debug.info("Reading messages from subscribed topics...") 38 | # Check is there any data in subscribed topics 39 | # in each 5 seconds for 5 times 40 | for _ in range(0, 5): 41 | result = picoLTE.aws.read_messages() 42 | debug.info(result.get("messages")) 43 | time.sleep(5) 44 | -------------------------------------------------------------------------------- /pico_lte/modules/peripherals.py: -------------------------------------------------------------------------------- 1 | """ 2 | Module for incuding periheral hardware functions of PicoLTE module. 3 | """ 4 | 5 | from machine import Pin 6 | from neopixel import NeoPixel 7 | 8 | 9 | class Periph: 10 | """ 11 | Class for inculding periheral hardware functions of PicoLTE module. 12 | """ 13 | 14 | user_button = Pin(21, Pin.IN) 15 | user_led = Pin(22, Pin.OUT) 16 | pico_led = Pin("LED", Pin.OUT) 17 | neopixel = Pin(15, Pin.OUT) 18 | 19 | def __init__(self): 20 | """ 21 | Constructor for Periph class 22 | """ 23 | 24 | def read_user_button(self): 25 | """ 26 | Function for reading user button 27 | 28 | Returns 29 | ------- 30 | status : int 31 | User button status 32 | """ 33 | return self.user_button.value() 34 | 35 | def adjust_neopixel(self, red, green, blue): 36 | """ 37 | Function for adjusting neopixel color and brightness 38 | 39 | Parameters 40 | ---------- 41 | red : int 42 | Red color value (0-255) 43 | green : int 44 | Green color value (0-255) 45 | blue : int 46 | Blue color value (0-255) 47 | """ 48 | neopixel = NeoPixel(self.neopixel, 8) 49 | neopixel[0] = (red, green, blue) 50 | neopixel.write() 51 | -------------------------------------------------------------------------------- /examples/thingspeak/mqtt_subscribe.py: -------------------------------------------------------------------------------- 1 | """ 2 | Example code for subscribing topics for Thingspeak and 3 | recerving data from Thingspeak channel by using MQTT. 4 | 5 | Example Configuration 6 | --------------------- 7 | Create a config.json file in the root directory of the PicoLTE device. 8 | config.json file must include the following parameters for this example: 9 | 10 | config.json 11 | { 12 | "thingspeak": { 13 | "channel_id": "[YOUR_CHANNEL_ID]", 14 | "mqtts": { 15 | "client_id": "[DEVICE_MQTT_CLIENT_ID]", 16 | "username": "[DEVICE_MQTT_USERNAME]", 17 | "password": "[DEVICE_MQTT_PASSWORD]", 18 | "sub_topics": [ 19 | ["[YOUR_MQTT_TOPIC]", [QOS]] 20 | ] 21 | } 22 | } 23 | } 24 | """ 25 | import time 26 | from pico_lte.core import PicoLTE 27 | from pico_lte.common import debug 28 | from pico_lte.utils.status import Status 29 | 30 | picoLTE = PicoLTE() 31 | 32 | debug.info("Subscribing to topics...") 33 | result = picoLTE.thingspeak.subscribe_topics() 34 | debug.info("Result:", result) 35 | 36 | 37 | if result.get("status") == Status.SUCCESS: 38 | # Check is there any data in subscribed topics 39 | # in each 5 seconds for 5 times 40 | debug.info("Reading messages from subscribed topics...") 41 | for _ in range(0, 5): 42 | result = picoLTE.thingspeak.read_messages() 43 | debug.info(result.get("messages")) 44 | time.sleep(5) 45 | -------------------------------------------------------------------------------- /examples/mqtt/publish.py: -------------------------------------------------------------------------------- 1 | """ 2 | Example code for publishing data to an MQTT broker. 3 | 4 | Example Configuration 5 | --------------------- 6 | Create a config.json file in the root directory of the PicoLTE device. 7 | config.json file must include the following parameters for this example: 8 | 9 | config.json 10 | { 11 | "mqtts":{ 12 | "host":"[HOST_ADDRESS]", 13 | "port": [PORT_NUMBER], 14 | "pub_qos": [QoS], 15 | "client_id": "[CLIENT_ID]", 16 | "username":"[MQTT_USERNAME]", 17 | "password":"[MQTT_PASSWORD]" 18 | }, 19 | } 20 | 21 | - [HOST_ADDRESS] should be an IP address or a domain name (without "mqtt://"). 22 | - [QoS] is the quality of service level for the message and can be 0, 1 or 2. Default value is 1. 23 | - "client_id", "username" and "password" are optional. If your MQTT broker does not require authentication, you can skip these parameters. 24 | """ 25 | 26 | from pico_lte.utils.status import Status 27 | from pico_lte.core import PicoLTE 28 | from pico_lte.common import debug 29 | 30 | picoLTE = PicoLTE() 31 | 32 | picoLTE.network.register_network() 33 | picoLTE.network.get_pdp_ready() 34 | picoLTE.mqtt.open_connection() 35 | picoLTE.mqtt.connect_broker() 36 | 37 | debug.info("Publishing a message.") 38 | 39 | # PAYLOAD and TOPIC have to be in string format. 40 | PAYLOAD = "[PAYLOAD_MESSAGE]" 41 | TOPIC = "[TOPIC_NAME]" 42 | 43 | # Publish the message to the topic. 44 | result = picoLTE.mqtt.publish_message(PAYLOAD, TOPIC) 45 | debug.info("Result:", result) 46 | 47 | if result["status"] == Status.SUCCESS: 48 | debug.info("Publish succeeded.") 49 | -------------------------------------------------------------------------------- /tests/test_modules_peripherals.py: -------------------------------------------------------------------------------- 1 | """ 2 | Test module for the modules.peripheral module. 3 | """ 4 | import pytest 5 | from machine import Pin 6 | 7 | from pico_lte.modules.peripherals import Periph 8 | 9 | 10 | class TestPeriph: 11 | """ 12 | Test class for Periph. 13 | """ 14 | 15 | @pytest.fixture 16 | def periph(self): 17 | """This fixture returns a Periph instance.""" 18 | return Periph() 19 | 20 | def test_pin_set_up(self, periph): 21 | """This method tests if the pin directions and their GPIO numbers are correct. 22 | "num" and "dir" is mock-attributes which is defined in conftest.py file. 23 | """ 24 | # Test User Button Pin 25 | assert isinstance(periph.user_button, Pin) 26 | assert periph.user_button.pin_num == 21 27 | assert periph.user_button.pin_dir == Pin.IN 28 | # Test User LED Pin 29 | assert isinstance(periph.user_led, Pin) 30 | assert periph.user_led.pin_num == 22 31 | assert periph.user_led.pin_dir == Pin.OUT 32 | # Test Neopixel Pin 33 | assert isinstance(periph.neopixel, Pin) 34 | assert periph.neopixel.pin_num == 15 35 | assert periph.neopixel.pin_dir == Pin.OUT 36 | 37 | @pytest.mark.parametrize("pin_value", [0, 1]) 38 | def test_read_user_button(self, mocker, periph, pin_value): 39 | """Test the read_user_button() method by mocking the Pin.value method.""" 40 | mocker.patch("pico_lte.modules.peripherals.Pin.value", return_value=pin_value) 41 | assert periph.read_user_button() == pin_value 42 | 43 | def test_adjust_neopixel(self): 44 | """No need since its a third-party library.""" 45 | assert True 46 | -------------------------------------------------------------------------------- /examples/mqtt/subscribe.py: -------------------------------------------------------------------------------- 1 | """ 2 | Example code for subcribing to topic(s) of an MQTT broker. 3 | 4 | Example Configuration 5 | --------------------- 6 | Create a config.json file in the root directory of the PicoLTE device. 7 | config.json file must include the following parameters for this example: 8 | 9 | config.json 10 | { 11 | "mqtts":{ 12 | "host":"[HOST_ADDRESS]", 13 | "port": [PORT_NUMBER], 14 | "client_id": "[CLIENT_ID]", 15 | "username":"[MQTT_USERNAME]", 16 | "password":"[MQTT_PASSWORD]", 17 | "sub_topics": [ 18 | ["[YOUR_MQTT_TOPIC_1]", [QoS]], 19 | ["[YOUR_MQTT_TOPIC_2]", [QoS]], 20 | ... 21 | ] 22 | }, 23 | } 24 | 25 | - [HOST_ADDRESS] could be an IP address or a domain name (without "mqtt://"). 26 | - "client_id", "username" and "password" are optional. If your MQTT broker does not require authentication, you can skip these parameters. 27 | - [QoS] is the quality of service level for the message and can be 0, 1 or 2. 28 | """ 29 | 30 | import time 31 | from pico_lte.utils.status import Status 32 | from pico_lte.core import PicoLTE 33 | from pico_lte.common import debug 34 | 35 | picoLTE = PicoLTE() 36 | 37 | picoLTE.network.register_network() 38 | picoLTE.network.get_pdp_ready() 39 | picoLTE.mqtt.open_connection() 40 | picoLTE.mqtt.connect_broker() 41 | 42 | debug.info("Subscribing to topics...") 43 | result = picoLTE.mqtt.subscribe_topics() 44 | debug.info("Result:", result) 45 | 46 | if result["status"] == Status.SUCCESS: 47 | # Check is there any data in subscribed topics in each 5 seconds for 5 times 48 | debug.info("Reading messages from subscribed topics...") 49 | for _ in range(0, 5): 50 | result = picoLTE.mqtt.read_messages() 51 | debug.info(result["messages"]) 52 | time.sleep(5) 53 | -------------------------------------------------------------------------------- /tests/test_modules_config.py: -------------------------------------------------------------------------------- 1 | """ 2 | Test module for the modules.config module. 3 | """ 4 | 5 | import pytest 6 | 7 | from pico_lte.modules.config import Config 8 | from pico_lte.common import config 9 | 10 | 11 | class TestConfig: 12 | """ 13 | Test class for Config. 14 | """ 15 | 16 | @pytest.fixture 17 | def config_instance(self): 18 | """This fixture returns a Config instance.""" 19 | return Config() 20 | 21 | @pytest.fixture 22 | def example_config_params(self): 23 | """This fixture returns an example Config.params.""" 24 | return { 25 | "mqtts": { 26 | "host": "test_global_mqtts_host", 27 | "port": "test_global_mqtts_port", 28 | }, 29 | "https": { 30 | "endpoint": "test_global_http_endpoint", 31 | "topic": "test_global_http_topic", 32 | }, 33 | "app_service": { 34 | "mqtts": { 35 | "host": "test_app_mqtts_host", 36 | "port": "test_app_mqtts_port", 37 | }, 38 | }, 39 | } 40 | 41 | def test_set_parameters(self, config_instance, example_config_params): 42 | """This method tests the set_parameters() method.""" 43 | config_instance.set_parameters(example_config_params) 44 | assert config["params"] == example_config_params 45 | 46 | def test_read_parameters_from_json_file(self, mocker, config_instance, example_config_params): 47 | """This method tests the read_parameters_from_json_file() method.""" 48 | mocker.patch("pico_lte.modules.config.read_json_file", return_value=example_config_params) 49 | 50 | config_instance.read_parameters_from_json_file("some_path.json") 51 | assert config["params"] == example_config_params 52 | -------------------------------------------------------------------------------- /examples/http/get_with_custom_header.py: -------------------------------------------------------------------------------- 1 | """ 2 | Example code for performing HTTP request to a server with using custom headers. 3 | 4 | Example Configuration 5 | --------------------- 6 | Create a config.json file in the root directory of the PicoLTE device. 7 | config.json file must include the following parameters for this example: 8 | 9 | config.json 10 | { 11 | "https":{ 12 | "server":"[HTTP_SERVER]", 13 | "username":"[YOUR_HTTP_USERNAME]", 14 | "password":"[YOUR_HTTP_PASSWORD]" 15 | }, 16 | } 17 | """ 18 | 19 | import json 20 | import time 21 | from pico_lte.utils.status import Status 22 | from pico_lte.core import PicoLTE 23 | from pico_lte.common import debug 24 | from pico_lte.utils.helpers import get_parameter 25 | 26 | # Prepare HTTP connection. 27 | picoLTE = PicoLTE() 28 | picoLTE.network.register_network() 29 | picoLTE.http.set_context_id() 30 | picoLTE.network.get_pdp_ready() 31 | picoLTE.http.set_server_url() 32 | 33 | # Get URL from the config.json. 34 | url = get_parameter(["https", "server"]) 35 | 36 | if url: 37 | url = url.replace("https://", "").replace("http://", "") 38 | index = url.find("/") if url.find("/") != -1 else len(url) 39 | host = url[:index] 40 | query = url[index:] 41 | else: 42 | debug.error("Missing argument: server") 43 | 44 | 45 | # Custom header 46 | HEADER = "\n".join( 47 | [ 48 | f"GET {query} HTTP/1.1", 49 | f"Host: {host}", 50 | "Custom-Header-Name: Custom-Data", 51 | "Content-Type: application/json", 52 | "Content-Length: 0\n", 53 | "\n\n", 54 | ] 55 | ) 56 | 57 | debug.info("Sending a GET request with custom header...") 58 | result = picoLTE.http.get(HEADER, header_mode=1) 59 | debug.info("Result:", result) 60 | 61 | time.sleep(5) 62 | 63 | result = picoLTE.http.read_response() 64 | debug.info(result) 65 | if result["status"] == Status.SUCCESS: 66 | debug.info("GET request succeeded.") 67 | -------------------------------------------------------------------------------- /examples/gps/send_location_to_telegram.py: -------------------------------------------------------------------------------- 1 | """ 2 | Example code for sending latitude longitude data which are received from GPS 3 | to the telegram channel. The data is sent to the telegram channel using HTTP 4 | POST method. 5 | 6 | Example Configuration 7 | --------------------- 8 | Create a config.json file in the root directory of the PicoLTE device. 9 | config.json file must include the following parameters for this example: 10 | 11 | config.json 12 | { 13 | "telegram": { 14 | "token": "[YOUR_BOT_TOKEN_ID]", 15 | "chat_id": "[YOUR_GROUP_CHAT_ID]" 16 | } 17 | } 18 | """ 19 | 20 | import time 21 | 22 | from pico_lte.core import PicoLTE 23 | from pico_lte.common import debug 24 | from pico_lte.utils.status import Status 25 | 26 | PERIOD = 30 # seconds 27 | fix = False 28 | 29 | picoLTE = PicoLTE() 30 | 31 | debug.info("GPS Example") 32 | 33 | while True: 34 | # First go to GNSS prior mode and turn on GPS. 35 | picoLTE.gps.set_priority(0) 36 | time.sleep(3) 37 | picoLTE.gps.turn_on() 38 | debug.info("Trying to fix GPS...") 39 | 40 | for _ in range(0, 45): 41 | result = picoLTE.gps.get_location() 42 | debug.info(result) 43 | 44 | if result["status"] == Status.SUCCESS: 45 | debug.info("GPS Fixed. Getting location data...") 46 | 47 | loc = result.get("value") 48 | debug.info("Lat-Lon:", loc) 49 | loc_message = ",".join(word for word in loc) 50 | 51 | fix = True 52 | break 53 | time.sleep(2) # 45*2 = 90 seconds timeout for GPS fix. 54 | 55 | if fix: 56 | # Go to WWAN prior mode and turn on GPS. 57 | picoLTE.gps.set_priority(1) 58 | picoLTE.gps.turn_off() 59 | 60 | debug.info("Sending message to telegram channel...") 61 | result = picoLTE.telegram.send_message(loc_message) 62 | debug.info(result) 63 | 64 | if result["status"] == Status.SUCCESS: 65 | debug.info("Message sent successfully.") 66 | fix = False 67 | 68 | time.sleep(PERIOD) # [PERIOD] seconds between each request. 69 | -------------------------------------------------------------------------------- /examples/http/post_with_custom_header.py: -------------------------------------------------------------------------------- 1 | """ 2 | Example code for performing HTTP request to a server with using custom headers. 3 | 4 | Example Configuration 5 | --------------------- 6 | Create a config.json file in the root directory of the PicoLTE device. 7 | config.json file must include the following parameters for this example: 8 | 9 | config.json 10 | { 11 | "https":{ 12 | "server":"[HTTP_SERVER]", 13 | "username":"[YOUR_HTTP_USERNAME]", 14 | "password":"[YOUR_HTTP_PASSWORD]" 15 | }, 16 | } 17 | """ 18 | 19 | import json 20 | import time 21 | from pico_lte.utils.status import Status 22 | from pico_lte.core import PicoLTE 23 | from pico_lte.common import debug 24 | from pico_lte.utils.helpers import get_parameter 25 | 26 | # Prepare HTTP connection. 27 | picoLTE = PicoLTE() 28 | picoLTE.network.register_network() 29 | picoLTE.http.set_context_id() 30 | picoLTE.network.get_pdp_ready() 31 | picoLTE.http.set_server_url() 32 | 33 | # Get URL from the config.json. 34 | url = get_parameter(["https", "server"]) 35 | 36 | if url: 37 | url = url.replace("https://", "").replace("http://", "") 38 | index = url.find("/") if url.find("/") != -1 else len(url) 39 | host = url[:index] 40 | query = url[index:] 41 | else: 42 | debug.error("Missing argument: server") 43 | 44 | # The messages that will be sent. 45 | DATA_TO_POST = {"message": "PicoLTE HTTP POST Example with Custom Header"} 46 | payload = json.dumps(DATA_TO_POST) 47 | 48 | # Custom header 49 | HEADER = "\n".join( 50 | [ 51 | f"POST /{query} HTTP/1.1", 52 | f"Host: {host}", 53 | "Custom-Header-Name: Custom-Data", 54 | "Content-Type: application/json", 55 | f"Content-Length: {len(payload)+1}", 56 | "\n\n", 57 | ] 58 | ) 59 | 60 | debug.info("Sending a POST request with custom header...") 61 | result = picoLTE.http.post(data=HEADER + payload, header_mode=1) 62 | debug.info("Result:", result) 63 | 64 | time.sleep(5) 65 | 66 | result = picoLTE.http.read_response() 67 | debug.info(result) 68 | if result["status"] == Status.SUCCESS: 69 | debug.info("POST request succeeded.") 70 | -------------------------------------------------------------------------------- /pico_lte/modules/file.py: -------------------------------------------------------------------------------- 1 | """ 2 | Module for including file functions of PicoLTE module. 3 | """ 4 | 5 | from pico_lte.utils.status import Status 6 | 7 | 8 | class File: 9 | """ 10 | Class for inculding functions of file operations of PicoLTE module. 11 | """ 12 | 13 | CTRL_Z = "\x1A" 14 | 15 | def __init__(self, atcom): 16 | """ 17 | Initialization of the class. 18 | """ 19 | self.atcom = atcom 20 | 21 | def get_file_list(self, path="*"): 22 | """ 23 | Function for getting file list 24 | 25 | Parameters 26 | ---------- 27 | path : str, default: "*" 28 | Path to the directory 29 | 30 | Returns 31 | ------- 32 | dict 33 | Result that includes "status" and "response" keys 34 | """ 35 | command = f'AT+QFLST="{path}"' 36 | return self.atcom.send_at_comm(command) 37 | 38 | def delete_file_from_modem(self, file_name): 39 | """ 40 | Function for deleting file from modem UFS storage 41 | 42 | Parameters 43 | ---------- 44 | file_path : str 45 | Path to the file 46 | 47 | Returns 48 | ------- 49 | dict 50 | Result that includes "status" and "response" keys 51 | """ 52 | command = f'AT+QFDEL="{file_name}"' 53 | return self.atcom.send_at_comm(command) 54 | 55 | def upload_file_to_modem(self, filename, file, timeout=5000): 56 | """ 57 | Function for uploading file to modem 58 | 59 | Parameters 60 | ---------- 61 | file : str 62 | Path to the file 63 | timeout : int, default: 5000 64 | Timeout for the command 65 | 66 | Returns 67 | ------- 68 | dict 69 | Result that includes "status" and "response" keys 70 | """ 71 | len_file = len(file) 72 | command = f'AT+QFUPL="{filename}",{len_file},{timeout}' 73 | result = self.atcom.send_at_comm(command, "CONNECT", urc=True) 74 | 75 | if result["status"] == Status.SUCCESS: 76 | self.atcom.send_at_comm_once(file) # send ca cert 77 | return self.atcom.send_at_comm(self.CTRL_Z) # send end char -> CTRL_Z 78 | return result 79 | -------------------------------------------------------------------------------- /examples/gps/send_location_to_server.py: -------------------------------------------------------------------------------- 1 | """ 2 | Example code for publishing location data which are received from GPS to the 3 | any test server. The data is sent to the server using HTTP POST method. webhook.site 4 | test server is used for this example. 5 | 6 | Example Configuration 7 | --------------------- 8 | Create a config.json file in the root directory of the PicoLTE device. 9 | config.json file must include the following parameters for this example: 10 | 11 | config.json 12 | { 13 | "https":{ 14 | "server": "[YOUR_SERVER_URL]", 15 | "username": "[YOUR_USERNAME]", 16 | "password": "[YOUR_PASSWORD]" 17 | } 18 | } 19 | """ 20 | 21 | import time 22 | 23 | from pico_lte.core import PicoLTE 24 | from pico_lte.common import debug 25 | from pico_lte.utils.status import Status 26 | 27 | 28 | fix = False 29 | picoLTE = PicoLTE() 30 | 31 | debug.info("GPS Example") 32 | picoLTE.peripherals.adjust_neopixel(255, 0, 0) 33 | 34 | while True: 35 | # First go to GNSS prior mode and turn on GPS. 36 | picoLTE.gps.set_priority(0) 37 | time.sleep(3) 38 | picoLTE.gps.turn_on() 39 | debug.info("Trying to fix GPS...") 40 | 41 | for _ in range(0, 45): 42 | result = picoLTE.gps.get_location() 43 | debug.info(result) 44 | 45 | if result["status"] == Status.SUCCESS: 46 | debug.debug("GPS Fixed. Getting location data...") 47 | 48 | loc = result.get("value") 49 | debug.info("Lat-Lon:", loc) 50 | loc_message = ",".join(word for word in loc) 51 | 52 | fix = True 53 | break 54 | time.sleep(2) # 45*2 = 90 seconds timeout for GPS fix. 55 | 56 | if fix: 57 | # Go to WWAN prior mode and turn on GPS. 58 | picoLTE.gps.set_priority(1) 59 | picoLTE.gps.turn_off() 60 | 61 | debug.info("Sending message to the server...") 62 | picoLTE.network.register_network() 63 | picoLTE.http.set_context_id() 64 | picoLTE.network.get_pdp_ready() 65 | picoLTE.http.set_server_url() 66 | 67 | result = picoLTE.http.post(data=loc_message) 68 | debug.info(result) 69 | 70 | if result["status"] == Status.SUCCESS: 71 | debug.info("Message sent successfully.") 72 | fix = False 73 | 74 | time.sleep(30) # 30 seconds between each request. 75 | -------------------------------------------------------------------------------- /tests/conftest.py: -------------------------------------------------------------------------------- 1 | import os 2 | import errno 3 | import pytest 4 | 5 | 6 | def write_file(file_path, data, file_type="t"): 7 | """This function creates a file with data.""" 8 | try: 9 | with open(file_path, "w" + file_type, encoding="utf-8") as file: 10 | file.write(data) 11 | except OSError: 12 | return None 13 | else: 14 | return data 15 | 16 | 17 | def remove_file(file_path): 18 | """This function deletes a file in the file-system if exists.""" 19 | try: 20 | os.remove(file_path) 21 | except OSError as error: 22 | if error.errno != errno.ENOENT: 23 | raise error 24 | 25 | 26 | MOCK_MACHINE_PY = """ 27 | class UART: 28 | def __init__(self, *args, **kwargs): 29 | pass 30 | 31 | def write(self): 32 | pass 33 | 34 | def any(self): 35 | pass 36 | 37 | def read(self): 38 | pass 39 | 40 | 41 | class Pin: 42 | 43 | OUT = 0 44 | IN = 1 45 | 46 | def __init__(self, pin_id=None, pin_dir=None, *args, **kwargs): 47 | self.pin_num = pin_id 48 | self.pin_dir = pin_dir 49 | 50 | def value(*args, **kwargs): 51 | pass 52 | 53 | def value(self, *args, **kwargs): 54 | pass 55 | 56 | 57 | class I2C: 58 | def __init__(self, *args, **kwargs): 59 | pass 60 | 61 | def scan(self, *args, **kwargs): 62 | pass 63 | 64 | class ADC: 65 | def __init__(self, *args, **kwargs): 66 | pass 67 | """ 68 | 69 | MOCK_NEOPIXEL_PY = """ 70 | class NeoPixel: 71 | 72 | def __init__(self, *args, **kwargs): 73 | pass 74 | """ 75 | 76 | MOCK_UBINASCII_PY = "" 77 | 78 | 79 | def prepare_test_enviroment(): 80 | """ 81 | Function for preparing test enviroment 82 | """ 83 | write_file("machine.py", MOCK_MACHINE_PY) 84 | write_file("neopixel.py", MOCK_NEOPIXEL_PY) 85 | write_file("ubinascii.py", MOCK_UBINASCII_PY) 86 | 87 | 88 | @pytest.hookimpl() 89 | def pytest_sessionstart(session): 90 | """This method auto-runs each time tests are started.""" 91 | print("Test enviroment preparing...") 92 | prepare_test_enviroment() 93 | 94 | 95 | @pytest.hookimpl() 96 | def pytest_sessionfinish(session): 97 | """This method auto-runs each time tests are ended.""" 98 | print("Test enviroment cleaning...") 99 | remove_file("machine.py") 100 | remove_file("neopixel.py") 101 | remove_file("ubinascii.py") 102 | -------------------------------------------------------------------------------- /tools/run.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # This script is used to run a MicroPython code on the PicoLTE device. 3 | 4 | PROJECT_DIR=$(pwd) 5 | PYBOARD_LOC="$PROJECT_DIR/tools/pyboard.py" 6 | 7 | find_os_of_the_user() { 8 | if [[ "$OSTYPE" == "linux-gnu"* ]]; then 9 | OS="linux" 10 | elif [[ "$OSTYPE" == "darwin"* ]]; then 11 | OS="macos" 12 | else 13 | echo "- Unknown OS detected. Exiting!" 14 | exit 1 15 | fi 16 | } 17 | 18 | find_the_port_of_micropython_from_os() { 19 | # Do nothing if PORT is already set. 20 | if [ -n "$PORT" ]; then 21 | return 22 | fi 23 | 24 | if [ "$OS" == "macos" ]; then 25 | PORT=$(ls /dev/tty.usbmodem*) 26 | elif [ "$OS" == "linux" ]; then 27 | PORT=$(ls /dev/ttyACM*) 28 | fi 29 | 30 | # Select the first port if there are multiple ports. 31 | PORT=$(echo $PORT | cut -d ' ' -f 1) 32 | 33 | echo "- The port of the PicoLTE device is $PORT." 34 | } 35 | 36 | check_if_pyboard_tool_is_installed() { 37 | # This function checks if the pyboard.py available 38 | # in the same directory as this script. 39 | if [ ! -f $PYBOARD_LOC ]; then 40 | echo "- pyboard.py is not found in the same directory as this script." 41 | echo "- Please download it from GitHub." 42 | echo "https://github.com/micropython/micropython/blob/master/tools/pyboard.py" 43 | exit 1 44 | fi 45 | 46 | # Check if python is installed. 47 | if ! command -v python3 &> /dev/null 48 | then 49 | echo "- Python3 is not installed." 50 | exit 1 51 | fi 52 | } 53 | 54 | run_the_code() { 55 | # Check if the code is provided. 56 | if [ -z "$1" ]; then 57 | echo "- Please provide the code to run." 58 | exit 1 59 | fi 60 | 61 | # Check if the code is a file. 62 | if [ ! -f "$1" ]; then 63 | echo "- The code is not a file." 64 | exit 1 65 | fi 66 | 67 | # Check if the code is a Python file. 68 | if [[ "$1" != *.py ]]; then 69 | echo "- The code is not a Python file." 70 | exit 1 71 | fi 72 | 73 | # Print some blank lines. 74 | echo "- Running the code: $1" 75 | echo "" 76 | 77 | # Run the code. 78 | python3 $PYBOARD_LOC --device $PORT $1 79 | } 80 | 81 | # Run the script. 82 | if [ ! -f "$PROJECT_DIR/tools/run.sh" ]; then 83 | echo "- This script must be run from the project root directory." 84 | exit 1 85 | fi 86 | 87 | find_os_of_the_user 88 | check_if_pyboard_tool_is_installed 89 | find_the_port_of_micropython_from_os 90 | run_the_code $1 -------------------------------------------------------------------------------- /pico_lte/core.py: -------------------------------------------------------------------------------- 1 | """ 2 | Module for ease to use of cellular PicoLTE. This module includes required functions 3 | for working with cellular modem without need of AT command knowledge. 4 | """ 5 | from pico_lte.common import config 6 | 7 | from pico_lte.utils.helpers import read_json_file 8 | from pico_lte.utils.atcom import ATCom 9 | 10 | from pico_lte.modules.base import Base 11 | from pico_lte.modules.auth import Auth 12 | from pico_lte.modules.file import File 13 | from pico_lte.modules.http import HTTP 14 | from pico_lte.modules.mqtt import MQTT 15 | from pico_lte.modules.network import Network 16 | from pico_lte.modules.peripherals import Periph 17 | from pico_lte.modules.ssl import SSL 18 | from pico_lte.modules.gps import GPS 19 | 20 | from pico_lte.apps.aws import AWS 21 | from pico_lte.apps.slack import Slack 22 | from pico_lte.apps.telegram import Telegram 23 | from pico_lte.apps.thingspeak import ThingSpeak 24 | from pico_lte.apps.azure import Azure 25 | from pico_lte.apps.scriptr import Scriptr 26 | from pico_lte.apps.google_sheets import GoogleSheets 27 | 28 | 29 | class PicoLTE: 30 | """ 31 | PicoLTE modem class that contains all functions for working with cellular modem 32 | """ 33 | 34 | def __init__(self): 35 | """ 36 | Initialize modem class 37 | """ 38 | config["params"] = read_json_file("config.json") 39 | 40 | self.peripherals = Periph() 41 | self.atcom = ATCom() 42 | 43 | self.base = Base(self.atcom) 44 | self.file = File(self.atcom) 45 | self.auth = Auth(self.atcom, self.file) 46 | self.network = Network(self.atcom, self.base) 47 | self.ssl = SSL(self.atcom) 48 | self.http = HTTP(self.atcom) 49 | self.mqtt = MQTT(self.atcom) 50 | self.gps = GPS(self.atcom) 51 | 52 | self.aws = AWS( 53 | self.base, self.auth, self.network, self.ssl, self.mqtt, self.http 54 | ) 55 | self.telegram = Telegram(self.base, self.network, self.http) 56 | self.thingspeak = ThingSpeak(self.base, self.network, self.mqtt) 57 | self.slack = Slack(self.base, self.network, self.http) 58 | self.azure = Azure( 59 | self.base, self.auth, self.network, self.ssl, self.mqtt, self.http 60 | ) 61 | self.scriptr = Scriptr(self.base, self.network, self.http) 62 | self.google_sheets = GoogleSheets(self.base, self.network, self.http) 63 | 64 | # Power up modem 65 | if self.base.power_status() != 0: 66 | self.base.power_on() 67 | self.base.wait_until_status_on() 68 | self.base.wait_until_modem_ready_to_communicate() 69 | self.base.set_echo_off() 70 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | Thank you so much for your interest in contributing. All types of contributions are encouraged and valued. All you need to do is read the guidelines before making any chances! 4 | 5 | ## Request Support/Feature or Report an Error 6 | If you have a problem with the SDK or you want a well-thought-out feature, please open an issue and provide all the information regarding your problem or idea. You don't need to label your own issue, the code maintainers will handle this job. If it is a problem, we're going to solve it as soon as possible. 7 | 8 | ## Code Contribution 9 | Before implementing your own solution or solving an issue, please read the following guidelines to have a better-looking and unified code: 10 | * **Always follow PEP-8**. The codes without proper styling is not going to be merged. 11 | * **Check your code with a linter before creating a pull request**. We're strongly recommending you to use a Python linter software to check your code before submitting it. 12 | * **Provide meaningful and standardized commit messages**. We have no rush to think about commit messages, since the project collaborators are all individual people, we have to stay on a standardized system. Please only use `feat`, `refactor`, `fix`, `docs`, and `chore`. 13 | * **Make lots of commits**! You don't need to worry about commit count of your pull request since we use squash and merge method. It is better to have more and meaningfully small commits instead of big breaking changes. 14 | * **Check the code on your PicoLTE device**. Do not forget that we are working on embedded devices. You may think that you've done a little change, but in the world of tiny things, the little changes can go giant! Run your code on a PicoLTE, and report the outputs or status on your pull request. 15 | * **Follow the commit message standards on pull requests' titles**. Clear titles as important as clear commits. 16 | * **Provide necessary information on your pull request**. "Why did you do this change, is it a feature, or a bug-fix? How does it change the previous code? Do we need to refactor something else? What are the successful results?" Don't let us have questions in our minds. Provide a well-written documentation about your change. 17 | 18 | ## Creating a Service as Application 19 | We encourage you to create your own applications to the services if they are not available built-in since the idea behind this project is to make easy connections on many services with using cellular. To follow this mission, we need to support more services. 20 | 21 | A small and simple guide given to you, developers, [in this file](examples/__sdk__/create_your_own_method.py) about how to create your own application using state manager model. -------------------------------------------------------------------------------- /pico_lte/utils/debug.py: -------------------------------------------------------------------------------- 1 | """ 2 | Module for debugging purposes. 3 | """ 4 | 5 | from machine import Pin, UART 6 | 7 | 8 | class DebugChannel: 9 | USBC = 0 10 | UART = 1 11 | 12 | 13 | class DebugLevel: 14 | DEBUG = 0 15 | INFO = 1 16 | WARNING = 2 17 | ERROR = 3 18 | CRITICAL = 4 19 | FOCUS = -1 20 | 21 | 22 | class Debug: 23 | """Class for debugging purposes.""" 24 | 25 | def __init__(self, enabled=True, channel=DebugChannel.USBC, level=DebugLevel.INFO): 26 | """Initializes the debug class.""" 27 | self.uart1 = UART(1, baudrate=115200, tx=Pin(4), rx=Pin(5)) 28 | self.debug_enabled = enabled 29 | self.debug_channel = channel 30 | self.debug_level = level 31 | 32 | def set_channel(self, channel): 33 | """Sets the debug channel.""" 34 | self.debug_channel = channel 35 | 36 | def set_level(self, level): 37 | """Sets the debug level.""" 38 | self.debug_level = level 39 | 40 | def enable(self, enabled): 41 | """Sets the debug enabled.""" 42 | self.debug_enabled = enabled 43 | 44 | def print(self, *args): 45 | """debug.prints the given arguments to the debug channel.""" 46 | if self.debug_enabled: 47 | if self.debug_channel == DebugChannel.USBC: 48 | print(*args) 49 | elif self.debug_channel == DebugChannel.UART: 50 | for arg in args: 51 | self.uart1.write(str(arg)) 52 | self.uart1.write(" ") 53 | self.uart1.write("\n") 54 | 55 | def debug(self, *args): 56 | """Function for DEBUG level messages.""" 57 | if self.debug_enabled and self.debug_level <= DebugLevel.DEBUG: 58 | self.print("DEBUG:", *args) 59 | 60 | def info(self, *args): 61 | """Function for INFO level messages.""" 62 | if self.debug_enabled and self.debug_level <= DebugLevel.INFO: 63 | self.print("INFO:", *args) 64 | 65 | def warning(self, *args): 66 | """Function for WARNING level messages.""" 67 | if self.debug_enabled and self.debug_level <= DebugLevel.WARNING: 68 | self.print("WARNING:", *args) 69 | 70 | def error(self, *args): 71 | """Function for ERROR level messages.""" 72 | if self.debug_enabled and self.debug_level <= DebugLevel.ERROR: 73 | self.print("ERROR:", *args) 74 | 75 | def critical(self, *args): 76 | """Function for CRITICAL level messages.""" 77 | if self.debug_enabled and self.debug_level <= DebugLevel.CRITICAL: 78 | self.print("CRITICAL:", *args) 79 | 80 | def focus(self, *args): 81 | """Function for FOCUSSED level messages.""" 82 | if self.debug_enabled and self.debug_level == DebugLevel.FOCUS: 83 | self.print("FOCUS:", *args) 84 | -------------------------------------------------------------------------------- /tests/test_modules_file.py: -------------------------------------------------------------------------------- 1 | """ 2 | Test module for the modules.file module. 3 | """ 4 | 5 | import pytest 6 | 7 | from pico_lte.modules.file import File 8 | from pico_lte.utils.atcom import ATCom 9 | from pico_lte.utils.status import Status 10 | 11 | 12 | class TestFile: 13 | """ 14 | Test class for File. 15 | """ 16 | 17 | @pytest.fixture 18 | def file(self): 19 | """This fixtures returns a File instance.""" 20 | atcom = ATCom() 21 | return File(atcom) 22 | 23 | def test_constructor(self, file): 24 | """This method checks the initialization of the object.""" 25 | assert isinstance(file.atcom, ATCom) 26 | assert file.CTRL_Z == "\x1A" 27 | 28 | def test_get_file_list(self, mocker, file): 29 | """This method checks the get_file_list() with mocked ATCom responses.""" 30 | mocked_return = {"status": Status.SUCCESS, "response": ["some", "response"]} 31 | mocking = mocker.patch("pico_lte.utils.atcom.ATCom.send_at_comm", return_value=mocked_return) 32 | 33 | result = file.get_file_list() 34 | 35 | mocking.assert_called_once_with('AT+QFLST="*"') 36 | assert result == mocked_return 37 | 38 | def test_delete_file_from_modem(self, mocker, file): 39 | """This method checks the delete_file_from_modem() with mocked ATCom responses.""" 40 | mocked_return = {"status": Status.SUCCESS, "response": ["some", "response"]} 41 | mocking = mocker.patch("pico_lte.utils.atcom.ATCom.send_at_comm", return_value=mocked_return) 42 | 43 | result = file.delete_file_from_modem("file.pem") 44 | 45 | mocking.assert_called_once_with('AT+QFDEL="file.pem"') 46 | assert result == mocked_return 47 | 48 | def test_upload_file_to_modem_ordinary_case(self, mocker, file): 49 | """This method checks the upload_file_to_modem() with mocked ATCom responses.""" 50 | # Mock the necessary function. 51 | mocker.patch("pico_lte.modules.file.len", return_value=60) 52 | mocker.patch("pico_lte.utils.atcom.ATCom.send_at_comm_once") 53 | mocked_responses = [ 54 | {"status": Status.SUCCESS, "response": ["CONNECT"]}, 55 | {"status": Status.SUCCESS, "response": ["OK"]}, 56 | ] 57 | mocking = mocker.patch("pico_lte.utils.atcom.ATCom.send_at_comm", side_effect=mocked_responses) 58 | 59 | result = file.upload_file_to_modem("file.pem", None) 60 | 61 | mocking.assert_any_call('AT+QFUPL="file.pem",60,5000', "CONNECT", urc=True) 62 | mocking.assert_any_call(file.CTRL_Z) 63 | assert result == mocked_responses[1] 64 | 65 | def test_upload_file_to_modem_no_connect(self, mocker, file): 66 | """This method checks the upload_file_to_modem() with mocked ATCom responses 67 | but with no CONNECT returned. 68 | """ 69 | # Mock the necessary function. 70 | mocker.patch("pico_lte.modules.file.len", return_value=60) 71 | mocked_response = {"status": Status.TIMEOUT, "response": "timeout"} 72 | mocking = mocker.patch("pico_lte.utils.atcom.ATCom.send_at_comm", return_value=mocked_response) 73 | 74 | result = file.upload_file_to_modem("file.pem", None) 75 | 76 | mocking.assert_any_call('AT+QFUPL="file.pem",60,5000', "CONNECT", urc=True) 77 | assert mocking.call_count == 1 78 | assert result == mocked_response 79 | -------------------------------------------------------------------------------- /pico_lte/modules/gps.py: -------------------------------------------------------------------------------- 1 | """ 2 | Module for including functions of location service of PicoLTE module. 3 | """ 4 | 5 | from pico_lte.utils.helpers import get_desired_data 6 | from pico_lte.utils.status import Status 7 | 8 | 9 | class GPS: 10 | """ 11 | Class for inculding functions of location service of PicoLTE module. 12 | """ 13 | 14 | def __init__(self, atcom): 15 | """ 16 | Initialization of the class. 17 | """ 18 | self.atcom = atcom 19 | 20 | def get_priority(self): 21 | """ 22 | Get the priority of the GPS. 23 | 24 | Returns 25 | ------- 26 | dict 27 | Result that includes "status" and "response" keys 28 | """ 29 | command = 'AT+QGPSCFG="priority"' 30 | return self.atcom.send_at_comm(command) 31 | 32 | def set_priority(self, priority): 33 | """ 34 | Set the priority of the GPS. 35 | 36 | Parameters 37 | ---------- 38 | priority : int 39 | Priority of the GPS. 40 | * 0 --> GNSS prior mode 41 | * 1 --> WWAN prior mode 42 | 43 | Returns 44 | ------- 45 | dict 46 | Result that includes "status" and "response" keys 47 | """ 48 | command = f'AT+QGPSCFG="priority",{priority}' 49 | return self.atcom.send_at_comm(command) 50 | 51 | def turn_on(self, mode=1, accuracy=3, fix_count=0, fix_rate=1): 52 | """ 53 | Turn on the GPS. 54 | 55 | Parameters 56 | ---------- 57 | mode : int, default: 1 58 | Mode of the GPS. 59 | accuracy : int, default: 3 60 | The desired level of accuracy acceptable for fix computation. 61 | * 1 --> Low Accuracy (1000 m) 62 | * 2 --> Medium Accuracy (500 m) 63 | * 3 --> High Accuracy (50 m) 64 | fix_count : int 65 | Number of positioning or continuous positioning attempts. (0-1000)(default=0) 66 | 0 indicates continuous positioning. Other values indicate the number of positioning 67 | attempts. When the value reaches the specified number of attempts, the GNSS will 68 | be stopped. 69 | fix_rate : int, default: 1 70 | The interval between the first- and second-time positioning. Unit: second. 71 | 72 | Returns 73 | ------- 74 | dict 75 | Result that includes "status" and "response" keys 76 | """ 77 | command = f"AT+QGPS={mode},{accuracy},{fix_count},{fix_rate}" 78 | return self.atcom.send_at_comm(command) 79 | 80 | def turn_off(self): 81 | """ 82 | Turn off the GPS. 83 | 84 | Returns 85 | ------- 86 | dict 87 | Result that includes "status" and "response" keys 88 | """ 89 | command = "AT+QGPSEND" 90 | return self.atcom.send_at_comm(command) 91 | 92 | def get_location(self): 93 | """ 94 | Get the location of the device. 95 | 96 | Returns 97 | ------- 98 | dict 99 | Result that includes "status","response" and "value" keys 100 | * "status" --> Status.SUCCESS or Status.ERROR 101 | * "response" --> Response of the command 102 | * "value" --> [lat,lon] Location of the device 103 | """ 104 | command = "AT+QGPSLOC=2" 105 | desired = "+QGPSLOC: " 106 | result = self.atcom.send_at_comm(command, desired) 107 | 108 | if result["status"] == Status.SUCCESS: 109 | return get_desired_data(result, desired, data_index=[1, 2]) 110 | return result 111 | -------------------------------------------------------------------------------- /pico_lte/apps/slack.py: -------------------------------------------------------------------------------- 1 | """ 2 | Module for including functions of Slack API operations 3 | """ 4 | 5 | import time 6 | import json 7 | 8 | from pico_lte.common import config 9 | from pico_lte.utils.manager import StateManager, Step 10 | from pico_lte.utils.status import Status 11 | from pico_lte.utils.helpers import get_parameter 12 | 13 | 14 | class Slack: 15 | """ 16 | Class for including Slack API functions. 17 | """ 18 | 19 | cache = config["cache"] 20 | 21 | def __init__(self, base, network, http): 22 | """ 23 | Initialize Slack class. 24 | """ 25 | self.base = base 26 | self.network = network 27 | self.http = http 28 | 29 | def send_message(self, message, webhook_url=None): 30 | """ 31 | Function for sending message to Slack channel by using 32 | incoming webhook feature of Slack. 33 | 34 | Parameters 35 | ---------- 36 | message: str 37 | Message to send 38 | webhook_url: str 39 | Webhook URL of the Slack application 40 | 41 | Returns 42 | ------- 43 | dict 44 | Result dictionary that contains "status" and "message" keys. 45 | """ 46 | 47 | payload_json = {"text": message} 48 | payload = json.dumps(payload_json) 49 | 50 | if webhook_url is None: 51 | webhook_url = get_parameter(["slack", "webhook_url"]) 52 | 53 | if not webhook_url: 54 | return {"status": Status.ERROR, "response": "Missing arguments!"} 55 | 56 | step_network_reg = Step( 57 | function=self.network.register_network, 58 | name="register_network", 59 | success="get_pdp_ready", 60 | fail="failure", 61 | ) 62 | 63 | step_get_pdp_ready = Step( 64 | function=self.network.get_pdp_ready, 65 | name="get_pdp_ready", 66 | success="set_server_url", 67 | fail="failure", 68 | ) 69 | 70 | step_set_server_url = Step( 71 | function=self.http.set_server_url, 72 | name="set_server_url", 73 | success="set_content_type", 74 | fail="failure", 75 | function_params={"url": webhook_url}, 76 | ) 77 | 78 | step_set_content_type = Step( 79 | function=self.http.set_content_type, 80 | name="set_content_type", 81 | success="post_request", 82 | fail="failure", 83 | function_params={"content_type": 4}, 84 | ) 85 | 86 | step_post_request = Step( 87 | function=self.http.post, 88 | name="post_request", 89 | success="read_response", 90 | fail="failure", 91 | function_params={"data": payload}, 92 | cachable=True, 93 | interval=2, 94 | ) 95 | 96 | step_read_response = Step( 97 | function=self.http.read_response, 98 | name="read_response", 99 | success="success", 100 | fail="failure", 101 | function_params={"desired_response": "ok"}, 102 | ) 103 | 104 | # Add cache if it is not already existed 105 | function_name = "slack.send_message" 106 | 107 | sm = StateManager(first_step=step_network_reg, function_name=function_name) 108 | 109 | sm.add_step(step_network_reg) 110 | sm.add_step(step_get_pdp_ready) 111 | sm.add_step(step_set_content_type) 112 | sm.add_step(step_set_server_url) 113 | sm.add_step(step_post_request) 114 | sm.add_step(step_read_response) 115 | 116 | while True: 117 | result = sm.run() 118 | if result["status"] == Status.SUCCESS: 119 | return result 120 | elif result["status"] == Status.ERROR: 121 | return result 122 | time.sleep(result["interval"]) 123 | -------------------------------------------------------------------------------- /pico_lte/apps/scriptr.py: -------------------------------------------------------------------------------- 1 | """ 2 | Module for including functions of scripter.io operations 3 | """ 4 | 5 | import time 6 | 7 | from pico_lte.common import config 8 | from pico_lte.utils.manager import StateManager, Step 9 | from pico_lte.utils.status import Status 10 | from pico_lte.utils.helpers import get_parameter 11 | 12 | 13 | class Scriptr: 14 | """ 15 | Class for including Scriptr.io functions. 16 | """ 17 | 18 | cache = config["cache"] 19 | 20 | def __init__(self, base, network, http): 21 | """Constructor of the class. 22 | 23 | Parameters 24 | ---------- 25 | base : Base 26 | PicoLTE Base class 27 | network : Network 28 | PicoLTE Network class 29 | http : HTTP 30 | PicoLTE HTTP class 31 | """ 32 | self.base = base 33 | self.network = network 34 | self.http = http 35 | 36 | def send_data(self, data, query=None, authorization=None): 37 | """ 38 | Function for sending data to script. 39 | 40 | Parameters 41 | ---------- 42 | data: str 43 | Json for sending data to the script 44 | query: str 45 | Query of script 46 | authorization: str 47 | Authorization token 48 | """ 49 | 50 | if query is None: 51 | query = get_parameter(["scriptr", "query"]) 52 | 53 | if authorization is None: 54 | authorization = get_parameter(["scriptr", "authorization"]) 55 | 56 | header = ( 57 | "POST " 58 | + query 59 | + " HTTP/1.1\n" 60 | + "Host: " 61 | + "api.scriptrapps.io" 62 | + "\n" 63 | + "Content-Type: application/json\n" 64 | + "Content-Length: " 65 | + str(len(data) + 1) 66 | + "\n" 67 | + "Authorization: Bearer " 68 | + authorization 69 | + "\n" 70 | + "\n\n" 71 | ) 72 | 73 | step_network_reg = Step( 74 | function=self.network.register_network, 75 | name="register_network", 76 | success="get_pdp_ready", 77 | fail="failure", 78 | ) 79 | 80 | step_get_pdp_ready = Step( 81 | function=self.network.get_pdp_ready, 82 | name="get_pdp_ready", 83 | success="set_server_url", 84 | fail="failure", 85 | ) 86 | 87 | step_set_server_url = Step( 88 | function=self.http.set_server_url, 89 | name="set_server_url", 90 | success="post_request", 91 | fail="failure", 92 | function_params={"url": "https://api.scriptrapps.io"}, 93 | ) 94 | 95 | step_post_request = Step( 96 | function=self.http.post, 97 | name="post_request", 98 | success="read_response", 99 | fail="failure", 100 | function_params={"data": header + data, "header_mode": "1"}, 101 | cachable=True, 102 | interval=1, 103 | ) 104 | 105 | step_read_response = Step( 106 | function=self.http.read_response, 107 | name="read_response", 108 | success="success", 109 | fail="failure", 110 | function_params={"desired_response": '"status": "success"'}, 111 | retry=3, 112 | interval=1, 113 | ) 114 | 115 | function_name = "scriptr_io.send_data" 116 | sm = StateManager(first_step=step_network_reg, function_name=function_name) 117 | 118 | sm.add_step(step_network_reg) 119 | sm.add_step(step_get_pdp_ready) 120 | sm.add_step(step_set_server_url) 121 | sm.add_step(step_post_request) 122 | sm.add_step(step_read_response) 123 | 124 | while True: 125 | result = sm.run() 126 | if result["status"] == Status.SUCCESS: 127 | return result 128 | elif result["status"] == Status.ERROR: 129 | return result 130 | time.sleep(result["interval"]) 131 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | 2 | ### Certification 3 | cert/ 4 | ### Configs 5 | config.json 6 | 7 | ### MacOS 8 | pico_lte/.DS_Store 9 | .DS_Store 10 | 11 | ### VSCode Config 12 | .vscode/ 13 | 14 | ### Mock modules 15 | machine.py 16 | neopixel.py 17 | ubinascii.py 18 | micropython.py 19 | pyb.py 20 | 21 | ### PYTHON 22 | # Byte-compiled / optimized / DLL files 23 | __pycache__/ 24 | *.py[cod] 25 | *$py.class 26 | 27 | # C extensions 28 | *.so 29 | 30 | # Distribution / packaging 31 | .Python 32 | build/ 33 | develop-eggs/ 34 | dist/ 35 | downloads/ 36 | eggs/ 37 | .eggs/ 38 | lib/ 39 | lib64/ 40 | parts/ 41 | sdist/ 42 | var/ 43 | wheels/ 44 | share/python-wheels/ 45 | *.egg-info/ 46 | .installed.cfg 47 | *.egg 48 | MANIFEST 49 | 50 | # PyInstaller 51 | # Usually these files are written by a python script from a template 52 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 53 | *.manifest 54 | *.spec 55 | 56 | # Installer logs 57 | pip-log.txt 58 | pip-delete-this-directory.txt 59 | 60 | # Unit test / coverage reports 61 | htmlcov/ 62 | .tox/ 63 | .nox/ 64 | .coverage 65 | .coverage.* 66 | .cache 67 | nosetests.xml 68 | coverage.xml 69 | *.cover 70 | *.py,cover 71 | .hypothesis/ 72 | .pytest_cache/ 73 | cover/ 74 | test.json 75 | pytest.ini 76 | 77 | # Translations 78 | *.mo 79 | *.pot 80 | 81 | # Django stuff: 82 | *.log 83 | local_settings.py 84 | db.sqlite3 85 | db.sqlite3-journal 86 | 87 | # Flask stuff: 88 | instance/ 89 | .webassets-cache 90 | 91 | # Scrapy stuff: 92 | .scrapy 93 | 94 | # Sphinx documentation 95 | docs/_build/ 96 | 97 | # PyBuilder 98 | .pybuilder/ 99 | target/ 100 | 101 | # Jupyter Notebook 102 | .ipynb_checkpoints 103 | 104 | # IPython 105 | profile_default/ 106 | ipython_config.py 107 | 108 | # pyenv 109 | # For a library or package, you might want to ignore these files since the code is 110 | # intended to run in multiple environments; otherwise, check them in: 111 | # .python-version 112 | 113 | # pipenv 114 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 115 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 116 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 117 | # install all needed dependencies. 118 | #Pipfile.lock 119 | 120 | # poetry 121 | # Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. 122 | # This is especially recommended for binary packages to ensure reproducibility, and is more 123 | # commonly ignored for libraries. 124 | # https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control 125 | #poetry.lock 126 | 127 | # pdm 128 | # Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control. 129 | #pdm.lock 130 | # pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it 131 | # in version control. 132 | # https://pdm.fming.dev/#use-with-ide 133 | .pdm.toml 134 | 135 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm 136 | __pypackages__/ 137 | 138 | # Celery stuff 139 | celerybeat-schedule 140 | celerybeat.pid 141 | 142 | # SageMath parsed files 143 | *.sage.py 144 | 145 | # Environments 146 | bin/ 147 | pyvenv.cfg 148 | .env 149 | .venv 150 | env/ 151 | venv/ 152 | ENV/ 153 | env.bak/ 154 | venv.bak/ 155 | 156 | # Spyder project settings 157 | .spyderproject 158 | .spyproject 159 | 160 | # Rope project settings 161 | .ropeproject 162 | 163 | # mkdocs documentation 164 | /site 165 | 166 | # mypy 167 | .mypy_cache/ 168 | .dmypy.json 169 | dmypy.json 170 | 171 | # Pyre type checker 172 | .pyre/ 173 | 174 | # pytype static type analyzer 175 | .pytype/ 176 | 177 | # Cython debug symbols 178 | cython_debug/ 179 | 180 | # PyCharm 181 | # JetBrains specific template is maintained in a separate JetBrains.gitignore that can 182 | # be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore 183 | # and can be added to the global gitignore or merged into this file. For a more nuclear 184 | # option (not recommended) you can uncomment the following to ignore the entire idea folder. 185 | #.idea/ 186 | 187 | package-lock.json 188 | -------------------------------------------------------------------------------- /pico_lte/modules/auth.py: -------------------------------------------------------------------------------- 1 | """ 2 | Module for including authentication functions of PicoLTE module. 3 | """ 4 | 5 | import os 6 | 7 | from pico_lte.common import debug 8 | from pico_lte.utils.status import Status 9 | from pico_lte.utils.helpers import read_file 10 | from pico_lte.modules.file import File 11 | 12 | 13 | class Auth: 14 | """ 15 | Class for including authentication functions of PicoLTE module. 16 | """ 17 | 18 | def __init__(self, atcom, file): 19 | """ 20 | Constructor for Auth class. 21 | """ 22 | self.atcom = atcom 23 | self.file = file 24 | 25 | def load_certificates(self): 26 | """ 27 | Function for loading certificates from file 28 | 29 | Returns 30 | ------- 31 | dict 32 | Result that includes "status" and "response" keys 33 | """ 34 | cacert = read_file("../cert/cacert.pem") 35 | client_cert = read_file("../cert/client.pem") 36 | client_key = read_file("../cert/user_key.pem") 37 | 38 | first_run = cacert and client_cert and client_key 39 | 40 | # If first run, upload the certificates to the modem 41 | if first_run: 42 | del first_run 43 | try: 44 | # delete old certificates if existed 45 | self.file.delete_file_from_modem("/security/cacert.pem") 46 | self.file.delete_file_from_modem("/security/client.pem") 47 | self.file.delete_file_from_modem("/security/user_key.pem") 48 | # Upload new certificates 49 | self.file.upload_file_to_modem("/security/cacert.pem", cacert) 50 | del cacert 51 | self.file.upload_file_to_modem("/security/client.pem", client_cert) 52 | del client_cert 53 | self.file.upload_file_to_modem("/security/user_key.pem", client_key) 54 | del client_key 55 | except Exception as error: 56 | debug.error("Error occured while uploading certificates", error) 57 | return {"status": Status.ERROR, "response": str(error)} 58 | 59 | debug.info( 60 | "Certificates uploaded secure storage. Deleting from file system..." 61 | ) 62 | try: 63 | os.remove("../cert/cacert.pem") 64 | os.remove("../cert/client.pem") 65 | os.remove("../cert/user_key.pem") 66 | except Exception as error: 67 | debug.error("Error occured while deleting certificates", error) 68 | return {"status": Status.ERROR, "response": str(error)} 69 | 70 | debug.info("Certificates deleted from file system.") 71 | 72 | # check certificates in modem 73 | result = self.file.get_file_list("ufs:/security/*") 74 | response = result.get("response", []) 75 | 76 | cacert_in_modem = False 77 | client_cert_in_modem = False 78 | client_key_in_modem = False 79 | 80 | if result["status"] == Status.SUCCESS: 81 | for line in response: 82 | if "cacert.pem" in line: 83 | cacert_in_modem = True 84 | if "client.pem" in line: 85 | client_cert_in_modem = True 86 | if "user_key.pem" in line: 87 | client_key_in_modem = True 88 | 89 | if cacert_in_modem and client_cert_in_modem and client_key_in_modem: 90 | debug.info("Certificates found in PicoLTE.") 91 | return { 92 | "status": Status.SUCCESS, 93 | "response": "Certificates found in PicoLTE.", 94 | } 95 | else: 96 | debug.error("Certificates couldn't find in modem!") 97 | return { 98 | "status": Status.ERROR, 99 | "response": "Certificates couldn't find in modem!", 100 | } 101 | else: 102 | debug.error("Error occured while getting certificates from modem!") 103 | return { 104 | "status": Status.ERROR, 105 | "response": "Error occured while getting certificates from modem!", 106 | } 107 | -------------------------------------------------------------------------------- /pico_lte/apps/telegram.py: -------------------------------------------------------------------------------- 1 | """ 2 | Module for including functions of Telegram bot for PicoLTE module. 3 | """ 4 | import time 5 | 6 | from pico_lte.common import config 7 | from pico_lte.utils.manager import StateManager, Step 8 | from pico_lte.utils.status import Status 9 | from pico_lte.utils.helpers import get_parameter 10 | 11 | 12 | class Telegram: 13 | """ 14 | Telegram App module for PicoLTE lets you to create 15 | connections to your Telegram bot easily. 16 | """ 17 | 18 | cache = config["cache"] 19 | 20 | def __init__(self, base, network, http): 21 | """Constructor of the class. 22 | 23 | Parameters 24 | ---------- 25 | base : Base 26 | PicoLTE Base class 27 | network : Network 28 | PicoLTE Network class 29 | http : HTTP 30 | PicoLTE HTTP class 31 | """ 32 | self.base = base 33 | self.network = network 34 | self.http = http 35 | 36 | def send_message(self, payload, host=None, bot_token=None, chat_id=None): 37 | """This function sends a message to the bot. 38 | 39 | Parameters 40 | ---------- 41 | payload : str 42 | Payload of the message. 43 | host : str 44 | Telegram's server endpoint address. 45 | bot_token : str 46 | Bot's private token. 47 | chat_id : str 48 | Chat ID of where the bot lives. 49 | 50 | Returns 51 | ------- 52 | dict 53 | Result that includes "status" and "response" keys 54 | """ 55 | if host is None: 56 | host = get_parameter(["telegram", "server"], "api.telegram.org/bot") 57 | 58 | if bot_token is None: 59 | bot_token = get_parameter(["telegram", "token"]) 60 | 61 | if chat_id is None: 62 | chat_id = get_parameter(["telegram", "chat_id"]) 63 | 64 | publish_url = ( 65 | f"https://{host}{bot_token}/" + f"sendMessage?chat_id={chat_id}&text={payload}" 66 | ) 67 | 68 | step_network_reg = Step( 69 | function=self.network.register_network, 70 | name="register_network", 71 | success="pdp_ready", 72 | fail="failure", 73 | ) 74 | 75 | step_pdp_ready = Step( 76 | function=self.network.get_pdp_ready, 77 | name="pdp_ready", 78 | success="http_ssl_configuration", 79 | fail="failure", 80 | ) 81 | 82 | step_http_ssl_configuration = Step( 83 | function=self.http.set_ssl_context_id, 84 | name="http_ssl_configuration", 85 | success="set_server_url", 86 | fail="failure", 87 | function_params={"cid": 2}, 88 | ) 89 | 90 | step_set_server_url = Step( 91 | function=self.http.set_server_url, 92 | name="set_server_url", 93 | success="get_request", 94 | fail="failure", 95 | function_params={"url": publish_url}, 96 | interval=2, 97 | ) 98 | 99 | step_get_request = Step( 100 | function=self.http.get, 101 | name="get_request", 102 | success="read_response", 103 | fail="failure", 104 | cachable=True, 105 | interval=5, 106 | ) 107 | 108 | step_read_response = Step( 109 | function=self.http.read_response, 110 | name="read_response", 111 | success="success", 112 | fail="failure", 113 | function_params={"desired_response": '"ok":true'}, 114 | interval=3, 115 | retry=5, 116 | ) 117 | 118 | # Add cache if it is not already existed 119 | function_name = "telegram.send_message" 120 | 121 | sm = StateManager(first_step=step_network_reg, function_name=function_name) 122 | 123 | sm.add_step(step_network_reg) 124 | sm.add_step(step_pdp_ready) 125 | sm.add_step(step_http_ssl_configuration) 126 | sm.add_step(step_set_server_url) 127 | sm.add_step(step_get_request) 128 | sm.add_step(step_read_response) 129 | 130 | while True: 131 | result = sm.run() 132 | 133 | if result["status"] == Status.SUCCESS: 134 | return result 135 | elif result["status"] == Status.ERROR: 136 | return result 137 | time.sleep(result["interval"]) 138 | -------------------------------------------------------------------------------- /pico_lte/utils/helpers.py: -------------------------------------------------------------------------------- 1 | """ 2 | Module for storing helper functions 3 | """ 4 | 5 | import json 6 | from pico_lte.common import config 7 | from pico_lte.utils.status import Status 8 | 9 | 10 | def read_json_file(file_path): 11 | """ 12 | Function for reading json file 13 | """ 14 | try: 15 | with open(file_path, "r") as file: 16 | data = json.load(file) 17 | except: 18 | return None 19 | else: 20 | return data 21 | 22 | 23 | def write_json_file(file_path, data): 24 | """ 25 | Function for writing json file 26 | """ 27 | try: 28 | with open(file_path, "w") as file: 29 | json.dump(data, file) 30 | except: 31 | return None 32 | else: 33 | return data 34 | 35 | 36 | def deep_copy_of_dictionary(dict_instance): 37 | """Create a deepcopy of the dictionary given. 38 | 39 | Parameters 40 | ---------- 41 | dict_instance : dict 42 | It is the dictionary to be copied. 43 | """ 44 | if isinstance(dict_instance, dict): 45 | dictionary_to_return = {} 46 | 47 | for key, value in dict_instance.items(): 48 | dictionary_to_return[key] = value 49 | 50 | return dictionary_to_return 51 | else: 52 | return None 53 | 54 | 55 | def get_desired_data(result, prefix, separator=",", data_index=0): 56 | """Function for getting actual data from response""" 57 | result_to_return = deep_copy_of_dictionary(result) 58 | 59 | valuable_lines = None 60 | 61 | if result.get("status") != Status.SUCCESS: 62 | result["value"] = None 63 | return result 64 | 65 | response = result_to_return.get("response") 66 | 67 | for index, value in enumerate(response): 68 | if value == "OK" and index > 0: 69 | valuable_lines = [response[i] for i in range(0, index)] 70 | 71 | if valuable_lines: 72 | for line in valuable_lines: 73 | prefix_index = line.find(prefix) 74 | 75 | if prefix_index != -1: 76 | index = prefix_index + len(prefix) # Find index of meaningful data 77 | data_array = line[index:].split(separator) 78 | 79 | if isinstance(data_index, list): # If desired multiple data 80 | data_index = data_index[: len(data_array)] # Truncate data_index 81 | result_to_return["value"] = [ 82 | simplify(data_array[i]) for i in data_index 83 | ] # Return list 84 | elif isinstance(data_index, int): 85 | # If data_index is out of range, return first element 86 | data_index = data_index if data_index < len(data_array) else 0 87 | result_to_return["value"] = simplify( 88 | data_array[data_index] 89 | ) # Return single data 90 | elif data_index == "all": 91 | result_to_return["value"] = [simplify(data) for data in data_array] 92 | else: 93 | # If data_index is unknown type, return first element 94 | data_index = 0 95 | result_to_return["value"] = simplify( 96 | data_array[data_index] 97 | ) # Return single data 98 | return result_to_return 99 | # if no valuable data found 100 | result_to_return["value"] = None 101 | return result_to_return 102 | 103 | 104 | def simplify(text): 105 | """Function for simplifying strings""" 106 | if isinstance(text, str): 107 | return text.replace('"', "").replace("'", "") 108 | return text 109 | 110 | 111 | def read_file(file_path, file_type="t"): 112 | """ 113 | Function for reading file 114 | """ 115 | try: 116 | with open(file_path, "r" + file_type) as file: 117 | data = file.read() 118 | except: 119 | return None 120 | else: 121 | return data 122 | 123 | 124 | def write_file(file_path, data, file_type="t"): 125 | """ 126 | Function for writing file 127 | """ 128 | try: 129 | with open(file_path, "w" + file_type) as file: 130 | file.write(data) 131 | except: 132 | return None 133 | else: 134 | return data 135 | 136 | 137 | def get_parameter(path, default=None): 138 | """ 139 | Function for getting parameters for SDK methods from global config dictionary. 140 | """ 141 | desired = config.get("params", None) 142 | 143 | if isinstance(desired, dict): 144 | for element in path: 145 | if desired: 146 | desired = desired.get(element, None) 147 | if desired: 148 | return desired 149 | if default: 150 | return default 151 | return None 152 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 | 3 |

PicoLTE SDK for MicroPython

4 |

5 |

6 | an embedded framework to make easier cellular connections 7 |

8 | 10 | 11 |
12 | 13 | ![version](https://img.shields.io/github/package-json/v/sixfab/picocell_python-sdk?logoColor=blue&style=flat-square) ![](https://img.shields.io/badge/license-MIT-critical?style=flat-square) ![applications](https://img.shields.io/badge/applications-6%20services-success?style=flat-square) ![stars](https://img.shields.io/github/stars/sixfab/picocell_python-sdk?style=flat-square) 14 | 15 |
16 | 17 | 18 | Pico LTE SDK is an innovative framework that enables developers to integrate cellular communication capabilities into their embedded systems projects seamlessly. Pico LTE SDK simplifies the complexities of wireless connectivity, allowing developers to focus on their applications rather than the intricacies of cellular communication processes. 19 | 20 | This powerful SDK empowers developers to seamlessly integrate cellular capabilities into their projects, allowing your projects to communicate over wide areas using cellular networks. 21 | 22 | One of the standout features of Pico LTE SDK is its comprehensive compatibility with popular backend services provided by Amazon Web Services (AWS), Azure, ThingSpeak, Slack, Scriptr.io and Telegram. This integration opens up a world of possibilities for leveraging the power of cloud-based services and enables seamless communication between embedded systems and the wider Internet ecosystem. Pico LTE SDK is a game-changer for developers seeking to integrate cellular communication capabilities into their Raspberry Pi Pico-based projects. 23 | 24 | - **Easy Integration:** Enables seamless integration of cellular communication capabilities into embedded systems projects, specifically designed for the Sixfab Pico LTE board. 25 | - **Minimalistic Code:** Connecting to a built-in application requires less than 40 lines of code, reducing complexity and allowing for quick and efficient development. 26 | - **GPS Integration:** Easy-to-use GPS integration, enabling developers to incorporate location-based functionalities into their applications, leveraging cellular network-based positioning. 27 | - **Custom Application Modules:** With the Pico LTE SDK, developers have the flexibility to create their own application modules using the SDK. This feature allows for custom functionality tailored to specific project requirements. 28 | - **Versatile Protocols:** Pico LTE SDK simplifies the implementation of various protocols such as GPS, HTTPS, and MQTT. Developers can easily leverage these protocols for location-based services, secure web communication, and efficient machine-to-machine communication in IoT applications. 29 | 30 | ## Installation 31 | 32 | The installation of the SDK is provided in detail and step-by-step on the ["Pico LTE SDK for MicroPython"](https://docs.sixfab.com/docs/sixfab-pico-lte-micropython-sdk) page. 33 | 34 | - Clone the repository to your local machine or download the repository as a zip and extract it on your local machine. 35 | 36 | - After that, upload the "[pico_lte](./pico_lte/)" folder to the root directory of your Pico LTE device. That's all. 37 | 38 | 39 | ## Usage 40 | Using the SDK is pretty straightforward. 41 | 42 | Import the SDK with `from pico_lte.core import PicoLTE` line, and code your IoT project! 43 | 44 | For more references on installation or usage, please refer to our [documentation page](https://docs.sixfab.com/docs/sixfab-pico-lte-micropython-sdk). By examining the [example codes](./examples/) provided on the platforms, you can delve into further details. You can connect various sensors to the Pico LTE, collect data on temperature, humidity, and air quality, and transmit this data over the cellular network using the Pico LTE SDK. 45 | 46 | Additionally, the Sixfab Community is available for any questions or suggestions you may have. 47 | 48 |

49 | 50 | 51 | 52 | 53 | 54 | 55 |

56 | 57 | ## Configuration Files 58 | You can use a configuration file to increase maintainability of your embedded code. This file is named as `config.json` and stores necessary connection parameters which are designed for you to easily connect to the applications. You can find example files for each application and module in [CONFIGURATIONS.md](./CONFIGURATIONS.md) page. 59 | 60 | This file has to be in the root directory of the Pico LTE device's file system. 61 | 62 | Please see the [Configure the Pico LTE SDK](https://docs.sixfab.com/docs/sixfab-pico-lte-micropython-sdk) page for more details. 63 | 64 | ## Contributing 65 | All contributions are welcome. You can find the guidelines in [CONTRIBUTING.md](./CONTRIBUTING.md). 66 | 67 | ## License 68 | Licensed under the [MIT license](https://choosealicense.com/licenses/mit/). 69 | -------------------------------------------------------------------------------- /examples/__sdk__/create_your_own_method.py: -------------------------------------------------------------------------------- 1 | """ 2 | This example is aim to find out how to use manager utility of PicoLTE SDK. 3 | Manager is a utility to manage the complicated processes have multiple steps, 4 | specific execution order, need of response binded decision, etc. 5 | 6 | In this example we will use manager to perform HTTP POST request to a server. 7 | We will explain how to create a method by using manager step by step. 8 | 9 | Example Configuration 10 | --------------------- 11 | Create a config.json file in the root directory of the PicoLTE device. 12 | config.json file must include the following parameters for this example: 13 | 14 | config.json 15 | { 16 | "https":{ 17 | "server":"[HTTP_SERVER]", 18 | "username":"[YOUR_HTTP_USERNAME]", 19 | "password":"[YOUR_HTTP_PASSWORD]" 20 | }, 21 | } 22 | """ 23 | import time 24 | 25 | from pico_lte.utils.status import Status 26 | from pico_lte.utils.manager import StateManager, Step 27 | from pico_lte.core import PicoLTE 28 | 29 | 30 | # First of all we need to create the function 31 | def our_http_post_method(message): 32 | """Function for performing HTTP POST request to a server.""" 33 | 34 | # create instance of Modem class 35 | picoLTE = PicoLTE() 36 | 37 | # Creating step 1. In this case this step 38 | # will check the network registeration status 39 | # and if it is not registered then it will 40 | 41 | step_check_network = Step( 42 | name="check_network", # name of the step 43 | function=picoLTE.network.register_network, # function to be executed 44 | success="prepare_pdp", # if succied then go to next step 45 | fail="failure", # if failed then go to next step 46 | retry=3 # number of retries if failed without going on the failure step 47 | ) 48 | 49 | # "success", "failure" and "organizer" steps are built-in. 50 | # They are defined in the manager class. 51 | 52 | # Creating step 2. 53 | # In this step we will check the PDP status 54 | 55 | step_prepare_pdp = Step( 56 | name="prepare_pdp", 57 | function=picoLTE.network.get_pdp_ready, 58 | success="set_server_url", 59 | fail="failure", 60 | ) 61 | 62 | # Creating step 3. 63 | # In this step we will set the server URL 64 | # picoLTE.http.set_server_url function gets server URL from 65 | # the config.json file automatically. You must just create a 66 | # config.json file and put it in the PicoLTE root path. 67 | 68 | step_set_server_url = Step( 69 | name="set_server_url", 70 | function=picoLTE.http.set_server_url, 71 | success="post_request", 72 | fail="failure", 73 | ) 74 | 75 | # Creating step 4. 76 | # In this step we will send the POST request 77 | # We will pass the function_params as a dictionary 78 | # "data" is the name of argument that will be passed to the function 79 | # message is the value of argument that will be passed to the function 80 | # it acts like --> function(data=message) 81 | 82 | step_post_request = Step( 83 | name="post_request", 84 | function=picoLTE.http.post, 85 | success="read_response", 86 | fail="failure", 87 | function_params={"data": message}, 88 | interval=3, # interval in seconds between each steps and retries 89 | ) 90 | 91 | # Creating step 5. 92 | # In this step we will read the response of server. 93 | # We will use the "read_response" step to check the response status. 94 | # If the response status is 200, the post request is successful. 95 | # if the response status is 400 or 404, then function will return failure 96 | 97 | step_read_response = Step( 98 | name="read_response", 99 | function=picoLTE.http.read_response, 100 | success="success", 101 | fail="failure", 102 | function_params={ 103 | "desired_response": ["200"], 104 | "fault_response": ["400","404"] 105 | }, 106 | ) 107 | 108 | # We created all required steps. 109 | # Now we will create the manager object and add all steps to it. 110 | 111 | manager = StateManager() 112 | manager.add_step(step_check_network) 113 | manager.add_step(step_prepare_pdp) 114 | manager.add_step(step_set_server_url) 115 | manager.add_step(step_post_request) 116 | manager.add_step(step_read_response) 117 | 118 | # Now we will execute the manager. 119 | # Manager will execute all steps in the order. 120 | # If the step is successful, it will go to it's success step. 121 | # If the step is failed, it will go to it's failure step. 122 | 123 | while True: # Create an infinite loop to keep the manager running. 124 | result = manager.run() # Run the manager. 125 | 126 | if result["status"] == Status.SUCCESS: # If the manager is successful, then break the loop. 127 | return result 128 | elif result["status"] == Status.ERROR: # If the manager is failed, then break the loop. 129 | return result 130 | time.sleep(result["interval"]) # If manager is still running, then wait for the interval. 131 | 132 | 133 | # Now we can use the function 134 | # We will pass "Hello World" as a message to the function. 135 | 136 | our_http_post_method("Hello World") 137 | -------------------------------------------------------------------------------- /tests/test_modules_gps.py: -------------------------------------------------------------------------------- 1 | """ 2 | Test module for the modules.gps module. 3 | """ 4 | 5 | import pytest 6 | 7 | from pico_lte.modules.gps import GPS 8 | from pico_lte.utils.atcom import ATCom 9 | from pico_lte.utils.status import Status 10 | 11 | 12 | def default_response_types(): 13 | """This method returns default and mostly-used responses for ATCom messages.""" 14 | return [ 15 | {"status": Status.SUCCESS, "response": ["OK"]}, 16 | {"status": Status.TIMEOUT, "response": "timeout"}, 17 | ] 18 | 19 | 20 | class TestGPS: 21 | """ 22 | Test class for GPS. 23 | """ 24 | 25 | @pytest.fixture 26 | def gps(self): 27 | """This fixture returns a GPS instance.""" 28 | atcom = ATCom() 29 | return GPS(atcom) 30 | 31 | @staticmethod 32 | def mock_send_at_comm(mocker, responses_to_return): 33 | """This is a wrapper function to repeated long mocker.patch() statements.""" 34 | return mocker.patch( 35 | "pico_lte.utils.atcom.ATCom.send_at_comm", return_value=responses_to_return 36 | ) 37 | 38 | def test_constructor(self, gps): 39 | """This method tests the __init__ constructor.""" 40 | assert isinstance(gps.atcom, ATCom) 41 | 42 | @pytest.mark.parametrize( 43 | "mocked_response", 44 | [ 45 | { 46 | "status": Status.SUCCESS, 47 | "response": ["APP RDY", '+QGPSCFG: "priority",0,3', "OK"], 48 | }, 49 | {"status": Status.SUCCESS, "response": ['+QGPSCFG: "priority",1,2', "OK"]}, 50 | ] 51 | + default_response_types(), 52 | ) 53 | def test_get_priority(self, mocker, gps, mocked_response): 54 | """This method tests the get_priority() with mocked ATCom responses.""" 55 | mocking = TestGPS.mock_send_at_comm(mocker, mocked_response) 56 | result = gps.get_priority() 57 | 58 | mocking.assert_called_once_with('AT+QGPSCFG="priority"') 59 | assert result == mocked_response 60 | 61 | @pytest.mark.parametrize("mocked_response", default_response_types()) 62 | def test_set_priority(self, mocker, gps, mocked_response): 63 | """This method tests the set_priority() with mocked ATCom responses.""" 64 | mocking = TestGPS.mock_send_at_comm(mocker, mocked_response) 65 | 66 | for priority in [0, 1]: 67 | result = gps.set_priority(priority) 68 | mocking.assert_any_call(f'AT+QGPSCFG="priority",{priority}') 69 | assert result == mocked_response 70 | 71 | @pytest.mark.parametrize( 72 | "mocked_response", 73 | [{"status": Status.SUCCESS, "response": ["+CME ERROR: 504"]}] 74 | + default_response_types(), 75 | ) 76 | def test_turn_on_default_parameters(self, mocker, gps, mocked_response): 77 | """This method tests the turn_on() with predefined parameters.""" 78 | mocking = TestGPS.mock_send_at_comm(mocker, mocked_response) 79 | result = gps.turn_on() 80 | 81 | mocking.assert_called_once_with("AT+QGPS=1,3,0,1") 82 | assert result == mocked_response 83 | 84 | @pytest.mark.parametrize( 85 | "mode, accuracy, fix_count, fix_rate", 86 | [(1, 1, 45, 5), (2, 2, 1000, 11), (3, 3, 0, 1)], 87 | ) 88 | def test_turn_on_with_different_parameters( 89 | self, mocker, gps, mode, accuracy, fix_count, fix_rate 90 | ): 91 | """This method tests the turn_on() with using parameter options.""" 92 | mocked_response = {"status": Status.SUCCESS, "response": ["OK"]} 93 | mocking = TestGPS.mock_send_at_comm(mocker, mocked_response) 94 | result = gps.turn_on(mode, accuracy, fix_count, fix_rate) 95 | 96 | mocking.assert_called_once_with( 97 | f"AT+QGPS={mode},{accuracy},{fix_count},{fix_rate}" 98 | ) 99 | assert result == mocked_response 100 | 101 | @pytest.mark.parametrize("mocked_response", default_response_types()) 102 | def test_turn_off(self, mocker, gps, mocked_response): 103 | """This method tests turn_off() with mocked ATCom responses.""" 104 | mocking = TestGPS.mock_send_at_comm(mocker, mocked_response) 105 | result = gps.turn_off() 106 | 107 | mocking.assert_called_once_with("AT+QGPSEND") 108 | assert result == mocked_response 109 | 110 | @pytest.mark.parametrize( 111 | "mocked_response", 112 | [ 113 | { 114 | "status": Status.SUCCESS, 115 | "response": [ 116 | "+QGPSLOC: 061951.00,41.02044,28.99797,0.7,62.2,2,0.00,0.0,0.0,110513,09", 117 | "OK", 118 | ], 119 | }, 120 | {"status": Status.ERROR, "response": ["+CME ERROR: 516"]}, 121 | {"status": Status.TIMEOUT, "response": "timeout"}, 122 | ], 123 | ) 124 | def test_get_location(self, mocker, gps, mocked_response): 125 | """This method tests get_location() with mocked ATCom responses.""" 126 | mocking = TestGPS.mock_send_at_comm(mocker, mocked_response) 127 | result = gps.get_location() 128 | 129 | mocking.assert_called_once_with("AT+QGPSLOC=2", "+QGPSLOC: ") 130 | 131 | if result["status"] == Status.SUCCESS: 132 | assert result["value"] == ["41.02044", "28.99797"] 133 | assert result["status"] == mocked_response["status"] 134 | assert result["response"] == mocked_response["response"] 135 | -------------------------------------------------------------------------------- /tools/upload.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # This script is used to upload the firmware to the PicoLTE. 3 | 4 | # Internal variables 5 | PYBOARD_LOC="$(pwd)/tools/pyboard.py" 6 | FW_LOCATIONS=$(pwd)/build 7 | GREEN='\033[0;32m' 8 | RED='\033[0;31m' 9 | NOCOLOR='\033[0m' 10 | 11 | print_the_status_of_command() { 12 | if [ $? -eq 0 ]; then 13 | echo -e " ${GREEN}OK${NOCOLOR}" 14 | else 15 | echo -e " ${RED}FAILED${NOCOLOR}" 16 | exit 1 17 | fi 18 | } 19 | 20 | 21 | find_os_of_the_user() { 22 | if [[ "$OSTYPE" == "linux-gnu"* ]]; then 23 | OS="linux" 24 | elif [[ "$OSTYPE" == "darwin"* ]]; then 25 | OS="macos" 26 | else 27 | echo -e "${RED}- ERROR: Unknown OS detected. Exiting!${NOCOLOR}" 28 | exit 1 29 | fi 30 | } 31 | 32 | wait_user_to_connect_the_pico() { 33 | # Display the prompts in green 34 | echo -e "${GREEN}- Please connect the PicoLTE board. Press any key to continue...${NOCOLOR}" 35 | 36 | # Wait for the user to press a key 37 | read -n 1 -s 38 | } 39 | 40 | enter_bootloader_mode_on_pico() { 41 | # This function uses pyboard tool to enter the bootloader mode on the PicoLTE. 42 | # with sending "import machine; machine.bootloader()"" command. 43 | echo -n -e "- Entering the bootloader mode on the PicoLTE..." 44 | python3 $PYBOARD_LOC -d /dev/cu.usbmodem* -c "import machine; machine.bootloader()" > /dev/null 2>&1 45 | sleep 1.5 46 | 47 | # Print OK for always since 'sleep' is always successes. 48 | print_the_status_of_command 49 | 50 | } 51 | 52 | check_board_connection() { 53 | # This function checks if the PicoLTE is connected to the computer. 54 | RPI_VENDOR_ID="2e8a" 55 | MP_PRODUCT_ID="0005" 56 | BOOT_PRODUCT_ID="0003" 57 | 58 | # It uses lsusb to check if the board is connected. 59 | # And the state of the connection as a storage or serial. 60 | if lsusb | grep -q "$VENDOR_ID:$MP_PRODUCT_ID"; then 61 | BOARD_MODE="serial" 62 | elif lsusb | grep -q "$VENDOR_ID:$BOOT_PRODUCT_ID"; then 63 | BOARD_MODE="storage" 64 | else 65 | BOARD_MODE="none" 66 | fi 67 | } 68 | 69 | check_pico_storage_attached() { 70 | # This function checks if the PicoLTE is connected to the computer. 71 | # If the board is connected, it returns 0. Otherwise, it returns 1. 72 | if [ "$OS" == "macos" ]; then 73 | if [ -d "/Volumes/RPI-RP2" ]; then 74 | return 0 75 | else 76 | return 1 77 | fi 78 | elif [ "$OS" == "linux" ]; then 79 | if [ -d "/media/$USER/RPI-RP2" ]; then 80 | return 0 81 | else 82 | return 1 83 | fi 84 | fi 85 | } 86 | 87 | pico_connection_logic() { 88 | check_board_connection 89 | 90 | # If the board is connected in serial mode, it enters the bootloader mode. 91 | # If the board is connected in storage mode, it does nothing. 92 | # If the board is not connected, it waits for the user to connect the board. 93 | if [ "$BOARD_MODE" == "serial" ]; then 94 | echo -e "- PicoLTE is connected in serial mode." 95 | enter_bootloader_mode_on_pico 96 | elif [ "$BOARD_MODE" == "storage" ]; then 97 | echo -e "- PicoLTE is connected in storage mode." 98 | elif [ "$BOARD_MODE" == "none" ]; then 99 | # If the board is not connected, it waits for the user to connect the board. 100 | wait_user_to_connect_the_pico 101 | check_board_connection 102 | 103 | # Does the same logic as above. 104 | if [ "$BOARD_MODE" == "serial" ]; then 105 | echo -e "- PicoLTE is connected in serial mode." 106 | enter_bootloader_mode_on_pico 107 | elif [ "$BOARD_MODE" == "storage" ]; then 108 | echo -e "- PicoLTE is connected in storage mode." 109 | elif [ "$BOARD_MODE" == "none" ]; then 110 | echo -e "${RED}- PicoLTE is not connected.${NOCOLOR}" 111 | exit 1 112 | fi 113 | fi 114 | 115 | # If the board is connected in storage mode, it uploads the firmware. 116 | if check_pico_storage_attached; then 117 | upload_the_firmware 118 | else 119 | echo -e "${RED}- PicoLTE is not connected. Please connect the PicoLTE and try again.${NOCOLOR}" 120 | exit 1 121 | fi 122 | } 123 | 124 | upload_the_firmware() { 125 | echo -n -e "- Uploading the firmware ${BUILD_ID} to the board..." 126 | 127 | if [ "$OS" == "macos" ]; then 128 | cp $FW_LOCATIONS/$BUILD_ID.uf2 /Volumes/RPI-RP2 129 | elif [ "$OS" == "linux" ]; then 130 | cp $FW_LOCATIONS/$BUILD_ID.uf2 /media/$USER/RPI-RP2 131 | fi 132 | 133 | wait_until_volume_detached 134 | print_the_status_of_command 135 | } 136 | 137 | wait_until_volume_detached() { 138 | if [ "$OS" == "macos" ]; then 139 | while [ -d /Volumes/RPI-RP2 ] 140 | do 141 | sleep 0.5 142 | done 143 | elif [ "$OS" == "linux" ]; then 144 | while [ -d /media/$USER/RPI-RP2 ] 145 | do 146 | sleep 1 147 | done 148 | fi 149 | } 150 | 151 | argument_parser() { 152 | HELP_TEXT="Usage: $0 \nExample: $0 picoLTE-2023-01-01-01" 153 | 154 | if [ -z "$1" ]; then 155 | echo -e $HELP_TEXT 156 | exit 1 157 | elif [ "$1" == "--help" ]; then 158 | echo -e $HELP_TEXT 159 | exit 0 160 | elif [ "$1" == "-h" ]; then 161 | echo -e $HELP_TEXT 162 | exit 0 163 | else 164 | BUILD_ID="$1" 165 | fi 166 | } 167 | 168 | # Main function 169 | argument_parser $1 170 | find_os_of_the_user 171 | pico_connection_logic 172 | echo "- Uploading process completed." -------------------------------------------------------------------------------- /examples/__basic__/monitor_network.py: -------------------------------------------------------------------------------- 1 | """ 2 | Full Device and Network Monitoring Script for PicoLTE and PicoLTE 2 3 | This script is designed for technical support teams to perform detailed, read-only monitoring of the device and network status. 4 | 5 | Each section includes clear comments explaining: 6 | - What the code checks 7 | - Why the check is important 8 | - What the output can tell you when diagnosing issues 9 | """ 10 | 11 | from pico_lte.core import PicoLTE 12 | from pico_lte.common import debug 13 | 14 | # Initialize PicoLTE core; it internally initializes atcom, base, network, etc. 15 | pico_lte = PicoLTE() 16 | 17 | # Serial counter for numbering debug outputs 18 | SERIAL_COUNTER = 1 19 | 20 | def numbered_debug(message): 21 | """Outputs a debug message with a serial number so logs are easier to follow.""" 22 | global SERIAL_COUNTER 23 | if message: 24 | debug.info(f"{SERIAL_COUNTER}. {message}") 25 | SERIAL_COUNTER += 1 26 | 27 | def safe_check(label, func): 28 | """ 29 | Helper function to safely run checks with a label. 30 | Catches specific errors and outputs a clear debug message. 31 | """ 32 | try: 33 | result = func() 34 | numbered_debug(f"{label}: {result}") 35 | except (RuntimeError, ValueError) as e: 36 | numbered_debug(f"{label}: Error retrieving data — {str(e)}") 37 | 38 | # --------------- Device Information Check --------------- 39 | def get_device_information(): 40 | """Check device hardware presence and retrieve identifiers.""" 41 | debug.info("--- Device Information ---") 42 | safe_check("Device General Info", lambda: pico_lte.atcom.send_at_comm('ATI')) 43 | safe_check("IMEI (Unique Module ID)", lambda: pico_lte.atcom.send_at_comm('AT+GSN')) 44 | safe_check("Firmware Version", lambda: pico_lte.atcom.send_at_comm('AT+QGMR')) 45 | safe_check("Manufacturer Name", lambda: pico_lte.atcom.send_at_comm('AT+CGMI')) 46 | safe_check("Model Name", lambda: pico_lte.atcom.send_at_comm('AT+CGMM')) 47 | debug.info("\n\n") 48 | 49 | # --------------- SIM Card Status Check --------------- 50 | def check_sim_information(): 51 | """Check SIM card presence, ICCID, and readiness.""" 52 | debug.info("--- SIM Card Information ---") 53 | safe_check("SIM ICCID (Card Serial Number)", lambda: pico_lte.base.get_sim_iccid()) 54 | safe_check("SIM Ready Status", lambda: pico_lte.base.check_sim_ready()) 55 | debug.info("\n\n") 56 | 57 | # --------------- Network Configuration Check --------------- 58 | def check_network_type(): 59 | """Retrieve network scan modes and IoT optimization settings.""" 60 | debug.info("--- Network Type Information ---") 61 | safe_check("Network Scan Mode", lambda: pico_lte.atcom.send_at_comm('AT+QCFG=\"nwscanmode\"')) 62 | safe_check("IoT Optimization Mode", lambda: pico_lte.atcom.send_at_comm('AT+QCFG=\"iotopmode\"')) 63 | safe_check("Current Network Technology", lambda: pico_lte.network.get_access_technology()) 64 | debug.info("\n\n") 65 | 66 | # --------------- Signal Quality Check --------------- 67 | def check_signal_quality(): 68 | """Retrieve basic signal strength and quality information.""" 69 | debug.info("--- Signal Quality ---") 70 | safe_check("Signal Quality (CSQ - RSSI/BER)", lambda: pico_lte.atcom.send_at_comm('AT+CSQ')) 71 | debug.info("\n\n") 72 | 73 | # --------------- Network Status Check --------------- 74 | def check_network_status(): 75 | """Retrieve operator, registration, cell, and connection status.""" 76 | debug.info("--- Network Status ---") 77 | safe_check("Operator Info + Access Tech", lambda: pico_lte.atcom.send_at_comm('AT+COPS?')) 78 | safe_check("LTE Network Registration (CEREG)", lambda: pico_lte.atcom.send_at_comm('AT+CEREG?')) 79 | safe_check("Serving Cell Info", lambda: pico_lte.atcom.send_at_comm('AT+QNWINFO')) 80 | safe_check("Extended Signal Quality (QCSQ - RSRP/RSRQ/SINR)", lambda: pico_lte.atcom.send_at_comm('AT+QCSQ')) 81 | safe_check("Signaling Connection Status (QCSCON)", lambda: pico_lte.atcom.send_at_comm('AT+QCSCON?')) 82 | debug.info("\n\n") 83 | 84 | # --------------- Packet Service and APN Check --------------- 85 | def check_packet_service_status(): 86 | """Check APN, IP assignment, and data attach status.""" 87 | debug.info("--- Packet Service and APN Info ---") 88 | safe_check("PDP Context (APN Settings)", lambda: pico_lte.atcom.send_at_comm('AT+CGDCONT?')) 89 | safe_check("IP Address Info", lambda: pico_lte.atcom.send_at_comm('AT+CGPADDR')) 90 | safe_check("Packet Attach Status (CGATT)", lambda: pico_lte.atcom.send_at_comm('AT+CGATT?')) 91 | debug.info("\n\n") 92 | 93 | # ------------------------- QPING Connectivity Check ------------------------- 94 | def check_qping(): 95 | """Perform a ping test to check internet connectivity.""" 96 | debug.info("--- QPING Command ---") 97 | safe_check("QPING (Single Ping Test)", lambda: pico_lte.atcom.send_at_comm('AT+QPING=1,\"www.google.com\"')) 98 | debug.info("\n\n") 99 | 100 | # --------------- Main Monitoring Function --------------- 101 | def main(): 102 | """Run all diagnostic checks sequentially.""" 103 | global SERIAL_COUNTER 104 | SERIAL_COUNTER = 1 105 | debug.info("========== PicoLTE Device and Network Status Check Start ==========\n\n") 106 | 107 | get_device_information() 108 | check_sim_information() 109 | check_network_type() 110 | check_signal_quality() 111 | check_network_status() 112 | check_packet_service_status() 113 | check_qping() 114 | 115 | debug.info("========== PicoLTE Device and Network Status Check Complete ==========") 116 | 117 | if __name__ == "__main__": 118 | main() 119 | -------------------------------------------------------------------------------- /tools/build.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # This script is used to embed the library as a frozen module and build the firmware. 3 | 4 | # Terminal ANSI colors 5 | GREEN='\033[0;32m' 6 | RED='\033[0;31m' 7 | NOCOLOR='\033[0m' 8 | YELLOW='\033[1;33m' 9 | 10 | print_the_status_of_command() { 11 | if [ $? -eq 0 ]; then 12 | echo -e " ${GREEN}OK${NOCOLOR}" 13 | else 14 | echo -e " ${RED}FAILED${NOCOLOR}" 15 | exit 1 16 | fi 17 | } 18 | 19 | download_firmware() { 20 | cd $DOWNLOAD_LOC 21 | # Check if the MicroPython source code is already downloaded 22 | if [ -d "micropython" ]; then 23 | echo -n "- MicroPython source code already downloaded. Pulling the latest changes..." 24 | cd micropython 25 | git pull > /dev/null 26 | else 27 | echo -n "- Downloading latest MicroPython source code..." 28 | git clone --quiet https://github.com/micropython/micropython.git > /dev/null 29 | fi 30 | 31 | print_the_status_of_command 32 | } 33 | 34 | find_os_of_the_user() { 35 | if [[ "$OSTYPE" == "linux-gnu"* ]]; then 36 | echo "- Linux OS detected." 37 | OS="linux" 38 | elif [[ "$OSTYPE" == "darwin"* ]]; then 39 | echo "- MacOS detected." 40 | OS="macos" 41 | else 42 | echo "- Unknown OS detected. Exiting!" 43 | exit 1 44 | fi 45 | } 46 | 47 | download_the_toolchain_macos() { 48 | echo -n "- Downloading the toolset for MacOS..." 49 | # Download CMake using brew 50 | brew install cmake > /dev/null 2>&1 51 | # Download the toolchain using brew 52 | brew tap ArmMbed/homebrew-formulae > /dev/null 2>&1 53 | brew install arm-none-eabi-gcc > /dev/null 2>&1 54 | 55 | print_the_status_of_command 56 | } 57 | 58 | download_the_toolchain_linux() { 59 | echo -n "- Downloading the toolset for Linux with apt-get..." 60 | # Download CMake 61 | sudo apt-get install cmake > /dev/null 2>&1 62 | # Download the toolchain 63 | sudo apt-get install gcc-arm-none-eabi > /dev/null 2>&1 64 | 65 | print_the_status_of_command 66 | } 67 | 68 | prepare_the_environment() { 69 | find_os_of_the_user 70 | if [ "$OS" == "macos" ]; then 71 | download_the_toolchain_macos 72 | elif [ "$OS" == "linux" ]; then 73 | download_the_toolchain_linux 74 | fi 75 | 76 | cd $DOWNLOAD_LOC/micropython 77 | 78 | echo -n "- Building the MicroPython cross-compiler..." 79 | make -C mpy-cross > /dev/null 80 | print_the_status_of_command 81 | 82 | cd ports/rp2 83 | 84 | echo -n "- Building the git submodules..." 85 | make BOARD=PICO_W submodules > /dev/null 2>&1 86 | print_the_status_of_command 87 | 88 | echo -n "- Cleaning the older build files..." 89 | make BOARD=PICO_W clean > /dev/null 90 | print_the_status_of_command 91 | } 92 | 93 | copy_umqtt_as_frozen_module() { 94 | UMQTT_SIMPLE_LOC="$DOWNLOAD_LOC/micropython-lib/micropython/umqtt.simple/umqtt" 95 | UMQTT_ROBUST_LOC="$DOWNLOAD_LOC/micropython-lib/micropython/umqtt.robust/umqtt" 96 | 97 | echo -n "- Copying the uMQTT library to frozen modules..." 98 | # Delete the older version of the library. 99 | rm -rf $DOWNLOAD_LOC/micropython/ports/rp2/modules/umqtt > /dev/null 2>&1 100 | cp -r $UMQTT_SIMPLE_LOC $DOWNLOAD_LOC/micropython/ports/rp2/modules/ 101 | status_1=$? 102 | 103 | cp -r $UMQTT_ROBUST_LOC $DOWNLOAD_LOC/micropython/ports/rp2/modules/ 104 | status_2=$? 105 | 106 | if [ $status_1 -eq 0 ] && [ $status_2 -eq 0 ]; then 107 | echo -e " ${GREEN}OK${NOCOLOR}" 108 | else 109 | echo -e " ${RED}FAILED${NOCOLOR}" 110 | exit 1 111 | fi 112 | } 113 | 114 | download_neccessary_libs_for_sdk() { 115 | echo -n "- Downloading the micropython-lib library..." 116 | if [ -d "$DOWNLOAD_LOC/micropython-lib" ]; then 117 | git pull > /dev/null 118 | else 119 | git clone --quiet https://github.com/micropython/micropython-lib.git $DOWNLOAD_LOC/micropython-lib > /dev/null 120 | fi 121 | print_the_status_of_command 122 | } 123 | 124 | copy_third_party_libs_as_frozen_module() { 125 | download_neccessary_libs_for_sdk 126 | copy_umqtt_as_frozen_module 127 | } 128 | 129 | copy_the_library_as_frozen_module() { 130 | echo -n "- Copying the PicoLTE SDK to frozen modules..." 131 | # Delete the older version of the library. 132 | rm -rf $DOWNLOAD_LOC/micropython/ports/rp2/modules/pico_lte > /dev/null 2>&1 133 | cp -r $PROJECT_DIR/pico_lte $DOWNLOAD_LOC/micropython/ports/rp2/modules/ 134 | print_the_status_of_command 135 | } 136 | 137 | build_the_firmware() { 138 | echo -n "- Building the firmware with frozen modules..." 139 | make -j 11 BOARD=PICO_W > /dev/null 2>&1 140 | print_the_status_of_command 141 | 142 | # Copy the firmware to the project directory. 143 | mkdir -p $PROJECT_DIR/build 144 | echo -n "- Copying the firmware to the project directory..." 145 | cp build-PICO_W/firmware.uf2 $PROJECT_DIR/build/$BUILD_ID.uf2 146 | print_the_status_of_command 147 | } 148 | 149 | # ######## MAIN FUNCTION ######## 150 | # If DOWNLOAD_LOC is not set, then the script will download the MicroPython source code to /tmp 151 | if [ -z "$DOWNLOAD_LOC" ]; then 152 | echo -e "${YELLOW}Warning: \$DOWNLOAD_LOC is not set. The script will download the MicroPython source code to /tmp.${NOCOLOR}" 153 | DOWNLOAD_LOC="/tmp" 154 | fi 155 | 156 | # If a parameter is given, then the build ID will be set to the parameter. 157 | if [ -n "$1" ]; then 158 | BUILD_ID="picoLTE-$1" 159 | else 160 | # Create the build ID from the date and time. 161 | BUILD_ID="picoLTE-$(date +%Y-%m-%d-%H-%M-%S)" 162 | fi 163 | 164 | # Save project directory to use later. 165 | PROJECT_DIR=$(pwd) 166 | 167 | echo "- Firmware build ID: $BUILD_ID" 168 | download_firmware 169 | prepare_the_environment 170 | copy_the_library_as_frozen_module 171 | copy_third_party_libs_as_frozen_module 172 | build_the_firmware 173 | echo "- Building process completed." -------------------------------------------------------------------------------- /CONFIGURATIONS.md: -------------------------------------------------------------------------------- 1 | # Configuration Files 2 | Each application module designed to work with configuration files for easier manipulation to server-side changes. A configuration file is named as `config.json` and stores necessary connection parameters which are designed for you to easily connect to the applications. 3 | 4 | In this file, you can find example configuration files for each application module and their mandatory and optional parameters. This file must be placed on the root directory of PicoLTE module. 5 | 6 | ## Table of Contents 7 | 1. [Amazon Web Services IoT Core](#amazon-web-services-iot-core-configurations) 8 | 2. [Microsoft Azure IoT Hub](#microsoft-azure-iot-hub-configurations) 9 | 3. [Slack](#slack-configurations) 10 | 4. [Telegram](#telegram-configurations) 11 | 5. [ThingSpeak™](#thingspeak-configurations) 12 | 6. [Native HTTPS](#https-configurations) 13 | 7. [Native MQTTS](#mqtts-configurations) 14 | 8. [Configuration Files for Your Own Application Module](#configuration-files-for-your-own-application-module) 15 | 16 | ## Applications 17 | In this section, we're going to give you better understanding about how to create a `config.json` file for specific application modules. 18 | 19 | ### Amazon Web Services IoT Core Configurations 20 | You can select MQTTS or HTTPS protocol and delete the other attribute. 21 | ```json 22 | { 23 | "aws": { 24 | "mqtts": { 25 | "host": "[YOUR_AWSIOT_ENDPOINT]", 26 | "port": "[YOUR_AWSIOT_MQTT_PORT]", 27 | "pub_topic": "[YOUR_MQTT_TOPIC]", 28 | "sub_topics": [ 29 | "[YOUR_MQTT_TOPIC/1]", 30 | "[YOUR_MQTT_TOPIC/2]" 31 | ] 32 | }, 33 | 34 | "https": { 35 | "endpoint": "[YOUR_AWS_IOT_ENDPOINT]", 36 | "topic": "[YOUR_DEVICE_TOPIC]" 37 | } 38 | } 39 | } 40 | ``` 41 | 42 | ### Microsoft Azure IoT Hub Configurations 43 | Within this level of configurations, you can use Azure IoT Hub directly. 44 | ```json 45 | { 46 | "azure": { 47 | "hub_name": "[YOUR_IOT_HUB_NAME]", 48 | "device_id": "[YOUR_DEVICE_ID]" 49 | } 50 | } 51 | ``` 52 | For more detailed configuration, you may want to use extra MQTTS parameters. Each attribute in MQTTS is optional. 53 | ```json 54 | { 55 | "azure": { 56 | "hub_name": "[YOUR_IOT_HUB_NAME]", 57 | "device_id": "[YOUR_DEVICE_ID]", 58 | "mqtts": { 59 | "host":"[YOUR_MQTT_HOST]", 60 | "port":"[YOUR_MQTT_PORT]", 61 | "pub_topic":"[YOUR_MQTT_PUB_TOPIC]", 62 | "sub_topics":[ 63 | ["[YOUR_MQTT_TOPIC/1]",[QOS]], 64 | ["[YOUR_MQTT_TOPIC/2]",[QOS]] 65 | ], 66 | "username":"[YOUR_MQTT_USERNAME]", 67 | "password":"[YOUR_MQTT_PASSWORD]" 68 | } 69 | } 70 | } 71 | ``` 72 | 73 | ### Slack Configurations 74 | To connect Slack, only need is a WebHook URL, there is no more detailed attributes. 75 | 76 | ```json 77 | { 78 | "slack":{ 79 | "webhook_url": "[INCOMING_WEBHOOK_URL]" 80 | } 81 | } 82 | ``` 83 | 84 | ### Telegram Configurations 85 | Within this level of configurations, you can use Telegram directly. 86 | ```json 87 | { 88 | "telegram": { 89 | "token": "[YOUR_BOT_TOKEN_ID]", 90 | "chat_id": "[YOUR_GROUP_CHAT_ID]" 91 | } 92 | } 93 | ``` 94 | In case of future server URL changes in Telegram side, you may want to add `server` attribute as shown below. 95 | ```json 96 | { 97 | "telegram": { 98 | "server": "[TELEGRAM_BOT_API_ENDPOINT]", 99 | "token": "[YOUR_BOT_TOKEN_ID]", 100 | "chat_id": "[YOUR_GROUP_CHAT_ID]" 101 | } 102 | } 103 | ``` 104 | 105 | 106 | ### ThingSpeak Configurations 107 | Within this level of configurations, you can use ThingSpeak directly. Subscription and publish operations are made directly to all channel fields. 108 | ```json 109 | { 110 | "thingspeak": { 111 | "channel_id": "[YOUR_CHANNEL_ID]", 112 | "mqtts": { 113 | "client_id": "[DEVICE_MQTT_CLIENT_ID]", 114 | "username": "[DEVICE_MQTT_USERNAME]", 115 | "password": "[DEVICE_MQTT_PASSWORD]" 116 | } 117 | } 118 | } 119 | ``` 120 | For better control on which fields to subscribe or publish, you may want to add extra attributes. Also, please note that host and port address can be change by its own attributes. 121 | ```json 122 | { 123 | "thingspeak": { 124 | "channel_id": "[YOUR_CHANNEL_ID]", 125 | "mqtts": { 126 | "host": "[THINGSPEAK_HOST_ADDRESS]", 127 | "port": "[THINGSPEAK_PORT_ADDRESS]", 128 | "client_id": "[DEVICE_MQTT_CLIENT_ID]", 129 | "username": "[DEVICE_MQTT_USERNAME]", 130 | "password": "[DEVICE_MQTT_PASSWORD]", 131 | "sub_topics": [ 132 | ["[YOUR_MQTT_TOPIC]", [QOS]] 133 | ], 134 | "pub_topic": "[YOUR_MQTT_TOPIC]" 135 | } 136 | } 137 | } 138 | ``` 139 | ## Modules 140 | Some use-cases can be implemented by using modules when there is no spesific application for that use-case. In this situtations, the developers can built their own solutions with using HTTPS and MQTTS modules. 141 | 142 | ### HTTPS Configurations 143 | ```json 144 | { 145 | "https": { 146 | "server": "[HTTP_SERVER]", 147 | "username": "[YOUR_HTTP_USERNAME]", 148 | "password": "[YOUR_HTTP_PASSWORD]" 149 | } 150 | } 151 | ``` 152 | ### MQTTS Configurations 153 | ```json 154 | { 155 | "mqtts": { 156 | "host": "[YOUR_MQTT_HOST]", 157 | "port": "[YOUR_MQTT_PORT]", 158 | "pub_topic": "[YOUR_MQTT_PUB_TOPIC]", 159 | "sub_topics": [ 160 | ["[YOUR_MQTT_TOPIC/1]",[QOS]], 161 | ["[YOUR_MQTT_TOPIC/2]",[QOS]] 162 | ], 163 | "username": "[YOUR_MQTT_USERNAME]", 164 | "password": "[YOUR_MQTT_PASSWORD]" 165 | } 166 | ``` 167 | 168 | ## Configuration Files for Your Own Application Module 169 | The most important feature that we've developed in PicoLTE SDK is the ability to create new applications for your specific services. Please refer to [CONTRIBUTING.md](./CONTRIBUTING.md) guidelines. You need to follow standarts that we used to create an application configuration parameters. 170 | 171 | This is the general structure of a `config.json` file: 172 | ``` 173 | { 174 | "your_own_app": { 175 | [application_specific_attributes], 176 | // If the connection made with MQTTS: 177 | "mqtts": { 178 | "host": "", 179 | "port": "", 180 | "pub_topic": "", 181 | "sub_topics": [ 182 | ["", [QOS]], 183 | ["", [QOS]] 184 | ], 185 | "username": "", 186 | "password": "" 187 | }, 188 | // If the connection made with HTTPS: 189 | "https": { 190 | "server": "", 191 | "username": "", 192 | "password": "" 193 | } 194 | } 195 | } 196 | ``` 197 | In case of redundant parameter in MQTTS or HTTPS, you can remove it from the structure. -------------------------------------------------------------------------------- /tests/test_modules_auth.py: -------------------------------------------------------------------------------- 1 | """ 2 | Test module for the modules.auth module. 3 | """ 4 | 5 | import pytest 6 | 7 | from pico_lte.modules.auth import Auth 8 | from pico_lte.modules.file import File 9 | from pico_lte.utils.atcom import ATCom 10 | from pico_lte.utils.status import Status 11 | 12 | 13 | class TestAuth: 14 | """Test class for Auth module.""" 15 | 16 | @pytest.fixture 17 | def auth(self): 18 | """It returns an Auth instance.""" 19 | atcom = ATCom() 20 | file = File(atcom) 21 | return Auth(atcom, file) 22 | 23 | @staticmethod 24 | def prepare_mocked_functions( 25 | mocker, 26 | simulation_data, 27 | side_effect_delete_file_from_modem=None, 28 | return_value_upload_to_file=None, 29 | get_file_list_status=True, 30 | side_effect_os_remove=None, 31 | ): 32 | """This function crates mocked function with predefined return values 33 | which will be given by the simulation_data parameter. 34 | """ 35 | mocked_response_get_file_list = { 36 | "status": Status.SUCCESS if get_file_list_status else Status.ERROR, 37 | "response": simulation_data["file_name"], 38 | } 39 | 40 | mocker.patch( 41 | "pico_lte.modules.auth.read_file", 42 | side_effect=simulation_data["file_inside"], 43 | ) 44 | mocker.patch( 45 | "pico_lte.modules.file.File.delete_file_from_modem", 46 | return_value=None, 47 | side_effect=side_effect_delete_file_from_modem, 48 | ) 49 | mocker.patch( 50 | "pico_lte.modules.file.File.upload_file_to_modem", 51 | return_value=return_value_upload_to_file, 52 | ) 53 | mocker.patch("os.remove", side_effect=side_effect_os_remove) 54 | mocker.patch( 55 | "pico_lte.modules.file.File.get_file_list", 56 | return_value=mocked_response_get_file_list, 57 | ) 58 | 59 | def test_constructor_method(self, auth): 60 | """This method tests the constructor method of the Auth class.""" 61 | assert isinstance(auth.atcom, ATCom) 62 | assert isinstance(auth.file, File) 63 | 64 | def test_load_certificates_ordinary_case(self, mocker, auth): 65 | """This method tests the load_certificates() method with 66 | expected and ordinary usage. 67 | """ 68 | # Data which simulates system behaviour. 69 | mocked_data = { 70 | "file_name": ["cacert.pem", "client.pem", "user_key.pem"], 71 | "file_inside": ["CACERT_CERT", "CLIENT_CERT", "USER_PRIV_KEY"], 72 | } 73 | 74 | # Assign mock functions. 75 | TestAuth.prepare_mocked_functions(mocker, mocked_data) 76 | 77 | result = auth.load_certificates() 78 | 79 | assert result["status"] == Status.SUCCESS 80 | assert result["response"] == "Certificates found in PicoLTE." 81 | 82 | def test_load_certificates_wrong_certificate_names(self, mocker, auth): 83 | """This method tests the load_certificates() method without 84 | proper names. 85 | """ 86 | # Data which simulates system behaviour. 87 | mocked_data = { 88 | "file_name": ["ca.pem", "cli.pem", "user.pem"], 89 | "file_inside": ["CACERT_CERT", "CLIENT_CERT", "USER_PRIV_KEY"], 90 | } 91 | 92 | # Assign mock functions. 93 | TestAuth.prepare_mocked_functions(mocker, mocked_data) 94 | 95 | result = auth.load_certificates() 96 | 97 | assert result["status"] == Status.ERROR 98 | assert result["response"] == "Certificates couldn't find in modem!" 99 | 100 | def test_load_certificates_file_throws_exception(self, mocker, auth): 101 | """This method tests the load_certificates() method, and file methods 102 | throws exception. 103 | """ 104 | # Data which simulates system behaviour. 105 | mocked_data = { 106 | "file_name": ["cacert.pem", "client.pem", "user_key.pem"], 107 | "file_inside": ["CACERT_CERT", "CLIENT_CERT", "USER_PRIV_KEY"], 108 | } 109 | 110 | # Assign mock functions. 111 | TestAuth.prepare_mocked_functions( 112 | mocker, 113 | simulation_data=mocked_data, 114 | side_effect_delete_file_from_modem=OSError("Example Exception"), 115 | ) 116 | result = auth.load_certificates() 117 | 118 | assert result["status"] == Status.ERROR 119 | assert result["response"] == "Example Exception" 120 | 121 | def test_load_certificates_with_already_certificate_inside(self, mocker, auth): 122 | """This method tests if the load_certificates() method cannot find new 123 | certificates in cert/ directory, and gets the old ones from modem file system. 124 | """ 125 | # Data which simulates system behaviour. Note that, since file_inside is None, 126 | # it won't run first_try block. 127 | mocked_data = { 128 | "file_name": ["cacert.pem", "client.pem", "user_key.pem"], 129 | "file_inside": None, 130 | } 131 | # Assign mock functions. 132 | TestAuth.prepare_mocked_functions( 133 | mocker, simulation_data=mocked_data, get_file_list_status=True 134 | ) 135 | 136 | result = auth.load_certificates() 137 | 138 | assert result["status"] == Status.SUCCESS 139 | assert result["response"] == "Certificates found in PicoLTE." 140 | 141 | def test_load_certificates_with_error_on_get_file_list(self, mocker, auth): 142 | """This method tests load_certificates() method when it is not the first_try 143 | and there is an error at connection with PicoLTE. 144 | """ 145 | # Data which simulates system behaviour. 146 | mocked_data = { 147 | "file_name": ["cacert.pem", "client.pem", "user_key.pem"], 148 | "file_inside": None, 149 | } 150 | # Assign mock functions. 151 | TestAuth.prepare_mocked_functions( 152 | mocker, simulation_data=mocked_data, get_file_list_status=False 153 | ) 154 | 155 | result = auth.load_certificates() 156 | 157 | assert result["status"] == Status.ERROR 158 | assert ( 159 | result["response"] == "Error occured while getting certificates from modem!" 160 | ) 161 | 162 | def test_load_certificates_with_error_on_os_remove(self, mocker, auth): 163 | """This method tests load_certificates() method when it is the first try, 164 | but the os_remove() method raises exception. 165 | """ 166 | # Data which simulates system behaviour. 167 | mocked_data = { 168 | "file_name": ["cacert.pem", "client.pem", "user_key.pem"], 169 | "file_inside": ["CACERT_CERT", "CLIENT_CERT", "USER_PRIV_KEY"], 170 | } 171 | 172 | # Assign mock functions. 173 | TestAuth.prepare_mocked_functions( 174 | mocker, 175 | simulation_data=mocked_data, 176 | side_effect_os_remove=OSError("Example Exception"), 177 | ) 178 | 179 | result = auth.load_certificates() 180 | 181 | assert result["status"] == Status.ERROR 182 | assert result["response"] == "Example Exception" 183 | -------------------------------------------------------------------------------- /pico_lte/utils/manager.py: -------------------------------------------------------------------------------- 1 | """ 2 | Module for managing processes on modem step by step. 3 | """ 4 | 5 | from pico_lte.common import config, debug 6 | from pico_lte.utils.status import Status 7 | 8 | 9 | class Step: 10 | """Data class for storing step data""" 11 | 12 | is_ok = False 13 | final_step = False 14 | 15 | def __init__( 16 | self, 17 | name, 18 | function, 19 | success, 20 | fail, 21 | function_params=None, 22 | interval=0, 23 | retry=0, 24 | final_step=False, 25 | cachable=False, 26 | ): 27 | self.function = function 28 | self.name = name 29 | self.success = success 30 | self.fail = fail 31 | self.interval = interval 32 | self.retry = retry 33 | self.function_params = function_params 34 | self.final_step = final_step 35 | self.cachable = cachable 36 | 37 | def update_function_params(self, **args): 38 | """Method for updating function_params key of the step.""" 39 | for key, value in args.items(): 40 | self.function_params[key] = value 41 | 42 | 43 | class StateManager: 44 | """Class for managing states""" 45 | 46 | NO_WAIT_INTERVAL = 0 47 | retry_counter = 0 48 | steps = {} 49 | cache = config["cache"] 50 | 51 | def __init__(self, first_step, function_name=None): 52 | """Initializes state manager""" 53 | self.first_step = first_step 54 | self.function_name = function_name 55 | 56 | if function_name: 57 | if not self.cache.states.get(function_name): 58 | self.cache.add_cache(function_name) 59 | 60 | self.organizer_step = Step( 61 | function=self.organizer, 62 | name="organizer", 63 | success="organizer", 64 | fail="organizer", 65 | function_params=None, 66 | interval=0, 67 | retry=0, 68 | ) 69 | 70 | self.success_step = Step( 71 | function=self.success, 72 | name="success", 73 | success="success", 74 | fail="success", 75 | function_params=None, 76 | interval=0, 77 | retry=0, 78 | final_step=True, 79 | ) 80 | 81 | self.failure_step = Step( 82 | function=self.failure, 83 | name="failure", 84 | success="failure", 85 | fail="failure", 86 | function_params=None, 87 | interval=0, 88 | retry=0, 89 | final_step=True, 90 | ) 91 | 92 | self.current = self.organizer_step 93 | 94 | # Add default steps to steps dictionary 95 | self.add_step(self.organizer_step) 96 | self.add_step(self.success_step) 97 | self.add_step(self.failure_step) 98 | 99 | def add_step(self, step): 100 | """Adds step to steps dictionary""" 101 | self.steps[step.name] = step 102 | 103 | def update_step(self, step): 104 | """Updates step in steps dictionary""" 105 | self.steps[step.name] = step 106 | 107 | def get_step(self, name): 108 | """Returns step with name""" 109 | return self.steps[name] 110 | 111 | def clear_counter(self): 112 | """Clears retry counter""" 113 | self.retry_counter = 0 114 | 115 | def counter_tick(self): 116 | """Increments retry counter""" 117 | self.retry_counter += 1 118 | 119 | def organizer(self): 120 | """Organizer step function""" 121 | if self.current.name == "organizer": 122 | self.current = self.first_step 123 | 124 | cached_step = self.cache.get_state(self.function_name) 125 | if cached_step: # if cached step is not None 126 | self.current = self.get_step(cached_step) 127 | 128 | else: 129 | if self.current.is_ok: # step succieded 130 | if self.current.cachable: # Assign new cache if step cachable 131 | self.cache.set_state(self.function_name, self.current.name) 132 | 133 | self.current.is_ok = False 134 | 135 | if self.current.final_step: 136 | self.current = self.get_step("success") 137 | else: 138 | self.current = self.get_step(self.current.success) 139 | else: 140 | if self.retry_counter >= self.current.retry: 141 | # step failed and retry counter is exceeded 142 | self.current = self.get_step(self.current.fail) 143 | # clear cache 144 | self.cache.set_state(self.function_name, None) 145 | 146 | self.clear_counter() 147 | self.current.interval = self.NO_WAIT_INTERVAL 148 | else: 149 | # step failed and retry counter is not exceeded, retrying... 150 | self.current = self.get_step(self.current.name) 151 | self.counter_tick() 152 | return {"status": Status.SUCCESS} 153 | 154 | def success(self): 155 | """Success step function""" 156 | return { 157 | "status": Status.SUCCESS, 158 | "response": self.cache.get_last_response(), 159 | } 160 | 161 | def failure(self): 162 | """Fail step function""" 163 | return { 164 | "status": Status.ERROR, 165 | "response": self.cache.get_last_response(), 166 | } 167 | 168 | def execute_organizer_step(self): 169 | """Executes organizer step""" 170 | self.organizer() 171 | 172 | def execute_current_step(self): 173 | """Executes current step""" 174 | params = self.current.function_params 175 | 176 | if params: 177 | result = self.current.function(**params) 178 | else: 179 | result = self.current.function() 180 | 181 | debug.debug(f"{self.current.function.__name__:<25} : {result}") 182 | self.cache.set_last_response(result.get("response")) 183 | 184 | if result["status"] == Status.SUCCESS: 185 | self.current.is_ok = True 186 | else: 187 | self.current.is_ok = False 188 | 189 | return result 190 | 191 | def run(self, begin=None, end=None): 192 | """Runs state manager.""" 193 | result = {} 194 | 195 | if begin: 196 | self.current = self.get_step(begin) 197 | begin = None # to run above line only once at the beginning 198 | else: 199 | self.execute_organizer_step() 200 | 201 | step_result = self.execute_current_step() 202 | 203 | if end: 204 | if self.current.name == self.get_step(end).name: 205 | self.current.final_step = True 206 | 207 | if not self.current.final_step: 208 | result["status"] = Status.ONGOING 209 | result["interval"] = self.current.interval 210 | result["response"] = step_result.get("response") 211 | return result 212 | else: 213 | if self.current.name == "success": 214 | result["status"] = Status.SUCCESS 215 | result["response"] = step_result.get("response") 216 | elif self.current.name == "failure": 217 | result["status"] = Status.ERROR 218 | result["response"] = step_result.get("response") 219 | 220 | result["interval"] = self.NO_WAIT_INTERVAL 221 | return result 222 | -------------------------------------------------------------------------------- /pico_lte/utils/atcom.py: -------------------------------------------------------------------------------- 1 | """ 2 | Module for communicating with cellular modem over UART interface. 3 | """ 4 | 5 | import time 6 | from machine import UART, Pin 7 | from pico_lte.common import debug 8 | from pico_lte.utils.status import Status 9 | 10 | 11 | class ATCom: 12 | """Class for handling AT communication with modem""" 13 | 14 | def __init__(self, uart_number=0, tx_pin=Pin(0), rx_pin=Pin(1), baudrate=115200, timeout=10000): 15 | self.modem_com = UART(uart_number, tx=tx_pin, rx=rx_pin, baudrate=baudrate, timeout=timeout) 16 | 17 | def send_at_comm_once(self, command, line_end=True): 18 | """ 19 | Function for sending AT commmand to modem 20 | 21 | Parameters 22 | ---------- 23 | command: str 24 | AT command to send 25 | line_end: bool, default: True 26 | If True, send line end 27 | """ 28 | if line_end: 29 | compose = f"{command}\r".encode() 30 | else: 31 | compose = command.encode() 32 | debug.focus(compose) 33 | 34 | try: 35 | self.modem_com.write(compose) 36 | except: 37 | debug.error("Error occured while AT command writing to modem") 38 | 39 | def get_response(self, desired_responses=None, fault_responses=None, timeout=5): 40 | """ 41 | Function for getting modem response 42 | 43 | Parameters 44 | ---------- 45 | desired_response: str, default: None 46 | Desired response from modem 47 | timeout: int 48 | Timeout for getting response 49 | 50 | Returns 51 | ------- 52 | dict 53 | Result that includes "status" and "response" keys 54 | """ 55 | response = "" 56 | processed = [] 57 | 58 | if desired_responses: 59 | if isinstance(desired_responses, str): # if desired response is string 60 | desired_responses = [desired_responses] # make it list 61 | if fault_responses: 62 | if isinstance(fault_responses, str): # if desired response is string 63 | fault_responses = [fault_responses] # make it list 64 | 65 | timer = time.time() 66 | while True: 67 | time.sleep(0.1) # wait for new chars 68 | 69 | if time.time() - timer < timeout: 70 | while self.modem_com.any(): 71 | try: 72 | response += self.modem_com.read(self.modem_com.any()).decode("utf-8") 73 | debug.debug("Response:", [response]) 74 | except: 75 | pass 76 | else: 77 | return {"status": Status.TIMEOUT, "response": "timeout"} 78 | 79 | if response != "": 80 | responses = response.split("\r\n") 81 | processed.extend([x for x in responses if x != ""]) 82 | debug.debug("Processed:", processed) 83 | response = "" 84 | 85 | head = 0 86 | for index, value in enumerate(processed): 87 | processed_part = processed[head : index + 1] 88 | 89 | if value == "OK": 90 | if not desired_responses: # if we don't look for specific responses 91 | return {"status": Status.SUCCESS, "response": processed_part} 92 | else: 93 | if index - head < 1: # we haven't got an informative response here 94 | return {"status": Status.ERROR, "response": processed_part} 95 | 96 | for focus_line in processed[head:index]: # scan lines before 'OK' 97 | if desired_responses: 98 | if any(desired in focus_line for desired in desired_responses): 99 | debug.debug("Desired:", focus_line) 100 | return {"status": Status.SUCCESS, "response": processed_part} 101 | if fault_responses: 102 | if any(fault in focus_line for fault in fault_responses): 103 | debug.debug("Fault:", focus_line) 104 | return {"status": Status.ERROR, "response": processed_part} 105 | 106 | elif "+CME ERROR:" in value or value == "ERROR": # error 107 | return {"status": Status.ERROR, "response": processed_part} 108 | 109 | def get_urc_response(self, desired_responses=None, fault_responses=None, timeout=5): 110 | """ 111 | Function for getting modem urc response 112 | 113 | Parameters 114 | ---------- 115 | desired_response: str or list, default: None 116 | List of desired responses 117 | fault_response: str or list, default: None 118 | List of fault response from modem 119 | timeout: int 120 | timeout for getting response 121 | 122 | Returns 123 | ------- 124 | dict 125 | Result that includes "status" and "response" keys 126 | """ 127 | response = "" 128 | processed = [] 129 | 130 | if desired_responses: 131 | if isinstance(desired_responses, str): # if desired response is string 132 | desired_responses = [desired_responses] # make it list 133 | if fault_responses: 134 | if isinstance(fault_responses, str): # if desired response is string 135 | fault_responses = [fault_responses] # make it list 136 | 137 | if not desired_responses and not fault_responses: 138 | return {"status": Status.SUCCESS, "response": "No desired or fault responses"} 139 | 140 | timer = time.time() 141 | while True: 142 | time.sleep(0.1) # wait for new chars 143 | 144 | if time.time() - timer < timeout: 145 | while self.modem_com.any(): 146 | try: 147 | response += self.modem_com.read(self.modem_com.any()).decode("utf-8") 148 | except: 149 | pass 150 | else: 151 | return {"status": Status.TIMEOUT, "response": "timeout"} 152 | 153 | if response != "": 154 | responses = response.split("\r\n") 155 | processed.extend([x for x in responses if x != ""]) 156 | debug.debug("Processed:", processed) 157 | response = "" 158 | 159 | head = 0 160 | for index, value in enumerate(processed): 161 | processed_part = processed[head : index + 1] 162 | 163 | if desired_responses: 164 | for desired in desired_responses: 165 | if desired in value: 166 | return {"status": Status.SUCCESS, "response": processed_part} 167 | if fault_responses: 168 | for fault in fault_responses: 169 | if fault in value: 170 | return {"status": Status.ERROR, "response": processed_part} 171 | 172 | def send_at_comm(self, command, desired=None, fault=None, timeout=5, line_end=True, urc=False): 173 | """ 174 | Function for writing AT command to modem and getting modem response 175 | 176 | Parameters 177 | ---------- 178 | command: str 179 | AT command to send 180 | desired: str or list, default: None 181 | List of desired responses 182 | fault: str or list, default: None 183 | List of fault responses 184 | timeout: int 185 | Timeout for getting response 186 | line_end: bool, default: True 187 | If True, send line end 188 | urc: bool, default: False 189 | If True, get urc response 190 | 191 | Returns 192 | ------- 193 | dict 194 | Result that includes "status" and "response" keys 195 | """ 196 | self.send_at_comm_once(command, line_end=line_end) 197 | time.sleep(0.1) 198 | if urc: 199 | return self.get_urc_response(desired, fault, timeout) 200 | return self.get_response(desired, fault, timeout) 201 | -------------------------------------------------------------------------------- /tests/test_apps_google_sheets.py: -------------------------------------------------------------------------------- 1 | """ 2 | Test module for the apps.google_sheets module. 3 | """ 4 | 5 | import pytest 6 | 7 | from pico_lte.utils.status import Status 8 | from pico_lte.apps.google_sheets import GoogleSheets 9 | from pico_lte.utils.manager import StateManager, Step 10 | 11 | from pico_lte.utils.atcom import ATCom 12 | from pico_lte.modules.base import Base 13 | from pico_lte.modules.http import HTTP 14 | from pico_lte.modules.network import Network 15 | 16 | 17 | class TestGoogleSheets: 18 | """ 19 | Test class for Google Sheets. 20 | """ 21 | 22 | @pytest.fixture() 23 | def google_sheets_object(self): 24 | """This fixture returns a GoogleSheets instance.""" 25 | atcom = ATCom() 26 | base = Base(atcom) 27 | network = Network(atcom, base) 28 | http = HTTP(atcom) 29 | 30 | google_sheets = GoogleSheets(base, network, http) 31 | return google_sheets 32 | 33 | def test_constructor(self, google_sheets_object): 34 | """This method tests the __init__ constructor.""" 35 | assert isinstance(google_sheets_object.base, Base) 36 | assert isinstance(google_sheets_object.network, Network) 37 | assert isinstance(google_sheets_object.http, HTTP) 38 | 39 | def test_set_network_success(self, mocker, google_sheets_object): 40 | """This method tests the set_network() with mocked StateManager responses.""" 41 | mocker.patch( 42 | "pico_lte.utils.manager.StateManager.run", 43 | return_value={"status": Status.SUCCESS}, 44 | ) 45 | 46 | response = google_sheets_object.set_network() 47 | assert response == {"status": Status.SUCCESS} 48 | 49 | def test_set_network_error(self, mocker, google_sheets_object): 50 | """This method tests the set_network() with mocked StateManager responses.""" 51 | mocker.patch( 52 | "pico_lte.utils.manager.StateManager.run", 53 | return_value={"status": Status.ERROR}, 54 | ) 55 | 56 | response = google_sheets_object.set_network() 57 | assert response == {"status": Status.ERROR} 58 | 59 | def test_get_data_success(self, mocker, google_sheets_object): 60 | """This method tests the get_data() with mocked StateManager responses.""" 61 | mocker.patch( 62 | "pico_lte.utils.manager.StateManager.run", 63 | return_value={"status": Status.SUCCESS, "response": "", "interval": ""}, 64 | ) 65 | 66 | response = google_sheets_object.get_data( 67 | sheet="sheet", data_range="[DATA_RANGE]" 68 | ) 69 | assert response == {"status": Status.SUCCESS, "response": ""} 70 | 71 | def test_get_data_error(self, mocker, google_sheets_object): 72 | """This method tests the get_data() with mocked StateManager responses.""" 73 | mocker.patch( 74 | "pico_lte.utils.manager.StateManager.run", 75 | return_value={"status": Status.ERROR, "response": "", "interval": ""}, 76 | ) 77 | 78 | response = google_sheets_object.get_data( 79 | sheet="sheet", data_range="[DATA_RANGE]" 80 | ) 81 | assert response == {"status": Status.ERROR, "response": ""} 82 | 83 | def test_add_row_success(self, mocker, google_sheets_object): 84 | """This method tests the add_row() with mocked StateManager responses.""" 85 | mocker.patch( 86 | "pico_lte.utils.manager.StateManager.run", 87 | return_value={"status": Status.SUCCESS, "response": "", "interval": ""}, 88 | ) 89 | 90 | response = google_sheets_object.add_row(sheet="sheet", data=[]) 91 | assert response == {"status": Status.SUCCESS, "response": ""} 92 | 93 | def test_add_row_error(self, mocker, google_sheets_object): 94 | """This method tests the add_row() with mocked StateManager responses.""" 95 | mocker.patch( 96 | "pico_lte.utils.manager.StateManager.run", 97 | return_value={"status": Status.ERROR, "response": "", "interval": ""}, 98 | ) 99 | 100 | response = google_sheets_object.add_row(sheet="sheet", data=[]) 101 | assert response == {"status": Status.ERROR, "response": ""} 102 | 103 | def test_add_data_success(self, mocker, google_sheets_object): 104 | """This method tests the add_data() with mocked StateManager responses.""" 105 | mocker.patch( 106 | "pico_lte.utils.manager.StateManager.run", 107 | return_value={"status": Status.SUCCESS, "response": "", "interval": ""}, 108 | ) 109 | 110 | response = google_sheets_object.add_data( 111 | sheet="sheet", data=[], data_range="[DATA_RANGE]" 112 | ) 113 | assert response == {"status": Status.SUCCESS, "response": ""} 114 | 115 | def test_add_data_error(self, mocker, google_sheets_object): 116 | """This method tests the add_data() with mocked StateManager responses.""" 117 | mocker.patch( 118 | "pico_lte.utils.manager.StateManager.run", 119 | return_value={"status": Status.ERROR, "response": "", "interval": ""}, 120 | ) 121 | 122 | response = google_sheets_object.add_data( 123 | sheet="sheet", data=[], data_range="[DATA_RANGE]" 124 | ) 125 | assert response == {"status": Status.ERROR, "response": ""} 126 | 127 | def test_create_sheet_success(self, mocker, google_sheets_object): 128 | """This method tests the create_sheet() with mocked StateManager responses.""" 129 | mocker.patch( 130 | "pico_lte.utils.manager.StateManager.run", 131 | return_value={"status": Status.SUCCESS, "response": "", "interval": ""}, 132 | ) 133 | 134 | response = google_sheets_object.create_sheet(sheets=[]) 135 | assert response == {"status": Status.SUCCESS, "response": ""} 136 | 137 | def test_create_sheet_error(self, mocker, google_sheets_object): 138 | """This method tests the create_sheet() with mocked StateManager responses.""" 139 | mocker.patch( 140 | "pico_lte.utils.manager.StateManager.run", 141 | return_value={"status": Status.ERROR, "response": "", "interval": ""}, 142 | ) 143 | 144 | response = google_sheets_object.create_sheet(sheets=[]) 145 | assert response == {"status": Status.ERROR, "response": ""} 146 | 147 | def test_delete_data_success(self, mocker, google_sheets_object): 148 | """This method tests the delete_data() with mocked StateManager responses.""" 149 | mocker.patch( 150 | "pico_lte.utils.manager.StateManager.run", 151 | return_value={"status": Status.SUCCESS, "response": "", "interval": ""}, 152 | ) 153 | 154 | response = google_sheets_object.delete_data( 155 | sheet="sheet", data_range="[DATA_RANGE]" 156 | ) 157 | assert response == {"status": Status.SUCCESS, "response": ""} 158 | 159 | def test_delete_data_error(self, mocker, google_sheets_object): 160 | """This method tests the delete_data() with mocked StateManager responses.""" 161 | mocker.patch( 162 | "pico_lte.utils.manager.StateManager.run", 163 | return_value={"status": Status.ERROR, "response": "", "interval": ""}, 164 | ) 165 | 166 | response = google_sheets_object.delete_data( 167 | sheet="sheet", data_range="[DATA_RANGE]" 168 | ) 169 | assert response == {"status": Status.ERROR, "response": ""} 170 | 171 | def test_generate_access_token_success(self, mocker, google_sheets_object): 172 | """This method tests the generate_access_token() with mocked StateManager responses.""" 173 | mocker.patch( 174 | "pico_lte.utils.manager.StateManager.run", 175 | return_value={ 176 | "status": Status.SUCCESS, 177 | "response": ['{"access_token": "[ACCESS_TOKEN]'], 178 | }, 179 | ) 180 | 181 | response = google_sheets_object.generate_access_token() 182 | assert response == { 183 | "status": Status.SUCCESS, 184 | "response": "Access token is generated.", 185 | } 186 | 187 | def test_generate_access_token_error(self, mocker, google_sheets_object): 188 | """This method tests the generate_access_token() with mocked StateManager responses.""" 189 | mocker.patch( 190 | "pico_lte.utils.manager.StateManager.run", 191 | return_value={ 192 | "status": Status.ERROR, 193 | "response": "", 194 | }, 195 | ) 196 | 197 | response = google_sheets_object.generate_access_token() 198 | assert response == { 199 | "status": Status.ERROR, 200 | "response": "Access token could not be generated.", 201 | } 202 | -------------------------------------------------------------------------------- /pico_lte/modules/base.py: -------------------------------------------------------------------------------- 1 | """ 2 | Module for including base functionalities of PicoLTE module. 3 | For example; power control of modem, basic communication check etc. 4 | """ 5 | 6 | import time 7 | 8 | from machine import Pin 9 | from pico_lte.common import debug 10 | from pico_lte.utils.status import Status 11 | from pico_lte.utils.helpers import get_desired_data 12 | 13 | 14 | class Base: 15 | """ 16 | Class for inculding basic functions of PicoLTE module. 17 | """ 18 | 19 | def __init__(self, atcom): 20 | """ 21 | Constructor for Base class 22 | """ 23 | self.atcom = atcom 24 | # self.module_power = Pin(26, Pin.OUT) 25 | self.powerkey_pin = Pin(17, Pin.OUT) 26 | self.status_pin = Pin(20, Pin.IN) 27 | 28 | def power_off(self): 29 | """ 30 | Function for powering off celullar modem 31 | """ 32 | self.powerkey_pin.value(1) 33 | time.sleep(1) 34 | self.powerkey_pin.value(0) 35 | 36 | def power_on(self): 37 | """ 38 | Function for powering on celullar modem 39 | """ 40 | self.powerkey_pin.value(1) 41 | time.sleep(0.5) 42 | self.powerkey_pin.value(0) 43 | 44 | def power_status(self): 45 | """ 46 | Function for getting power status of modem 47 | 48 | Returns 49 | ------- 50 | power_status : int 51 | Power status of modem (0=on, 1=off) 52 | """ 53 | debug.debug("Power status:", self.status_pin.value()) 54 | return self.status_pin.value() 55 | 56 | def wait_until_status_on(self, timeout=30): 57 | """ 58 | Function for waiting until modem status is on 59 | 60 | Parameters 61 | ---------- 62 | timeout : int, default: 30 63 | Timeout in seconds for waiting. 64 | 65 | Returns 66 | ------- 67 | dict 68 | Result that includes "status" and "response" keys 69 | """ 70 | start_time = time.time() 71 | while time.time() - start_time < timeout: 72 | status = self.power_status() 73 | if status == 0: 74 | return {"status": Status.SUCCESS, "response": "Success"} 75 | time.sleep(1) 76 | return {"status": Status.TIMEOUT, "response": "Timeout"} 77 | 78 | def check_communication(self): 79 | """ 80 | Function for checking modem communication 81 | 82 | Returns 83 | ------- 84 | dict 85 | Result that includes "status" and "response" keys 86 | """ 87 | return self.atcom.send_at_comm("AT") 88 | 89 | def wait_until_modem_ready_to_communicate(self, timeout=30): 90 | """ 91 | Function for waiting until modem is ready to communicate 92 | 93 | Parameters 94 | ---------- 95 | timeout : int, optional 96 | Timeout for waiting. The default is 30. 97 | 98 | Returns 99 | ------- 100 | dict 101 | Result that includes "status" and "response" keys 102 | """ 103 | result_timeout = {"status": Status.TIMEOUT, "response": "timeout"} 104 | 105 | start_time = time.time() 106 | while time.time() - start_time < timeout: 107 | result = self.check_communication() 108 | debug.debug("COM:", result) 109 | if result["status"] == Status.SUCCESS: 110 | return result 111 | time.sleep(1) 112 | 113 | return result_timeout 114 | 115 | def set_echo_off(self): 116 | """ 117 | Function for setting modem echo off 118 | 119 | Returns 120 | ------- 121 | dict 122 | Result that includes "status" and "response" keys 123 | """ 124 | return self.atcom.send_at_comm("ATE0") 125 | 126 | def set_echo_on(self): 127 | """ 128 | Function for setting modem echo on 129 | 130 | Returns 131 | ------- 132 | dict 133 | Result that includes "status" and "response" keys 134 | """ 135 | return self.atcom.send_at_comm("ATE1") 136 | 137 | def check_sim_ready(self): 138 | """ 139 | Function for checking SIM ready status 140 | 141 | Returns 142 | ------- 143 | dict 144 | Result that includes "status" and "response" keys 145 | """ 146 | desired_reponses = ["+CPIN: READY"] 147 | return self.atcom.send_at_comm("AT+CPIN?", desired_reponses) 148 | 149 | def enter_sim_pin_code(self, pin_code): 150 | """ 151 | Function for entering SIM PIN code 152 | 153 | Parameters 154 | ---------- 155 | pin_code : str 156 | SIM PIN code 157 | 158 | Returns 159 | ------- 160 | dict 161 | Result that includes "status" and "response" keys 162 | """ 163 | command = f'AT+CPIN="{pin_code}"' 164 | return self.atcom.send_at_comm(command) 165 | 166 | def get_sim_iccid(self): 167 | """ 168 | Function for getting SIM ICCID 169 | 170 | Returns 171 | ------- 172 | dict 173 | Result that includes "status", "response" and "value" keys 174 | """ 175 | command = "AT+QCCID" 176 | result = self.atcom.send_at_comm(command) 177 | return get_desired_data(result, "+QCCID: ") 178 | 179 | #################### 180 | ### Modem Config ### 181 | #################### 182 | def config_network_scan_mode(self, scan_mode=0): 183 | """ 184 | Function for configuring modem network scan mode 185 | 186 | Parameters 187 | ---------- 188 | scan_mode : int 189 | Scan mode (default=0) 190 | * 0 --> Automatic 191 | * 1 --> GSM Only 192 | * 3 --> LTE Only 193 | 194 | Returns 195 | ------- 196 | dict 197 | Result that includes "status" and "response" keys 198 | """ 199 | command = f'AT+QCFG="nwscanmode",{scan_mode}' 200 | return self.atcom.send_at_comm(command) 201 | 202 | def config_network_scan_sequence(self, scan_sequence="00"): 203 | """ 204 | Function for configuring modem scan sequence 205 | 206 | Parameters 207 | ---------- 208 | scan_sequence : str 209 | Scan sequence (default=00) 210 | * 00 --> Automatic (eMTC → NB-IoT → GSM) 211 | * 01 --> GSM 212 | * 02 --> eMTC 213 | * 03 --> NB-IoT 214 | 215 | Returns 216 | ------- 217 | dict 218 | Result that includes "status" and "response" keys 219 | """ 220 | command = f'AT+QCFG="nwscanseq",{scan_sequence}' 221 | return self.atcom.send_at_comm(command) 222 | 223 | def config_network_iot_operation_mode(self, iotopmode=2): 224 | """ 225 | Function for configuring modem IoT operation mode 226 | 227 | Parameters 228 | ---------- 229 | iotopmode : int 230 | Operation mode (default=2) 231 | * 0 --> eMTC 232 | * 1 --> NB-IoT 233 | * 2 --> eMTC and NB-IoT 234 | 235 | Returns 236 | ------- 237 | dict 238 | Result that includes "status" and "response" keys 239 | """ 240 | command = f'AT+QCFG="iotopmode",{iotopmode}' 241 | return self.atcom.send_at_comm(command) 242 | 243 | #################### 244 | ### CellularTech ### 245 | #################### 246 | def get_cell_information(self, cell_type): 247 | """ 248 | Function for getting cell information 249 | 250 | Parameters 251 | ---------- 252 | cell_type : str 253 | Cell type ("servingcell" or "neighbourcell") 254 | 255 | Returns 256 | ------- 257 | dict 258 | Result that includes "status" and "response" keys. 259 | """ 260 | if cell_type not in ["servingcell", "neighbourcell"]: 261 | return {"status": Status.ERROR, "response": "Invalid cell type"} 262 | 263 | command = f'AT+QENG="{cell_type}"' 264 | return self.atcom.send_at_comm(command) 265 | 266 | def get_all_cells(self, technology="eMTC", timeout=60): 267 | """ 268 | Function for getting all cells 269 | 270 | Parameters 271 | ---------- 272 | technology : str 273 | Technology (default="eMTC") 274 | * "GSM" 275 | * "eMTC" 276 | * "NBIoT" 277 | 278 | Returns 279 | ------- 280 | dict 281 | Result that includes "status" and "response" keys. 282 | """ 283 | if technology == "GSM": 284 | technology_no = 1 285 | elif technology == "eMTC": 286 | technology_no = 8 287 | elif technology == "NBIoT": 288 | technology_no = 9 289 | else: 290 | return {"status": Status.ERROR, "response": "Invalid technology"} 291 | 292 | # TODO: Get all the information from the URC, not the first one. 293 | command = f"AT+QCELLSCAN={technology_no},{timeout}" 294 | return self.atcom.send_at_comm( 295 | command, timeout=timeout, urc=True, desired='+QCELLSCAN: "{technology}",' 296 | ) 297 | -------------------------------------------------------------------------------- /tests/test_modules_ssl.py: -------------------------------------------------------------------------------- 1 | """ 2 | Test module for the modules.ssl module. 3 | """ 4 | 5 | import pytest 6 | 7 | from pico_lte.modules.ssl import SSL 8 | from pico_lte.utils.atcom import ATCom 9 | from pico_lte.utils.status import Status 10 | 11 | 12 | def default_response_types(): 13 | """This method returns default and mostly-used responses for ATCom messages.""" 14 | return [ 15 | {"status": Status.SUCCESS, "response": ["OK"]}, 16 | {"status": Status.TIMEOUT, "response": "timeout"}, 17 | ] 18 | 19 | 20 | class TestSSL: 21 | """ 22 | Test class for SSL. 23 | """ 24 | 25 | @pytest.fixture 26 | def ssl(self): 27 | """This method returns a SSL instance.""" 28 | atcom = ATCom() 29 | return SSL(atcom) 30 | 31 | @staticmethod 32 | def mock_send_at_comm(mocker, responses_to_return): 33 | """This is a wrapper function to repeated long mocker.patch() statements.""" 34 | return mocker.patch("pico_lte.utils.atcom.ATCom.send_at_comm", return_value=responses_to_return) 35 | 36 | @pytest.mark.parametrize("mocked_response", default_response_types()) 37 | def test_set_ca_cert_with_default_parameters(self, mocker, ssl, mocked_response): 38 | """This method tests set_ca_cert() with its default parameters.""" 39 | mocking = TestSSL.mock_send_at_comm(mocker, mocked_response) 40 | result = ssl.set_ca_cert() 41 | 42 | mocking.assert_called_once_with('AT+QSSLCFG="cacert",2,"/security/cacert.pem"') 43 | assert result == mocked_response 44 | 45 | @pytest.mark.parametrize("mocked_response", default_response_types()) 46 | def test_set_ca_cert_with_different_parameters(self, mocker, ssl, mocked_response): 47 | """This method tests set_ca_cert() with its default parameters.""" 48 | mocking = TestSSL.mock_send_at_comm(mocker, mocked_response) 49 | result = ssl.set_ca_cert(1, "some/path.crt") 50 | 51 | mocking.assert_called_once_with('AT+QSSLCFG="cacert",1,"some/path.crt"') 52 | assert result == mocked_response 53 | 54 | @pytest.mark.parametrize("mocked_response", default_response_types()) 55 | def test_set_client_cert_with_default_parameters(self, mocker, ssl, mocked_response): 56 | """This method tests set_ca_cert() with its default parameters.""" 57 | mocking = TestSSL.mock_send_at_comm(mocker, mocked_response) 58 | result = ssl.set_client_cert() 59 | 60 | mocking.assert_called_once_with('AT+QSSLCFG="clientcert",2,"/security/client.pem"') 61 | assert result == mocked_response 62 | 63 | @pytest.mark.parametrize("mocked_response", default_response_types()) 64 | def test_set_client_cert_with_different_parameters(self, mocker, ssl, mocked_response): 65 | """This method tests set_ca_cert() with its default parameters.""" 66 | mocking = TestSSL.mock_send_at_comm(mocker, mocked_response) 67 | result = ssl.set_client_cert(1, "some/path.crt") 68 | 69 | mocking.assert_called_once_with('AT+QSSLCFG="clientcert",1,"some/path.crt"') 70 | assert result == mocked_response 71 | 72 | @pytest.mark.parametrize("mocked_response", default_response_types()) 73 | def test_set_client_key_with_default_parameters(self, mocker, ssl, mocked_response): 74 | """This method tests set_ca_cert() with its default parameters.""" 75 | mocking = TestSSL.mock_send_at_comm(mocker, mocked_response) 76 | result = ssl.set_client_key() 77 | 78 | mocking.assert_called_once_with('AT+QSSLCFG="clientkey",2,"/security/user_key.pem"') 79 | assert result == mocked_response 80 | 81 | @pytest.mark.parametrize("mocked_response", default_response_types()) 82 | def test_set_client_key_with_different_parameters(self, mocker, ssl, mocked_response): 83 | """This method tests set_ca_cert() with its default parameters.""" 84 | mocking = TestSSL.mock_send_at_comm(mocker, mocked_response) 85 | result = ssl.set_client_key(1, "some/path.crt") 86 | 87 | mocking.assert_called_once_with('AT+QSSLCFG="clientkey",1,"some/path.crt"') 88 | assert result == mocked_response 89 | 90 | @pytest.mark.parametrize("mocked_response", default_response_types()) 91 | def test_set_sec_level_with_default_parameters(self, mocker, ssl, mocked_response): 92 | """This method tests set_sec_level() with its default parameters.""" 93 | mocking = TestSSL.mock_send_at_comm(mocker, mocked_response) 94 | result = ssl.set_sec_level() 95 | 96 | mocking.assert_called_once_with('AT+QSSLCFG="seclevel",2,2') 97 | assert result == mocked_response 98 | 99 | @pytest.mark.parametrize("mocked_response", default_response_types()) 100 | def test_set_sec_level_with_different_parameters(self, mocker, ssl, mocked_response): 101 | """This method tests set_sec_level() with its default parameters.""" 102 | mocking = TestSSL.mock_send_at_comm(mocker, mocked_response) 103 | result = ssl.set_sec_level(1, 3) 104 | 105 | mocking.assert_called_once_with('AT+QSSLCFG="seclevel",1,3') 106 | assert result == mocked_response 107 | 108 | @pytest.mark.parametrize("mocked_response", default_response_types()) 109 | def test_set_version_with_default_parameters(self, mocker, ssl, mocked_response): 110 | """This method tests set_version() with its default parameters.""" 111 | mocking = TestSSL.mock_send_at_comm(mocker, mocked_response) 112 | result = ssl.set_version() 113 | 114 | mocking.assert_called_once_with('AT+QSSLCFG="sslversion",2,4') 115 | assert result == mocked_response 116 | 117 | @pytest.mark.parametrize("mocked_response", default_response_types()) 118 | def test_set_version_with_different_parameters(self, mocker, ssl, mocked_response): 119 | """This method tests set_version() with its default parameters.""" 120 | mocking = TestSSL.mock_send_at_comm(mocker, mocked_response) 121 | result = ssl.set_version(1, 3) 122 | 123 | mocking.assert_called_once_with('AT+QSSLCFG="sslversion",1,3') 124 | assert result == mocked_response 125 | 126 | @pytest.mark.parametrize("mocked_response", default_response_types()) 127 | def test_set_cipher_suite_with_default_parameters(self, mocker, ssl, mocked_response): 128 | """This method tests set_cipher_suite() with its default parameters.""" 129 | mocking = TestSSL.mock_send_at_comm(mocker, mocked_response) 130 | result = ssl.set_cipher_suite() 131 | 132 | mocking.assert_called_once_with('AT+QSSLCFG="ciphersuite",2,0xFFFF') 133 | assert result == mocked_response 134 | 135 | @pytest.mark.parametrize("mocked_response", default_response_types()) 136 | def test_set_cipher_suite_with_different_parameters(self, mocker, ssl, mocked_response): 137 | """This method tests set_cipher_suite() with its default parameters.""" 138 | mocking = TestSSL.mock_send_at_comm(mocker, mocked_response) 139 | result = ssl.set_cipher_suite(1, "0X0004") 140 | 141 | mocking.assert_called_once_with('AT+QSSLCFG="ciphersuite",1,0X0004') 142 | assert result == mocked_response 143 | 144 | @pytest.mark.parametrize("mocked_response", default_response_types()) 145 | def test_set_ignore_local_time_with_default_parameters(self, mocker, ssl, mocked_response): 146 | """This method tests set_ignore_local_time() with its default parameters.""" 147 | mocking = TestSSL.mock_send_at_comm(mocker, mocked_response) 148 | result = ssl.set_ignore_local_time() 149 | 150 | mocking.assert_called_once_with('AT+QSSLCFG="ignorelocaltime",2,1') 151 | assert result == mocked_response 152 | 153 | @pytest.mark.parametrize("mocked_response", default_response_types()) 154 | def test_set_ignore_local_time_with_different_parameters(self, mocker, ssl, mocked_response): 155 | """This method tests set_ignore_local_time() with its default parameters.""" 156 | mocking = TestSSL.mock_send_at_comm(mocker, mocked_response) 157 | result = ssl.set_ignore_local_time(1, 2) 158 | 159 | mocking.assert_called_once_with('AT+QSSLCFG="ignorelocaltime",1,2') 160 | assert result == mocked_response 161 | 162 | def test_configure_for_x509_certification_all_success_case(self, mocker, ssl): 163 | """This method tests the state manager with mocking ATCom, and checks if 164 | they all called. 165 | """ 166 | mocked_response = {"status": Status.SUCCESS, "response": "not important"} 167 | mocking = TestSSL.mock_send_at_comm(mocker, mocked_response) 168 | result = ssl.configure_for_x509_certification() 169 | 170 | mocking.assert_any_call('AT+QSSLCFG="cacert",2,"/security/cacert.pem"') 171 | mocking.assert_any_call('AT+QSSLCFG="clientcert",2,"/security/client.pem"') 172 | mocking.assert_any_call('AT+QSSLCFG="clientkey",2,"/security/user_key.pem"') 173 | mocking.assert_any_call('AT+QSSLCFG="seclevel",2,2') 174 | mocking.assert_any_call('AT+QSSLCFG="sslversion",2,4') 175 | mocking.assert_any_call('AT+QSSLCFG="ciphersuite",2,0xFFFF') 176 | mocking.assert_any_call('AT+QSSLCFG="ignorelocaltime",2,1') 177 | assert result["status"] == mocked_response["status"] 178 | assert result["response"] == mocked_response["response"] 179 | 180 | def test_configure_for_x509_certification_fail(self, mocker, ssl): 181 | """This method tests the state manager with mocking ATCom as failed response. 182 | Hence, we can understand if failed responses also returned. 183 | """ 184 | mocked_response = {"status": Status.ERROR, "response": "not important"} 185 | TestSSL.mock_send_at_comm(mocker, mocked_response) 186 | result = ssl.configure_for_x509_certification() 187 | 188 | assert result["status"] == mocked_response["status"] 189 | assert result["response"] == mocked_response["response"] 190 | -------------------------------------------------------------------------------- /tests/test_modules_base.py: -------------------------------------------------------------------------------- 1 | """ 2 | Test module for the modules.base module. 3 | """ 4 | 5 | import pytest 6 | from machine import Pin 7 | 8 | from pico_lte.modules.base import Base 9 | from pico_lte.utils.atcom import ATCom 10 | from pico_lte.utils.status import Status 11 | 12 | 13 | class TestBase: 14 | """ 15 | Test class for Base. 16 | """ 17 | 18 | @pytest.fixture 19 | def base(self): 20 | """This fixture returns a Base instance.""" 21 | atcom = ATCom() 22 | return Base(atcom) 23 | 24 | def test_init_constructor(self, base): 25 | """This method tests the atcom attribute.""" 26 | assert isinstance(base.atcom, ATCom) 27 | 28 | def test_power_on(self, mocker, base): 29 | """This method tests power_on() method.""" 30 | mocker.patch("time.sleep") 31 | mocking = mocker.patch("machine.Pin.value") 32 | 33 | base.power_on() 34 | 35 | assert mocking.call_count == 2 36 | mocking.assert_any_call(1) 37 | mocking.assert_any_call(0) 38 | 39 | def test_power_off(self, mocker, base): 40 | """This method tests power_off() method.""" 41 | mocker.patch("time.sleep") 42 | mocking = mocker.patch("machine.Pin.value") 43 | 44 | base.power_off() 45 | 46 | assert mocking.call_count == 2 47 | mocking.assert_any_call(1) 48 | mocking.assert_any_call(0) 49 | 50 | @pytest.mark.parametrize("status_pin_value", [0, 1]) 51 | def test_power_status_response(self, mocker, base, status_pin_value): 52 | """This method tests the power_status() method by mocking Pin value.""" 53 | mocking = mocker.patch("pico_lte.modules.base.Pin.value", return_value=status_pin_value) 54 | result = base.power_status() 55 | 56 | assert mocking.call_count == 2 57 | assert result == status_pin_value 58 | 59 | @pytest.mark.parametrize( 60 | "start_time, stop_time, power_status", [(0, 15, 0), (0, 31, 0), (0, 31, 1)] 61 | ) 62 | def test_wait_until_status_on_with_default_timeout( 63 | self, mocker, base, start_time, stop_time, power_status 64 | ): 65 | """This method tests the wait_status_on() with mocked time.""" 66 | mocker.patch("time.sleep") 67 | mocker.patch("time.time", side_effect=[start_time, stop_time, stop_time + 30]) 68 | mocker.patch("pico_lte.modules.base.Base.power_status", return_value=power_status) 69 | 70 | result = base.wait_until_status_on() 71 | 72 | # If the power_status is not 0, and timeout has reached, we may expect timeout response. 73 | expected_result = {"status": Status.TIMEOUT, "response": "Timeout"} 74 | if power_status == 0 and stop_time - start_time < 30: 75 | expected_result = {"status": Status.SUCCESS, "response": "Success"} 76 | 77 | assert result == expected_result 78 | 79 | @pytest.mark.parametrize( 80 | "mocked_result", 81 | [ 82 | {"status": Status.SUCCESS, "response": "OK"}, 83 | {"status": Status.SUCCESS, "response": "APP RDY"}, 84 | {"status": Status.TIMEOUT, "response": "timeout"}, 85 | ], 86 | ) 87 | def test_check_communication(self, mocker, base, mocked_result): 88 | """This method tests check_communication() with mocked ATCom.""" 89 | mocking = mocker.patch("pico_lte.utils.atcom.ATCom.send_at_comm", return_value=mocked_result) 90 | result = base.check_communication() 91 | 92 | mocking.assert_called_once_with("AT") 93 | assert result == mocked_result 94 | 95 | @pytest.mark.parametrize( 96 | "start_time, stop_time, mocked_result", 97 | [ 98 | (0, 15, {"status": Status.SUCCESS, "response": "OK"}), 99 | (0, 29, {"status": Status.ERROR, "reponse": "error"}), 100 | (0, 31, {"status": Status.ERROR, "reponse": "error"}), 101 | ], 102 | ) 103 | def test_wait_until_modem_ready_to_communicate( 104 | self, mocker, base, start_time, stop_time, mocked_result 105 | ): 106 | """This method tests the wait_until_modem_ready_to_communicate() with mocked time.""" 107 | mocker.patch("time.sleep") 108 | mocker.patch("time.time", side_effect=[start_time, stop_time, stop_time + 30]) 109 | mocker.patch("pico_lte.utils.atcom.ATCom.send_at_comm", return_value=mocked_result) 110 | 111 | result = base.wait_until_modem_ready_to_communicate() 112 | 113 | # If the status is ERROR, or the timeout reached, we may expect a timeout response. 114 | expected_result = {"status": Status.TIMEOUT, "response": "timeout"} 115 | if mocked_result["status"] == Status.SUCCESS and stop_time - start_time < 30: 116 | expected_result = {"status": Status.SUCCESS, "response": "OK"} 117 | 118 | assert result == expected_result 119 | 120 | @pytest.mark.parametrize( 121 | "mocked_result", 122 | [ 123 | {"status": Status.SUCCESS, "response": "OK"}, 124 | {"status": Status.TIMEOUT, "response": "timeout"}, 125 | ], 126 | ) 127 | def test_set_echo_off(self, mocker, base, mocked_result): 128 | """This method tests the set_echo_off() with mocked ATCom.""" 129 | mocking = mocker.patch("pico_lte.utils.atcom.ATCom.send_at_comm", return_value=mocked_result) 130 | 131 | result = base.set_echo_off() 132 | mocking.assert_called_once_with("ATE0") 133 | assert result == mocked_result 134 | 135 | @pytest.mark.parametrize( 136 | "mocked_result", 137 | [ 138 | {"status": Status.SUCCESS, "response": "OK"}, 139 | {"status": Status.TIMEOUT, "response": "timeout"}, 140 | ], 141 | ) 142 | def test_set_echo_on(self, mocker, base, mocked_result): 143 | """This method tests the set_echo_on() with mocked ATCom.""" 144 | mocking = mocker.patch("pico_lte.utils.atcom.ATCom.send_at_comm", return_value=mocked_result) 145 | 146 | result = base.set_echo_on() 147 | mocking.assert_called_once_with("ATE1") 148 | assert result == mocked_result 149 | 150 | @pytest.mark.parametrize( 151 | "mocked_result", 152 | [ 153 | {"status": Status.SUCCESS, "response": ["+CPIN: READY", "OK"]}, 154 | {"status": Status.TIMEOUT, "response": "timeout"}, 155 | ], 156 | ) 157 | def test_check_sim_ready(self, mocker, base, mocked_result): 158 | """This method tests the check_sim_ready() with mocked ATCom.""" 159 | mocking = mocker.patch("pico_lte.utils.atcom.ATCom.send_at_comm", return_value=mocked_result) 160 | 161 | result = base.check_sim_ready() 162 | mocking.assert_called_once_with("AT+CPIN?", ["+CPIN: READY"]) 163 | assert result == mocked_result 164 | 165 | @pytest.mark.parametrize( 166 | "mocked_result", 167 | [ 168 | {"status": Status.SUCCESS, "response": ["+CPIN: READY", "OK"]}, 169 | {"status": Status.TIMEOUT, "response": "timeout"}, 170 | ], 171 | ) 172 | def test_enter_sim_pin_code(self, mocker, base, mocked_result): 173 | """This method tests the enter_sim_pin_code() with mocked ATCom.""" 174 | mocking = mocker.patch("pico_lte.utils.atcom.ATCom.send_at_comm", return_value=mocked_result) 175 | 176 | result = base.enter_sim_pin_code(1234) 177 | mocking.assert_called_once_with('AT+CPIN="1234"') 178 | assert result == mocked_result 179 | 180 | @pytest.mark.parametrize( 181 | "mocked_result", 182 | [ 183 | { 184 | "status": Status.SUCCESS, 185 | "response": ["+QCCID: 12345678910111213140", "OK"], 186 | }, 187 | {"status": Status.TIMEOUT, "response": "timeout"}, 188 | ], 189 | ) 190 | def test_get_sim_iccid(self, mocker, base, mocked_result): 191 | """This method tests the get_sim_iccid() method with mocked ATCom.""" 192 | mocking = mocker.patch("pico_lte.utils.atcom.ATCom.send_at_comm", return_value=mocked_result) 193 | 194 | result = base.get_sim_iccid() 195 | 196 | mocking.assert_called_once_with("AT+QCCID") 197 | assert result["status"] == mocked_result["status"] 198 | assert result["response"] == mocked_result["response"] 199 | assert result["value"] in ["12345678910111213140", None] 200 | 201 | @pytest.mark.parametrize("scan_mode", [0, 1, 3]) 202 | def test_config_network_scan_mode(self, mocker, base, scan_mode): 203 | """This method tests the config_network_scan_mode() method with mocked ATCom.""" 204 | mocked_result = {"status": Status.SUCCESS, "response": ["OK"]} 205 | mocking = mocker.patch("pico_lte.utils.atcom.ATCom.send_at_comm", return_value=mocked_result) 206 | 207 | result = base.config_network_scan_mode(scan_mode) 208 | 209 | mocking.assert_called_once_with(f'AT+QCFG="nwscanmode",{scan_mode}') 210 | assert result == mocked_result 211 | 212 | @pytest.mark.parametrize("scan_sequence", ["00", "01", "02", "03"]) 213 | def test_config_network_scan_sequence(self, mocker, base, scan_sequence): 214 | """This method tests the config_network_scan_sequence() method with mocked ATCom.""" 215 | mocked_result = {"status": Status.SUCCESS, "response": ["OK"]} 216 | mocking = mocker.patch("pico_lte.utils.atcom.ATCom.send_at_comm", return_value=mocked_result) 217 | 218 | result = base.config_network_scan_sequence(scan_sequence) 219 | 220 | mocking.assert_called_once_with(f'AT+QCFG="nwscanseq",{scan_sequence}') 221 | assert result == mocked_result 222 | 223 | @pytest.mark.parametrize("iotopmode", [0, 1, 2]) 224 | def test_config_network_iot_operation_mode(self, mocker, base, iotopmode): 225 | """This method tests the config_network_iot_operation_mode() method with mocked ATCom.""" 226 | mocked_result = {"status": Status.SUCCESS, "response": ["OK"]} 227 | mocking = mocker.patch("pico_lte.utils.atcom.ATCom.send_at_comm", return_value=mocked_result) 228 | 229 | result = base.config_network_iot_operation_mode(iotopmode) 230 | 231 | mocking.assert_called_once_with(f'AT+QCFG="iotopmode",{iotopmode}') 232 | assert result == mocked_result 233 | -------------------------------------------------------------------------------- /pico_lte/modules/ssl.py: -------------------------------------------------------------------------------- 1 | """ 2 | Module for including functions of ssl operations of PicoLTE module. 3 | """ 4 | 5 | import time 6 | 7 | from pico_lte.utils.manager import StateManager, Step 8 | from pico_lte.utils.status import Status 9 | 10 | 11 | class SSL: 12 | """ 13 | Class for including functions of ssl operations of PicoLTE module. 14 | """ 15 | 16 | def __init__(self, atcom): 17 | """ 18 | Initialization of the class. 19 | """ 20 | self.atcom = atcom 21 | 22 | def set_ca_cert(self, ssl_context_id=2, file_path="/security/cacert.pem"): 23 | """ 24 | Function for setting modem CA certificate 25 | 26 | Parameters 27 | ---------- 28 | ssl_context_id : int 29 | SSL context identifier 30 | 31 | file_path : str, default: "/security/cacert.pem" 32 | Path to the CA certificate file 33 | 34 | Returns 35 | ------- 36 | dict 37 | Result that includes "status" and "response" keys 38 | """ 39 | command = f'AT+QSSLCFG="cacert",{ssl_context_id},"{file_path}"' 40 | return self.atcom.send_at_comm(command) 41 | 42 | def set_client_cert(self, ssl_context_id=2, file_path="/security/client.pem"): 43 | """ 44 | Function for setting modem client certificate 45 | 46 | Parameters 47 | ---------- 48 | ssl_context_id : int 49 | SSL context identifier 50 | 51 | file_path : str, default: "/security/client.pem" 52 | Path to the client certificate file 53 | 54 | Returns 55 | ------- 56 | dict 57 | Result that includes "status" and "response" keys 58 | """ 59 | command = f'AT+QSSLCFG="clientcert",{ssl_context_id},"{file_path}"' 60 | return self.atcom.send_at_comm(command) 61 | 62 | def set_client_key(self, ssl_context_id=2, file_path="/security/user_key.pem"): 63 | """ 64 | Function for setting modem client key 65 | 66 | Parameters 67 | ---------- 68 | ssl_context_id : int 69 | SSL context identifier 70 | 71 | file_path : str, default: "/security/user_key.pem" 72 | Path to the client key file 73 | 74 | Returns 75 | ------- 76 | dict 77 | Result that includes "status" and "response" keys 78 | """ 79 | command = f'AT+QSSLCFG="clientkey",{ssl_context_id},"{file_path}"' 80 | return self.atcom.send_at_comm(command) 81 | 82 | def set_sec_level(self, ssl_context_id=2, sec_level=2): 83 | """ 84 | Function for setting modem security level 85 | 86 | Parameters 87 | ---------- 88 | ssl_context_id : int, default: 2 89 | SSL context identifier 90 | 91 | sec_level : int 92 | SSL Security level 93 | * 0 --> No authentication 94 | * 1 --> Perform server authentication 95 | * 2 --> Perform server and client authentication if requested by the remote server 96 | 97 | Returns 98 | ------- 99 | dict 100 | Result that includes "status" and "response" keys 101 | """ 102 | command = f'AT+QSSLCFG="seclevel",{ssl_context_id},{sec_level}' 103 | return self.atcom.send_at_comm(command) 104 | 105 | def set_version(self, ssl_context_id=2, ssl_version=4): 106 | """ 107 | Function for setting modem SSL version 108 | 109 | Parameters 110 | ---------- 111 | ssl_context_id : int, default: 2 112 | SSL context identifier 113 | 114 | ssl_version : int 115 | SSL version (default=4) 116 | * 0 --> SSL3.0 117 | * 1 --> TLS1.0 118 | * 2 --> TLS1.1 119 | * 3 --> TLS1.2 120 | * 4 --> All 121 | 122 | Returns 123 | ------- 124 | dict 125 | Result that includes "status" and "response" keys 126 | """ 127 | command = f'AT+QSSLCFG="sslversion",{ssl_context_id},{ssl_version}' 128 | return self.atcom.send_at_comm(command) 129 | 130 | def set_cipher_suite(self, ssl_context_id=2, cipher_suite="0xFFFF"): 131 | """ 132 | Function for setting modem SSL cipher suite 133 | 134 | Parameters 135 | ---------- 136 | ssl_context_id : int, default: 2 137 | SSL context identifier 138 | 139 | cipher_suite : str, default: "0xFFFF" 140 | SSL Cipher suite. 141 | * 0X0035 --> TLS_RSA_WITH_AES_256_CBC_SHA 142 | * 0X002F --> TLS_RSA_WITH_AES_128_CBC_SHA 143 | * 0X0005 --> TLS_RSA_WITH_RC4_128_SHA 144 | * 0X0004 --> TLS_RSA_WITH_RC4_128_MD5 145 | * 0X000A --> TLS_RSA_WITH_3DES_EDE_CBC_SHA 146 | * 0X003D --> TLS_RSA_WITH_AES_256_CBC_SHA256 147 | * 0XC002 --> TLS_ECDH_ECDSA_WITH_RC4_128_SHA 148 | * 0XC003 --> TLS_ECDH_ECDSA_WITH_3DES_EDE_CBC_SHA 149 | * 0XC004 --> TLS_ECDH_ECDSA_WITH_AES_128_CBC_SHA 150 | * 0XC005 --> TLS_ECDH_ECDSA_WITH_AES_256_CBC_SHA 151 | * 0XC007 --> TLS_ECDHE_ECDSA_WITH_RC4_128_SHA 152 | * 0XC008 --> TLS_ECDHE_ECDSA_WITH_3DES_EDE_CBC_SHA 153 | * 0XC009 --> TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA 154 | * 0XC00A --> TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA 155 | * 0XC011 --> TLS_ECDHE_RSA_WITH_RC4_128_SHA 156 | * 0XC012 --> TLS_ECDHE_RSA_WITH_3DES_EDE_CBC_SHA 157 | * 0XC013 --> TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA 158 | * 0XC014 --> TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA 159 | * 0XC00C --> TLS_ECDH_RSA_WITH_RC4_128_SHA 160 | * 0XC00D --> TLS_ECDH_RSA_WITH_3DES_EDE_CBC_SHA 161 | * 0XC00E --> TLS_ECDH_RSA_WITH_AES_128_CBC_SHA 162 | * 0XC00F --> TLS_ECDH_RSA_WITH_AES_256_CBC_SHA 163 | * 0XC023 --> TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA256 164 | * 0XC024 --> TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA384 165 | * 0XC025 --> TLS_ECDH_ECDSA_WITH_AES_128_CBC_SHA256 166 | * 0XC026 --> TLS_ECDH_ECDSA_WITH_AES_256_CBC_SHA384 167 | * 0XC027 --> TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA256 168 | * 0XC028 --> TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA384 169 | * 0XC029 --> TLS_ECDH_RSA_WITH_AES_128_CBC_SHA256 170 | * 0XC02A --> TLS_ECDH_RSA_WITH_AES_256_CBC_SHA384 171 | * 0XC02B --> TLS-ECDHE-ECDSA-WITH-AES-128-GCM-SHA256 172 | * 0XC02F --> TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256 173 | * 0XC0A8 --> TLS_PSK_WITH_AES_128_CCM_8 174 | * 0X00AE --> TLS_PSK_WITH_AES_128_CBC_SHA256 175 | * 0XC0AE --> TLS_ECDHE_ECDSA_WITH_AES_128_CCM_8 176 | * 0XFFFF --> Support all cipher suites 177 | 178 | Returns 179 | ------- 180 | dict 181 | Result that includes "status" and "response" keys 182 | """ 183 | command = f'AT+QSSLCFG="ciphersuite",{ssl_context_id},{cipher_suite}' 184 | return self.atcom.send_at_comm(command) 185 | 186 | def set_ignore_local_time(self, ssl_context_id=2, ignore_local_time=1): 187 | """ 188 | Function for setting modem SSL ignore local time 189 | 190 | Parameters 191 | ---------- 192 | ssl_context_id : int, default: 2 193 | SSL context identifier 194 | 195 | ignore_local_time : int, default: 1 196 | Ignore local time 197 | * 0 --> Do not ignore local time 198 | * 1 --> Ignore local time 199 | 200 | Returns 201 | ------- 202 | dict 203 | Result that includes "status" and "response" keys 204 | """ 205 | command = f'AT+QSSLCFG="ignorelocaltime",{ssl_context_id},{ignore_local_time}' 206 | return self.atcom.send_at_comm(command) 207 | 208 | def configure_for_x509_certification(self): 209 | """ 210 | Function for configuring the modem for X.509 certification. 211 | 212 | Returns 213 | ------- 214 | dict 215 | Result that includes "status" and "response" keys 216 | """ 217 | 218 | step_set_ca = Step( 219 | function=self.set_ca_cert, 220 | name="set_ca", 221 | success="set_client_cert", 222 | fail="failure", 223 | ) 224 | 225 | step_set_client_cert = Step( 226 | function=self.set_client_cert, 227 | name="set_client_cert", 228 | success="set_client_key", 229 | fail="failure", 230 | ) 231 | 232 | step_set_client_key = Step( 233 | function=self.set_client_key, 234 | name="set_client_key", 235 | success="set_sec_level", 236 | fail="failure", 237 | ) 238 | 239 | step_set_sec_level = Step( 240 | function=self.set_sec_level, 241 | name="set_sec_level", 242 | success="set_ssl_ver", 243 | fail="failure", 244 | ) 245 | 246 | step_set_ssl_ver = Step( 247 | function=self.set_version, 248 | name="set_ssl_ver", 249 | success="set_ssl_ciphers", 250 | fail="failure", 251 | ) 252 | 253 | step_set_ssl_ciphers = Step( 254 | function=self.set_cipher_suite, 255 | name="set_ssl_ciphers", 256 | success="set_ignore_local_time", 257 | fail="failure", 258 | ) 259 | 260 | step_set_ignore_local_time = Step( 261 | function=self.set_ignore_local_time, 262 | name="set_ignore_local_time", 263 | success="success", 264 | fail="failure", 265 | ) 266 | 267 | sm = StateManager(first_step=step_set_ca) 268 | sm.add_step(step_set_ca) 269 | sm.add_step(step_set_client_cert) 270 | sm.add_step(step_set_client_key) 271 | sm.add_step(step_set_sec_level) 272 | sm.add_step(step_set_ssl_ver) 273 | sm.add_step(step_set_ssl_ciphers) 274 | sm.add_step(step_set_ignore_local_time) 275 | 276 | while True: 277 | result = sm.run() 278 | 279 | if result["status"] == Status.SUCCESS: 280 | return result 281 | elif result["status"] == Status.ERROR: 282 | return result 283 | time.sleep(result["interval"]) 284 | -------------------------------------------------------------------------------- /pico_lte/apps/thingspeak.py: -------------------------------------------------------------------------------- 1 | """ 2 | Module for including functions of ThingSpeak for PicoLTE module. 3 | """ 4 | import time 5 | 6 | from pico_lte.common import config 7 | from pico_lte.utils.manager import StateManager, Step 8 | from pico_lte.utils.status import Status 9 | from pico_lte.utils.helpers import get_parameter 10 | 11 | 12 | class ThingSpeak: 13 | """ 14 | Class for including functions of ThingSpeak operations for PicoLTE module. 15 | """ 16 | 17 | cache = config["cache"] 18 | 19 | def __init__(self, base, network, mqtt, channel_id=None): 20 | """Constructor of the class. 21 | 22 | Parameters 23 | ---------- 24 | base : Base 25 | Modem Base instance 26 | network : Network 27 | Modem Network instance 28 | mqtt : MQTT 29 | Modem MQTT instance 30 | """ 31 | self.base = base 32 | self.network = network 33 | self.mqtt = mqtt 34 | self.channel_id = ( 35 | get_parameter(["thingspeak", "channel_id"]) if (channel_id is None) else channel_id 36 | ) 37 | 38 | def publish_message( 39 | self, 40 | payload, 41 | host=None, 42 | port=None, 43 | topic=None, 44 | client_id=None, 45 | username=None, 46 | password=None, 47 | ): 48 | """ 49 | Function for publishing a message to ThingSpeak. 50 | 51 | Parameters 52 | ---------- 53 | payload : str 54 | Payload of the message. 55 | host : str 56 | Host of the MQTT broker. 57 | port : int 58 | Port of the MQTT broker. 59 | topic : str 60 | Topic of the message. 61 | 62 | Returns 63 | ------- 64 | dict 65 | Result that includes "status" and "response" keys 66 | """ 67 | if host is None: 68 | host = get_parameter(["thingspeak", "mqtts", "host"], "mqtt3.thingspeak.com") 69 | 70 | if port is None: 71 | port = get_parameter(["thingspeak", "mqtts", "port"], 1883) 72 | 73 | if client_id is None: 74 | client_id = get_parameter(["thingspeak", "mqtts", "client_id"]) 75 | 76 | if username is None: 77 | username = get_parameter(["thingspeak", "mqtts", "username"]) 78 | 79 | if password is None: 80 | password = get_parameter(["thingspeak", "mqtts", "password"]) 81 | 82 | if topic is None: 83 | topic = get_parameter( 84 | ["thingspeak", "mqtts", "pub_topic"], 85 | "channels/" + str(self.channel_id) + "/publish", 86 | ) 87 | 88 | # Create message from dictionary if needed. 89 | if isinstance(payload, dict): 90 | payload = ThingSpeak.create_message(payload) 91 | 92 | # Check if client is connected to the broker 93 | step_check_mqtt_connected = Step( 94 | function=self.mqtt.is_connected_to_broker, 95 | name="check_connected", 96 | success="publish_message", 97 | fail="check_opened", 98 | ) 99 | 100 | # Check if client connected to Google Cloud IoT 101 | step_check_mqtt_opened = Step( 102 | function=self.mqtt.has_opened_connection, 103 | name="check_opened", 104 | success="connect_mqtt_broker", 105 | fail="register_network", 106 | ) 107 | 108 | # If client is not connected to the broker and have no open connection with 109 | # ThingSpeak, begin the first step of the state machine. 110 | step_network_reg = Step( 111 | function=self.network.register_network, 112 | name="register_network", 113 | success="get_pdp_ready", 114 | fail="failure", 115 | ) 116 | 117 | step_pdp_ready = Step( 118 | function=self.network.get_pdp_ready, 119 | name="get_pdp_ready", 120 | success="open_mqtt_connection", 121 | fail="failure", 122 | ) 123 | 124 | step_open_mqtt_connection = Step( 125 | function=self.mqtt.open_connection, 126 | name="open_mqtt_connection", 127 | success="connect_mqtt_broker", 128 | fail="failure", 129 | function_params={"host": host, "port": port}, 130 | interval=1, 131 | ) 132 | 133 | step_connect_mqtt_broker = Step( 134 | function=self.mqtt.connect_broker, 135 | name="connect_mqtt_broker", 136 | success="publish_message", 137 | fail="failure", 138 | function_params={ 139 | "client_id_string": client_id, 140 | "username": username, 141 | "password": password, 142 | }, 143 | ) 144 | 145 | step_publish_message = Step( 146 | function=self.mqtt.publish_message, 147 | name="publish_message", 148 | success="success", 149 | fail="failure", 150 | function_params={"payload": payload, "topic": topic, "qos": 1}, 151 | retry=3, 152 | interval=1, 153 | ) 154 | 155 | # Add cache if it is not already existed 156 | function_name = "thingspeak.publish_message" 157 | 158 | sm = StateManager(first_step=step_check_mqtt_connected, function_name=function_name) 159 | 160 | sm.add_step(step_check_mqtt_connected) 161 | sm.add_step(step_check_mqtt_opened) 162 | sm.add_step(step_network_reg) 163 | sm.add_step(step_pdp_ready) 164 | sm.add_step(step_open_mqtt_connection) 165 | sm.add_step(step_connect_mqtt_broker) 166 | sm.add_step(step_publish_message) 167 | 168 | while True: 169 | result = sm.run() 170 | 171 | if result["status"] == Status.SUCCESS: 172 | return result 173 | elif result["status"] == Status.ERROR: 174 | return result 175 | time.sleep(result["interval"]) 176 | 177 | def subscribe_topics( 178 | self, host=None, port=None, topics=None, client_id=None, username=None, password=None 179 | ): 180 | """ 181 | Function for subscribing to topics of ThingSpeak. 182 | 183 | Parameters 184 | ---------- 185 | topics : list 186 | List of topics. 187 | 188 | Returns 189 | ------- 190 | dict 191 | Result that includes "status" and "response" keys 192 | """ 193 | if host is None: 194 | host = get_parameter(["thingspeak", "mqtts", "host"], "mqtt3.thingspeak.com") 195 | 196 | if port is None: 197 | port = get_parameter(["thingspeak", "mqtts", "port"], 1883) 198 | 199 | if client_id is None: 200 | client_id = get_parameter(["thingspeak", "mqtts", "client_id"]) 201 | 202 | if username is None: 203 | username = get_parameter(["thingspeak", "mqtts", "username"]) 204 | 205 | if password is None: 206 | password = get_parameter(["thingspeak", "mqtts", "password"]) 207 | 208 | if topics is None: 209 | topics = get_parameter( 210 | ["thingspeak", "mqtts", "sub_topics"], 211 | ("channels/" + str(self.channel_id) + "/subscribe/fields/+", 0), 212 | ) 213 | 214 | # Check if client is connected to the broker 215 | step_check_mqtt_connected = Step( 216 | function=self.mqtt.is_connected_to_broker, 217 | name="check_connected", 218 | success="subscribe_topics", 219 | fail="check_opened", 220 | ) 221 | 222 | # Check if client connected to Google Cloud IoT 223 | step_check_mqtt_opened = Step( 224 | function=self.mqtt.has_opened_connection, 225 | name="check_opened", 226 | success="connect_mqtt_broker", 227 | fail="register_network", 228 | ) 229 | 230 | # If client is not connected to the broker and have no open connection with 231 | # ThingSpeak, begin the first step of the state machine. 232 | step_network_reg = Step( 233 | function=self.network.register_network, 234 | name="register_network", 235 | success="get_pdp_ready", 236 | fail="failure", 237 | ) 238 | 239 | step_pdp_ready = Step( 240 | function=self.network.get_pdp_ready, 241 | name="get_pdp_ready", 242 | success="open_mqtt_connection", 243 | fail="failure", 244 | ) 245 | 246 | step_open_mqtt_connection = Step( 247 | function=self.mqtt.open_connection, 248 | name="open_mqtt_connection", 249 | success="connect_mqtt_broker", 250 | fail="failure", 251 | function_params={"host": host, "port": port}, 252 | interval=1, 253 | ) 254 | 255 | step_connect_mqtt_broker = Step( 256 | function=self.mqtt.connect_broker, 257 | name="connect_mqtt_broker", 258 | success="subscribe_topics", 259 | fail="failure", 260 | function_params={ 261 | "client_id_string": client_id, 262 | "username": username, 263 | "password": password, 264 | }, 265 | ) 266 | 267 | step_subscribe_topics = Step( 268 | function=self.mqtt.subscribe_topics, 269 | name="subscribe_topics", 270 | success="success", 271 | fail="failure", 272 | function_params={"topics": topics}, 273 | retry=3, 274 | interval=1, 275 | ) 276 | 277 | # Add cache if it is not already existed 278 | function_name = "thingspeak.subscribe_topics" 279 | 280 | sm = StateManager(first_step=step_check_mqtt_connected, function_name=function_name) 281 | 282 | sm.add_step(step_check_mqtt_connected) 283 | sm.add_step(step_check_mqtt_opened) 284 | sm.add_step(step_network_reg) 285 | sm.add_step(step_pdp_ready) 286 | sm.add_step(step_open_mqtt_connection) 287 | sm.add_step(step_connect_mqtt_broker) 288 | sm.add_step(step_subscribe_topics) 289 | 290 | while True: 291 | result = sm.run() 292 | 293 | if result["status"] == Status.SUCCESS: 294 | return result 295 | elif result["status"] == Status.ERROR: 296 | return result 297 | time.sleep(result["interval"]) 298 | 299 | def read_messages(self): 300 | """ 301 | Read messages from subscribed topics. 302 | """ 303 | return self.mqtt.read_messages() 304 | 305 | @staticmethod 306 | def create_message(payload_dict): 307 | """This function generates a payload message for publishing ThingSpeak messages. 308 | 309 | Parameters 310 | ---------- 311 | payload_dict : dict 312 | A dictionary instance that has "field" keys, 313 | and values. It's also possible to assing a 314 | "status" key. 315 | 316 | Returns 317 | ---------- 318 | payload_string : str 319 | Returns a string similar to URL queries to add 320 | as a payload to mqtt.publish_message function. 321 | """ 322 | payload_string = "" 323 | 324 | if "status" not in payload_dict: 325 | payload_dict["status"] = "MQTT_PicoLTE_PUBLISH" 326 | 327 | for key, value in payload_dict.items(): 328 | payload_string += f"{key}={value}&" 329 | 330 | return payload_string[:-1] 331 | --------------------------------------------------------------------------------