pamoja.lorawan

Idiomatic LoRaWAN facade.

A long-range public-band link is wide open, so LoRaWAN wraps every frame in two guarantees: a message integrity code keyed to the network proves the frame is authentic and intact, and the payload is encrypted to the application so only its owner can read it. This builds and verifies exactly that.

  1"""Idiomatic LoRaWAN facade.
  2
  3A long-range public-band link is wide open, so LoRaWAN wraps every frame in two
  4guarantees: a message integrity code keyed to the network proves the frame is
  5authentic and intact, and the payload is encrypted to the application so only its
  6owner can read it. This builds and verifies exactly that.
  7"""
  8
  9from __future__ import annotations
 10
 11import enum
 12
 13from pamoja._native import (
 14    LorawanDevice,
 15    LorawanGrant,
 16    LorawanHeader,
 17    LorawanJoinAccept,
 18    LorawanJoinRequest,
 19    LorawanRxData,
 20    LorawanSession,
 21)
 22from pamoja._native import lorawan_parse_header as _parse_header
 23from pamoja._native import lorawan_parse_join_request as _parse_join_request
 24
 25__all__ = [
 26    "Device",
 27    "Direction",
 28    "Grant",
 29    "Header",
 30    "JoinAccept",
 31    "JoinRequest",
 32    "MessageType",
 33    "RxData",
 34    "Session",
 35    "device",
 36    "grant",
 37    "parse_header",
 38    "parse_join_request",
 39    "session",
 40]
 41
 42#: An activated session: a device address and its two session keys.
 43Session = LorawanSession
 44#: The root credentials over-the-air activation is built on.
 45Device = LorawanDevice
 46#: An accepted join: the network settings, and the session it grants.
 47JoinAccept = LorawanJoinAccept
 48#: A decoded data frame, with its payload decrypted.
 49RxData = LorawanRxData
 50#: What a frame says about itself before any key is involved.
 51Header = LorawanHeader
 52#: A join-request a device broadcast, with its integrity already verified.
 53JoinRequest = LorawanJoinRequest
 54#: What a network grants a device that joined.
 55Grant = LorawanGrant
 56
 57
 58class MessageType(str, enum.Enum):
 59    """What kind of message a frame is, read from its header."""
 60
 61    #: A device asking to join a network.
 62    JOIN_REQUEST = "JoinRequest"
 63    #: A network admitting a device.
 64    JOIN_ACCEPT = "JoinAccept"
 65    #: Data from a device that does not need acknowledging.
 66    UNCONFIRMED_UP = "UnconfirmedUp"
 67    #: Data from a device that asks to be acknowledged.
 68    CONFIRMED_UP = "ConfirmedUp"
 69    #: Data to a device that does not need acknowledging.
 70    UNCONFIRMED_DOWN = "UnconfirmedDown"
 71    #: Data to a device that asks to be acknowledged.
 72    CONFIRMED_DOWN = "ConfirmedDown"
 73
 74
 75class Direction(str, enum.Enum):
 76    """The direction a frame travelled, which its MIC and encryption fold in."""
 77
 78    #: From an end device up to the network.
 79    UPLINK = "Uplink"
 80    #: From the network down to an end device.
 81    DOWNLINK = "Downlink"
 82
 83
 84def session(dev_addr: int, nwk_skey: bytes, app_skey: bytes) -> LorawanSession:
 85    """Create a session for a device already activated by personalization.
 86
 87    :param dev_addr: The device address the network assigned.
 88    :param nwk_skey: The 16-byte network session key, which authenticates frames.
 89    :param app_skey: The 16-byte application session key, which encrypts payloads.
 90    :returns: The session, ready to encode and decode data frames.
 91    :raises PamojaError: If either key is not 16 bytes.
 92    """
 93    return LorawanSession(dev_addr, bytes(nwk_skey), bytes(app_skey))
 94
 95
 96def device(dev_eui: bytes, app_eui: bytes, app_key: bytes) -> LorawanDevice:
 97    """Create a device holding the root credentials for over-the-air activation.
 98
 99    :param dev_eui: The 8-byte device EUI.
100    :param app_eui: The 8-byte application (join) EUI.
101    :param app_key: The 16-byte application key the join exchange is secured with.
102    :returns: The device, ready to build a join request.
103    :raises PamojaError: If any credential is the wrong length.
104    """
105    return LorawanDevice(bytes(dev_eui), bytes(app_eui), bytes(app_key))
106
107
108def parse_header(data: bytes) -> Header:
109    """Read a frame far enough to route it, without any key.
110
111    A receiver holding many sessions uses this to find which one a frame belongs
112    to: the device address travels in the clear, so it can be read before the
113    session that would verify the frame is even known.
114
115    Nothing this reports is authenticated. Treat it as a routing hint until
116    :meth:`Session.decode` has verified the frame.
117
118    :param data: The raw frame as it came off the radio.
119    :returns: What the header says the frame is.
120    :raises PamojaError: If the frame is truncated or carries a message type this
121        build does not read.
122    """
123    return _parse_header(bytes(data))
124
125
126def parse_join_request(data: bytes, app_key: bytes) -> JoinRequest:
127    """Verify a join-request and read the identifiers out of it.
128
129    This is the network side of activation: it proves the request came from a
130    holder of the application key before reporting who sent it.
131
132    :param data: The raw join-request as it came off the radio.
133    :param app_key: The 16-byte application root key the device shares.
134    :returns: The verified request.
135    :raises PamojaError: If the MIC does not verify or the frame is not a
136        join-request.
137    """
138    return _parse_join_request(bytes(data), bytes(app_key))
139
140
141def grant(
142    app_nonce: int,
143    net_id: int,
144    dev_addr: int,
145    dl_settings: int = 0,
146    rx_delay: int = 0,
147    cflist: bytes | None = None,
148) -> Grant:
149    """Describe what this network grants a device that joined.
150
151    :param app_nonce: A nonce this network must not reuse for the device, since
152        the session keys are derived from it; low 24 bits only.
153    :param net_id: The network identifier; low 24 bits only.
154    :param dev_addr: The address to assign the device.
155    :param dl_settings: The downlink settings byte.
156    :param rx_delay: The delay before the first receive window, in seconds.
157    :param cflist: The optional 16-byte channel list.
158    :returns: The grant, which signs its own join-accept and derives the session.
159    :raises PamojaError: If the channel list is not 16 bytes.
160    """
161    return Grant(
162        app_nonce,
163        net_id,
164        dev_addr,
165        dl_settings,
166        rx_delay,
167        None if cflist is None else bytes(cflist),
168    )
Device = <class 'builtins.LorawanDevice'>
class Direction(builtins.str, enum.Enum):
76class Direction(str, enum.Enum):
77    """The direction a frame travelled, which its MIC and encryption fold in."""
78
79    #: From an end device up to the network.
80    UPLINK = "Uplink"
81    #: From the network down to an end device.
82    DOWNLINK = "Downlink"

