Skip to content

lora/lora-lorawan: Add LoRaWAN 1.0.x MAC layer#1111

Open
ddtdanilo wants to merge 1 commit into
micropython:masterfrom
ddtdanilo:add-pycom-lorawan
Open

lora/lora-lorawan: Add LoRaWAN 1.0.x MAC layer#1111
ddtdanilo wants to merge 1 commit into
micropython:masterfrom
ddtdanilo:add-pycom-lorawan

Conversation

@ddtdanilo
Copy link
Copy Markdown

Summary

Add lora-lorawan: a LoRaWAN 1.0.x MAC layer for MicroPython.

  • OTAA and ABP activation modes
  • Class A device operation (TX → RX1 → RX2)
  • AES-128-CMAC (RFC 4493) and CTR-mode crypto built on ucryptolib
  • MIC calculation, payload encryption, session-key derivation

The MAC layer is radio-agnostic: it consumes a radio object that exposes
the lora send/recv API. Tested with lora-sx127x-pycom on Pycom LoPy/LoPy4 boards.

Split out of #1102 per @projectgus's request to
make the driver and the MAC layer reviewable independently.

Testing

  • All files pass ruff check and ruff format
  • LoRaWAN crypto verified against RFC 4493 test vectors
  • End-to-end OTAA join + uplink against The Things Network on a LoPy4 (Pycom SX1276)

Related

Add LoRaWAN Class A device operation with OTAA and ABP activation.
Includes AES-128-CMAC/CTR crypto primitives built on ucryptolib for
MIC calculation, payload encryption, and session key derivation per
the LoRaWAN 1.0.x specification.

Used by Pycom LoPy/LoPy4 boards as defined in
micropython/micropython#19026.

Signed-off-by: Danilo D <danilodt@gmail.com>
@ddtdanilo
Copy link
Copy Markdown
Author

Hi @projectgus — gentle ping on the LoRaWAN side of the split. #1111 holds the lora-lorawan MAC layer on its own branch, CI green, no dependency edge with #1102 so they can be reviewed independently and in any order. Let me know if you'd like the scope or structure tweaked. Thanks!

Copy link
Copy Markdown
Contributor

@projectgus projectgus left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is neat! A lot less code than I expected for a functional LoRaWAN Class A node as well, nice work.

I gave this a quick skim and put a few notes below. My main concern at this point is the same one raised in #1102 - we already have a LoRa modem driver API and we can't merge a second incompatible API.

If we can resolve that in #1102, and then this PR is made to work with that, then I think we'd be on a good path to getting this merged as well.

return True

# RX2 window: JoinAcceptDelay2 at RX2 frequency/DR
sf, bw = _DR_TABLE_EU868[_RX2_DR]
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Need a way to make data rates configurable rather than hardcoded to one region (for example, I'm in Australia so I legally cannot even test EU868 settings!)

from lora import LoRa
from lorawan import LoRaWAN

radio = LoRa(frequency=868100000, sf=7, bw=125000)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This will need to work with the existing LoRa modem drivers as well. (See related discussion at #1102 (comment))

Specifically here: The existing drivers pass the hardware-specific config as constructor kwargs, but the LoRa radio settings are passed in a lora_cfg dictionary which is non-hardware-specific so it's easy to factor code to work with different modem drivers.

if L[0] & 0x80:
K1 = _xor(K1, b"\x00" * 15 + b"\x87")
K2 = _shift_left_1(K1)
if K1[0] & 0x80:
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I have some mild concern about whether any of these kind of branches create timing side channels.

Naive way to make this "constant time" would be perhaps

K2 = _xor(K2, b"\x00" * 15 + (b"\x87" if (K1[0] & 0x80) else b"\x00"))

Whether this is actually better for timing would need to be measured.

Alternatively, mbedTLS has AES-CMAC support so we could expose the primitives from there via cryptolib. Pros: faster, not rolling own crypto. Cons: binary size would probably be higher, and on all boards... :/

now = time.ticks_ms()
wait = time.ticks_diff(tx_end + delay_ms, now)
if wait > 0:
time.sleep_ms(wait)
Copy link
Copy Markdown
Contributor

@projectgus projectgus May 27, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not asking for it in this PR, it would be interesting to see if this driver could be made async so an async task is blocking for the correct window to open, while the code is doing something else. (As async is non-preemptive this might turn out to not be tight enough on timing, though.)

if (mhdr & 0xE0) != _MTYPE_JOIN_ACCEPT:
return False

from ucryptolib import aes
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
from ucryptolib import aes
from cryptolib import aes

(We deprecated the u-prefix a couple of versions ago.)

Also please move all imports to the top, unless there's a runtime reason why this code path should be optional.

"""
LoRaWAN cryptographic primitives (AES-128-CMAC, CTR, key derivation).

Uses ucryptolib.aes (AES-128-ECB) as the only hardware dependency.
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
Uses ucryptolib.aes (AES-128-ECB) as the only hardware dependency.
Uses cryptolib.aes (AES-128-ECB) as the only hardware dependency.

(and same below)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants