├── 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 |    
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 |
--------------------------------------------------------------------------------