The direction a frame travelled, which its MIC and encryption fold in.

Grant = <class 'builtins.LorawanGrant'>
JoinAccept = <class 'builtins.LorawanJoinAccept'>
JoinRequest = <class 'builtins.LorawanJoinRequest'>
class MessageType(builtins.str, enum.Enum):
59class MessageType(str, enum.Enum):
60    """What kind of message a frame is, read from its header."""
61
62    #: A device asking to join a network.
63    JOIN_REQUEST = "JoinRequest"
64    #: A network admitting a device.
65    JOIN_ACCEPT = "JoinAccept"
66    #: Data from a device that does not need acknowledging.
67    UNCONFIRMED_UP = "UnconfirmedUp"
68    #: Data from a device that asks to be acknowledged.
69    CONFIRMED_UP = "ConfirmedUp"
70    #: Data to a device that does not need acknowledging.
71    UNCONFIRMED_DOWN = "UnconfirmedDown"
72    #: Data to a device that asks to be acknowledged.
73    CONFIRMED_DOWN = "ConfirmedDown"

What kind of message a frame is, read from its header.

JOIN_REQUEST = <MessageType.JOIN_REQUEST: 'JoinRequest'>
JOIN_ACCEPT = <MessageType.JOIN_ACCEPT: 'JoinAccept'>
UNCONFIRMED_UP = <MessageType.UNCONFIRMED_UP: 'UnconfirmedUp'>
CONFIRMED_UP = <MessageType.CONFIRMED_UP: 'ConfirmedUp'>
UNCONFIRMED_DOWN = <MessageType.UNCONFIRMED_DOWN: 'UnconfirmedDown'>
CONFIRMED_DOWN = <MessageType.CONFIRMED_DOWN: 'ConfirmedDown'>
RxData = <class 'builtins.LorawanRxData'>
Session = <class 'builtins.LorawanSession'>
def device(dev_eui: bytes, app_eui: bytes, app_key: bytes) -> LorawanDevice:
 97def device(dev_eui: bytes, app_eui: bytes, app_key: bytes) -> LorawanDevice:
 98    """Create a device holding the root credentials for over-the-air activation.
 99
100    :param dev_eui: The 8-byte device EUI.
101    :param app_eui: The 8-byte application (join) EUI.
102    :param app_key: The 16-byte application key the join exchange is secured with.
103    :returns: The device, ready to build a join request.
104    :raises PamojaError: If any credential is the wrong length.
105    """
106    return LorawanDevice(bytes(dev_eui), bytes(app_eui), bytes(app_key))

Create a device holding the root credentials for over-the-air activation.

Parameters
  • dev_eui: The 8-byte device EUI.
  • app_eui: The 8-byte application (join) EUI.
  • app_key: The 16-byte application key the join exchange is secured with. :returns: The device, ready to build a join request.
