diff --git a/adafruit_tinylora/adafruit_tinylora.py b/adafruit_tinylora/adafruit_tinylora.py index 2533690..8345afc 100755 --- a/adafruit_tinylora/adafruit_tinylora.py +++ b/adafruit_tinylora/adafruit_tinylora.py @@ -27,10 +27,31 @@ import time from random import randint -from micropython import const + import adafruit_bus_device.spi_device +from micropython import const + from adafruit_tinylora.adafruit_tinylora_encryption import AES +try: # typing + from types import TracebackType + from typing import Optional, Type, Union + + import busio + import digitalio + from typing_extensions import Self # Python <3.11 + from typing_extensions import Annotated, TypeAlias + + # type aliases + bytearray2: TypeAlias = Annotated[bytearray, 2] + bytearray4: TypeAlias = Annotated[bytearray, 4] + bytearray16: TypeAlias = Annotated[bytearray, 16] + + registeraddress: TypeAlias = Union[const, int] +except ImportError: + pass + + __version__ = "0.0.0+auto.0" __repo__ = "https://github.com/adafruit/Adafruit_CircuitPython_TinyLoRa.git" @@ -70,7 +91,13 @@ class TTN: """TTN Class""" - def __init__(self, dev_address, net_key, app_key, country="US"): + def __init__( + self, + dev_address: bytearray4, + net_key: bytearray16, + app_key: bytearray16, + country: str = "US", + ): """Interface for TheThingsNetwork :param bytearray dev_address: TTN Device Address. :param bytearray net_key: TTN Network Key. @@ -83,22 +110,22 @@ def __init__(self, dev_address, net_key, app_key, country="US"): self.region = country @property - def country(self): + def country(self) -> str: """Returns the TTN Frequency Country.""" return self.region @property - def device_address(self): + def device_address(self) -> bytearray4: """Returns the TTN Device Address.""" return self.dev_addr @property - def application_key(self): + def application_key(self) -> bytearray16: """Returns the TTN Application Key.""" return self.app_key @property - def network_key(self): + def network_key(self) -> bytearray16: """Returns the TTN Network Key.""" return self.net_key @@ -108,10 +135,18 @@ class TinyLoRa: """TinyLoRa Interface""" # SPI Write Buffer - _BUFFER = bytearray(2) + _BUFFER: bytearray2 = bytearray(2) # pylint: disable=too-many-arguments,invalid-name - def __init__(self, spi, cs, irq, rst, ttn_config, channel=None): + def __init__( + self, + spi: busio.SPI, + cs: digitalio.DigitalInOut, + irq: digitalio.DigitalInOut, + rst: digitalio.DigitalInOut, + ttn_config: digitalio.DigitalInOut, + channel: Optional[int] = None, + ): """Interface for a HopeRF RFM95/6/7/8(w) radio module. Sets module up for sending to The Things Network. @@ -141,13 +176,13 @@ def __init__(self, spi, cs, irq, rst, ttn_config, channel=None): if self._version != 18: raise TypeError("Can not detect LoRa Module. Please check wiring!") # Set Frequency registers - self._rfm_msb = None - self._rfm_mid = None - self._rfm_lsb = None + self._rfm_msb: Optional[registeraddress] = None + self._rfm_mid: Optional[registeraddress] = None + self._rfm_lsb: Optional[registeraddress] = None # Set datarate registers - self._sf = None - self._bw = None - self._modemcfg = None + self._sf: Optional[registeraddress] = None + self._bw: Optional[registeraddress] = None + self._modemcfg: Optional[registeraddress] = None self.set_datarate("SF7BW125") # Set regional frequency plan # pylint: disable=import-outside-toplevel @@ -201,13 +236,18 @@ def __init__(self, spi, cs, irq, rst, ttn_config, channel=None): # Give the lora object ttn configuration self._ttn_config = ttn_config - def __enter__(self): + def __enter__(self) -> Self: return self - def __exit__(self, exception_type, exception_value, traceback): + def __exit__( + self, + exception_type: Optional[Type[type]], + exception_value: Optional[BaseException], + traceback: Optional[TracebackType], + ) -> None: self.deinit() - def deinit(self): + def deinit(self) -> None: """Deinitializes the TinyLoRa object properties and pins.""" self._irq = None self._rst = None @@ -220,7 +260,9 @@ def deinit(self): self._bw = None self._modemcfg = None - def send_data(self, data, data_length, frame_counter, timeout=2): + def send_data( + self, data: bytearray, data_length: int, frame_counter: int, timeout: int = 2 + ) -> None: """Function to assemble and send data :param data: data to send :param data_length: length of data to send @@ -258,7 +300,7 @@ def send_data(self, data, data_length, frame_counter, timeout=2): # recalculate packet length lora_pkt_len += data_length # Calculate MIC - mic = bytearray(4) + mic: bytearray4 = bytearray(4) mic = aes.calculate_mic(lora_pkt, lora_pkt_len, mic) # load mic in package lora_pkt[lora_pkt_len : lora_pkt_len + 4] = mic[0:4] @@ -266,7 +308,9 @@ def send_data(self, data, data_length, frame_counter, timeout=2): lora_pkt_len += 4 self.send_packet(lora_pkt, lora_pkt_len, timeout) - def send_packet(self, lora_packet, packet_length, timeout): + def send_packet( + self, lora_packet: bytearray, packet_length: int, timeout: int + ) -> None: """Sends a LoRa packet using the RFM Module :param bytearray lora_packet: assembled LoRa packet from send_data :param int packet_length: length of LoRa packet to send @@ -312,10 +356,11 @@ def send_packet(self, lora_packet, packet_length, timeout): if timed_out: raise RuntimeError("Timeout during packet send") - def set_datarate(self, datarate): + def set_datarate(self, datarate: str) -> None: """Sets the RFM Datarate :param datarate: Bandwidth and Frequency Plan """ + # TODO: Convert these to enum data_rates = { "SF7BW125": (0x74, 0x72, 0x04), "SF7BW250": (0x74, 0x82, 0x04), @@ -330,13 +375,15 @@ def set_datarate(self, datarate): except KeyError as err: raise KeyError("Invalid or Unsupported Datarate.") from err - def set_channel(self, channel): + def set_channel(self, channel: int) -> None: """Sets the RFM Channel (if single-channel) :param int channel: Transmit Channel (0 through 7). """ self._rfm_msb, self._rfm_mid, self._rfm_lsb = self._frequencies[channel] - def _read_into(self, address, buf, length=None): + def _read_into( + self, address: registeraddress, buf: bytearray2, length: Optional[int] = None + ) -> None: """Read a number of bytes from the specified address into the provided buffer. If length is not specified (default) the entire buffer will be filled. @@ -353,14 +400,14 @@ def _read_into(self, address, buf, length=None): device.write(self._BUFFER, end=1) device.readinto(buf, end=length) - def _read_u8(self, address): + def _read_u8(self, address: registeraddress) -> int: """Read a single byte from the provided address and return it. :param bytearray address: Register Address. """ self._read_into(address, self._BUFFER, length=1) return self._BUFFER[0] - def _write_u8(self, address, val): + def _write_u8(self, address: registeraddress, val: int) -> None: """Writes to the RFM register given an address and data. :param bytearray address: Register Address. :param val: Data to write. diff --git a/adafruit_tinylora/adafruit_tinylora_encryption.py b/adafruit_tinylora/adafruit_tinylora_encryption.py index 4713232..6198550 100755 --- a/adafruit_tinylora/adafruit_tinylora_encryption.py +++ b/adafruit_tinylora/adafruit_tinylora_encryption.py @@ -11,15 +11,28 @@ * Author(s): adafruit """ +try: # typing + from typing import Annotated, List, Tuple, TypeAlias + + # pylint: disable=invalid-name + bytearray2: TypeAlias = Annotated[bytearray, 2] + bytearray4: TypeAlias = Annotated[bytearray, 4] + bytearray16: TypeAlias = Annotated[bytearray, 16] + + StateMatrix: TypeAlias = List[bytearray4] +except ImportError: + pass + # from http://cs.ucsb.edu/~koc/cs178/projects/JT/aes.c -def xtime(col): +def xtime(col: int) -> int: """xtime impl. for _mix_single_column()""" return (((col << 1) ^ 0x1B) & 0xFF) if (col & 0x80) else (col << 1) # AES S-box -S_BOX = ( +# NOTE(typing): Each of these 16 items is 16b, can use bytearray16 here +S_BOX: Tuple[bytes, ...] = ( b"c|w{\xf2ko\xc50\x01g+\xfe\xd7\xabv", b"\xca\x82\xc9}\xfaYG\xf0\xad\xd4\xa2\xaf\x9c\xa4r\xc0", b"\xb7\xfd\x93&6?\xf7\xcc4\xa5\xe5\xf1q\xd81\x15", @@ -45,20 +58,26 @@ class AES: (https://github.com/bozhu/AES-Python) and TinyLoRa () """ - def __init__(self, device_address, app_key, network_key, frame_counter): + def __init__( + self, + device_address: bytearray4, + app_key: bytearray16, + network_key: bytearray16, + frame_counter: int, + ): self._app_key = app_key self._device_address = device_address self._network_key = network_key self.frame_counter = frame_counter - def encrypt(self, aes_data): + def encrypt(self, aes_data: bytearray) -> bytearray: """Performs AES Encryption routine with data. :param bytearray data: Data to-be encrypted. """ self.encrypt_payload(aes_data) return aes_data - def encrypt_payload(self, data): + def encrypt_payload(self, data: bytearray) -> None: """Encrypts data payload. :param bytearray data: Data to-be-encrypted. """ @@ -105,18 +124,18 @@ def encrypt_payload(self, data): data_pointer += 1 block_counter += 1 - def _aes_encrypt(self, data, key): + def _aes_encrypt(self, data: bytearray, key: bytearray) -> None: """Performs 9 rounds of AES encryption on data per TinyLoRa spec. NOTE: This is not an accurate aes_encrypt impl., tinylora performs an additional key calculation after 9 rounds. :param bytearray data: Data array. :param bytearray key: Round Key Array. """ - state = [ - ["0", "0", "0", "0"], - ["0", "0", "0", "0"], - ["0", "0", "0", "0"], - ["0", "0", "0", "0"], + state: StateMatrix = [ + bytearray(b"0000"), + bytearray(b"0000"), + bytearray(b"0000"), + bytearray(b"0000"), ] # Copy Data to State Array for manipulation for col in range(4): @@ -136,7 +155,9 @@ def _aes_encrypt(self, data, key): for col in range(4): data[col + (row << 2)] = state[row][col] - def _round_encrypt(self, state, key, num_round): + def _round_encrypt( + self, state: StateMatrix, key: bytearray, num_round: int + ) -> None: """Performs one round of AES. :param bytearray state: State array. :param bytearray key: Round key array. @@ -148,7 +169,7 @@ def _round_encrypt(self, state, key, num_round): self._aes_calculate_key(num_round, key) self._aes_add_round_key(key, state) - def _aes_calculate_key(self, num_round, round_key): + def _aes_calculate_key(self, num_round: int, round_key: bytearray) -> None: """Performs round key calculation per TinyLoRa's spec. :param int num_round: Round number :param bytearray round_key: Round key array. @@ -177,7 +198,7 @@ def _aes_calculate_key(self, num_round, round_key): tmp_arr[col] = round_key[col + (row << 2)] @staticmethod - def _aes_add_round_key(round_key, state): + def _aes_add_round_key(round_key: bytearray, state: StateMatrix) -> None: """AES AddRoundKey Step: Round_Key combined with the state. :param bytearray round_key: Subkey for each round. :param bytearray state: State array. @@ -187,7 +208,7 @@ def _aes_add_round_key(round_key, state): state[col][row] ^= round_key[row + (col << 2)] @staticmethod - def _aes_sub_byte(sub_byte): + def _aes_sub_byte(sub_byte: int) -> int: """Sub-Byte Step: Used for returning specific byte from the AES S_BOX. :param byte sub_byte: byte to be replaced with S_BOX byte. @@ -196,7 +217,7 @@ def _aes_sub_byte(sub_byte): col = sub_byte & 0x0F return S_BOX[row][col] - def _aes_sub_bytes(self, state): + def _aes_sub_bytes(self, state: StateMatrix) -> None: """AES SubBytes Step: Replace state arr. bytes w/sub-byte from S_BOX :param bytearray s: State array. """ @@ -205,7 +226,7 @@ def _aes_sub_bytes(self, state): state[row][col] = self._aes_sub_byte(state[row][col]) @staticmethod - def _mix_single_column(col): + def _mix_single_column(col: bytearray4) -> None: """Mixes individual columns with state array :param bytearray col: Column from statearray """ @@ -216,7 +237,7 @@ def _mix_single_column(col): col[2] ^= temp ^ xtime(col[2] ^ col[3]) col[3] ^= temp ^ xtime(col[3] ^ col_zero) - def _aes_mix_columns(self, state): + def _aes_mix_columns(self, state: StateMatrix) -> None: """AES MixColumns Step: Multiplies each column of the state array with xtime. :param bytearray state: State array. """ @@ -224,7 +245,7 @@ def _aes_mix_columns(self, state): self._mix_single_column(state[column_index]) @staticmethod - def _aes_shift_rows(arr): + def _aes_shift_rows(arr: StateMatrix) -> None: """AES ShiftRows Step: State array's bytes shifted to the left. :param bytearray state: State array. """ @@ -247,7 +268,9 @@ def _aes_shift_rows(arr): arr[2][3], ) - def calculate_mic(self, lora_packet, lora_packet_length, mic): + def calculate_mic( + self, lora_packet: bytearray, lora_packet_length: int, mic: bytearray4 + ) -> bytearray4: """Calculates the validity of data messages, generates message integrity check bytearray.""" block_b = bytearray(16) key_k1 = bytearray(16) @@ -325,7 +348,7 @@ def calculate_mic(self, lora_packet, lora_packet_length, mic): # return message integrity check array to calling method return mic - def _mic_generate_keys(self, key_1, key_2): + def _mic_generate_keys(self, key_1: bytearray16, key_2: bytearray16) -> None: # encrypt the 0's in k1 with network key self._aes_encrypt(key_1, self._network_key) # perform gen_key on key_1 @@ -346,7 +369,7 @@ def _mic_generate_keys(self, key_1, key_2): key_2[15] ^= 0x87 @staticmethod - def _shift_left(data): + def _shift_left(data: bytearray16) -> None: """Shifts data bytearray left by 1""" for byte_index in range(16): if byte_index < 15: @@ -360,7 +383,7 @@ def _shift_left(data): data[byte_index] = ((data[byte_index] << 1) + overflow) & 0xFF @staticmethod - def _xor_data(new_data, old_data): + def _xor_data(new_data: bytearray16, old_data: bytearray16) -> None: """XOR two data arrays :param bytearray new_data: Calculated data. :param bytearray old_data: data to be xor'd. diff --git a/examples/tinylora_simpletest.py b/examples/tinylora_simpletest.py index 365e31e..1f8804d 100644 --- a/examples/tinylora_simpletest.py +++ b/examples/tinylora_simpletest.py @@ -2,11 +2,21 @@ # SPDX-License-Identifier: MIT import time + +import board import busio import digitalio -import board + from adafruit_tinylora.adafruit_tinylora import TTN, TinyLoRa +try: # typing + from typing import Annotated, TypeAlias + + bytearray4: TypeAlias = Annotated[bytearray, 4] + bytearray16: TypeAlias = Annotated[bytearray, 16] +except ImportError: + pass + # Board LED led = digitalio.DigitalInOut(board.D13) led.direction = digitalio.Direction.OUTPUT @@ -24,10 +34,10 @@ # rst = digitalio.DigitalInOut(board.RFM9X_RST) # TTN Device Address, 4 Bytes, MSB -devaddr = bytearray([0x00, 0x00, 0x00, 0x00]) +devaddr: bytearray4 = bytearray([0x00, 0x00, 0x00, 0x00]) # TTN Network Key, 16 Bytes, MSB -nwkey = bytearray( +nwkey: bytearray16 = bytearray( [ 0x00, 0x00, @@ -49,7 +59,7 @@ ) # TTN Application Key, 16 Bytess, MSB -app = bytearray( +app: bytearray16 = bytearray( [ 0x00, 0x00, @@ -75,7 +85,7 @@ lora = TinyLoRa(spi, cs, irq, rst, ttn_config) while True: - data = bytearray(b"\x43\x57\x54\x46") + data: bytearray4 = bytearray(b"\x43\x57\x54\x46") print("Sending packet...") lora.send_data(data, len(data), lora.frame_counter) print("Packet sent!") diff --git a/examples/tinylora_simpletest_si7021.py b/examples/tinylora_simpletest_si7021.py index 08c73fb..27aae35 100644 --- a/examples/tinylora_simpletest_si7021.py +++ b/examples/tinylora_simpletest_si7021.py @@ -4,12 +4,22 @@ """Using TinyLoRa with a Si7021 Sensor. """ import time + +import adafruit_si7021 +import board import busio import digitalio -import board -import adafruit_si7021 + from adafruit_tinylora.adafruit_tinylora import TTN, TinyLoRa +try: # typing + from typing import Annotated, TypeAlias + + bytearray4: TypeAlias = Annotated[bytearray, 4] + bytearray16: TypeAlias = Annotated[bytearray, 16] +except ImportError: + pass + # Board LED led = digitalio.DigitalInOut(board.D13) led.direction = digitalio.Direction.OUTPUT @@ -32,10 +42,10 @@ # rst = digitalio.DigitalInOut(board.RFM9X_RST) # TTN Device Address, 4 Bytes, MSB -devaddr = bytearray([0x00, 0x00, 0x00, 0x00]) +devaddr: bytearray4 = bytearray([0x00, 0x00, 0x00, 0x00]) # TTN Network Key, 16 Bytes, MSB -nwkey = bytearray( +nwkey: bytearray16 = bytearray( [ 0x00, 0x00, @@ -57,7 +67,7 @@ ) # TTN Application Key, 16 Bytess, MSB -app = bytearray( +app: bytearray16 = bytearray( [ 0x00, 0x00, @@ -83,7 +93,7 @@ lora = TinyLoRa(spi, cs, irq, rst, ttn_config) # Data Packet to send to TTN -data = bytearray(4) +data: bytearray4 = bytearray(4) while True: temp_val = sensor.temperature diff --git a/examples/tinylora_simpletest_single_channel.py b/examples/tinylora_simpletest_single_channel.py index c3a3e7b..f55d48f 100644 --- a/examples/tinylora_simpletest_single_channel.py +++ b/examples/tinylora_simpletest_single_channel.py @@ -2,11 +2,18 @@ # SPDX-License-Identifier: MIT import time + +import board import busio import digitalio -import board + from adafruit_tinylora.adafruit_tinylora import TTN, TinyLoRa +try: + from adafruit_tinylora.adafruit_tinylora import bytearray4, bytearray16 +except ImportError: + pass + # Board LED led = digitalio.DigitalInOut(board.D13) led.direction = digitalio.Direction.OUTPUT @@ -24,10 +31,10 @@ # rst = digitalio.DigitalInOut(board.RFM9X_RST) # TTN Device Address, 4 Bytes, MSB -devaddr = bytearray([0x00, 0x00, 0x00, 0x00]) +devaddr: bytearray4 = bytearray([0x00, 0x00, 0x00, 0x00]) # TTN Network Key, 16 Bytes, MSB -nwkey = bytearray( +nwkey: bytearray16 = bytearray( [ 0x00, 0x00, @@ -49,7 +56,7 @@ ) # TTN Application Key, 16 Bytess, MSB -app = bytearray( +app: bytearray16 = bytearray( [ 0x00, 0x00, diff --git a/requirements.txt b/requirements.txt index a45c547..1d49003 100644 --- a/requirements.txt +++ b/requirements.txt @@ -4,3 +4,4 @@ Adafruit-Blinka adafruit-circuitpython-busdevice +typing_extensions