Raises
  • PamojaError: If any credential is the wrong length.
def grant( app_nonce: int, net_id: int, dev_addr: int, dl_settings: int = 0, rx_delay: int = 0, cflist: bytes | None = None) -> LorawanGrant:
142def grant(
143    app_nonce: int,
144    net_id: int,
145    dev_addr: int,
146    dl_settings: int = 0,
147    rx_delay: int = 0,
148    cflist: bytes | None = None,
149) -> Grant:
150    """Describe what this network grants a device that joined.
151
152    :param app_nonce: A nonce this network must not reuse for the device, since
153        the session keys are derived from it; low 24 bits only.
154    :param net_id: The network identifier; low 24 bits only.
155    :param dev_addr: The address to assign the device.
156    :param dl_settings: The downlink settings byte.
157    :param rx_delay: The delay before the first receive window, in seconds.
158    :param cflist: The optional 16-byte channel list.
159    :returns: The grant, which signs its own join-accept and derives the session.
160    :raises PamojaError: If the channel list is not 16 bytes.
161    """
162    return Grant(
163        app_nonce,
164        net_id,
165        dev_addr,
166        dl_settings,
167        rx_delay,
168        None if cflist is None else bytes(cflist),
169    )

Describe what this network grants a device that joined.

Parameters
  • app_nonce: A nonce this network must not reuse for the device, since the session keys are derived from it; low 24 bits only.
  • net_id: The network identifier; low 24 bits only.
  • dev_addr: The address to assign the device.
  • dl_settings: The downlink settings byte.
  • rx_delay: The delay before the first receive window, in seconds.
  • cflist: The optional 16-byte channel list. :returns: The grant, which signs its own join-accept and derives the session.
Raises
  • PamojaError: If the channel list is not 16 bytes.
def parse_header(data: bytes) -> LorawanHeader:
109def parse_header(data: bytes) -> Header:
110    """Read a frame far enough to route it, without any key.
111
112    A receiver holding many sessions uses this to find which one a frame belongs
113    to: the device address travels in the clear, so it can be read before the
114    session that would verify the frame is even known.
115
116    Nothing this reports is authenticated. Treat it as a routing hint until
117    :meth:`Session.decode` has verified the frame.
118
119    :param data: The raw frame as it came off the radio.
120    :returns: What the header says the frame is.
121    :raises PamojaError: If the frame is truncated or carries a message type this
122        build does not read.
123    """
124    return _parse_header(bytes(data))

Read a frame far enough to route it, without any key.

A receiver holding many sessions uses this to find which one a frame belongs to: the device address travels in the clear, so it can be read before the session that would verify the frame is even known.

Nothing this reports is authenticated. Treat it as a routing hint until Session.decode() has verified the frame.

Parameters
  • data: The raw frame as it came off the radio. :returns: What the header says the frame is.
Raises
  • PamojaError: If the frame is truncated or carries a message type this build does not read.
def parse_join_request(data: bytes, app_key: bytes) -> LorawanJoinRequest:
127def parse_join_request(data: bytes, app_key: bytes) -> JoinRequest:
128    """Verify a join-request and read the identifiers out of it.
129
130    This is the network side of activation: it proves the request came from a
131    holder of the application key before reporting who sent it.
132
133    :param data: The raw join-request as it came off the radio.
134    :param app_key: The 16-byte application root key the device shares.
135    :returns: The verified request.
136    :raises PamojaError: If the MIC does not verify or the frame is not a
137        join-request.
138    """
139    return _parse_join_request(bytes(data), bytes(app_key))

Verify a join-request and read the identifiers out of it.

This is the network side of activation: it proves the request came from a holder of the application key before reporting who sent it.

Parameters
  • data: The raw join-request as it came off the radio.
  • app_key: The 16-byte application root key the device shares. :returns: The verified request.
Raises
  • PamojaError: If the MIC does not verify or the frame is not a join-request.
def session(dev_addr: int, nwk_skey: bytes, app_skey: bytes) -> LorawanSession:
85def session(dev_addr: int, nwk_skey: bytes, app_skey: bytes) -> LorawanSession:
86    """Create a session for a device already activated by personalization.
87
88    :param dev_addr: The device address the network assigned.
89    :param nwk_skey: The 16-byte network session key, which authenticates frames.
90    :param app_skey: The 16-byte application session key, which encrypts payloads.
91    :returns: The session, ready to encode and decode data frames.
92    :raises PamojaError: If either key is not 16 bytes.
93    """
94    return LorawanSession(dev_addr, bytes(nwk_skey), bytes(app_skey))

Create a session for a device already activated by personalization.

Parameters
  • dev_addr: The device address the network assigned.
  • nwk_skey: The 16-byte network session key, which authenticates frames.
  • app_skey: The 16-byte application session key, which encrypts payloads. :returns: The session, ready to encode and decode data frames.
Raises
  • PamojaError: If either key is not 16 bytes.