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 LORAWAN_ADR_ACK_DELAY, 15 LORAWAN_ADR_ACK_LIMIT, 16 LORAWAN_JOIN_ACCEPT_DELAY1_US, 17 LORAWAN_JOIN_ACCEPT_DELAY2_US, 18 LORAWAN_MAX_FCNT_GAP, 19 LORAWAN_RECEIVE_DELAY1_US, 20 LORAWAN_RECEIVE_DELAY2_US, 21 LORAWAN_RECEIVE_WINDOW_TOLERANCE_US, 22 LORAWAN_RETRANSMIT_TIMEOUT_MAX_US, 23 LORAWAN_RETRANSMIT_TIMEOUT_MIN_US, 24 LORAWAN_SAVED_LEN, 25 LorawanBackoff, 26 LorawanBackoffStep, 27 LorawanCfList, 28 LorawanChannel, 29 LorawanDelivery, 30 LorawanDevice, 31 LorawanDeviceError, 32 LorawanDeviceSettings, 33 LorawanEndDevice, 34 LorawanHeard, 35 LorawanNext, 36 LorawanTransmission, 37 LorawanWindow, 38 LorawanGrant, 39 LorawanHeader, 40 LorawanJoinAccept, 41 LorawanJoinRequest, 42 LorawanMacCommand, 43 LorawanPackageCommand, 44 LorawanRxData, 45 LorawanSession, 46 LorawanAckWindow, 47 LorawanAcknowledgment, 48 LorawanListen, 49 LorawanRelay, 50 LorawanRelayError, 51 LorawanRelayExchange, 52 LorawanRelayHeard, 53 LorawanRelayStatus, 54 LorawanRxrDownlink, 55 LorawanScan, 56 LorawanWake, 57 LorawanWakeUp, 58 LorawanWorNext, 59) 60from pamoja._native import lorawan_mac_parse as _mac_parse 61from pamoja._native import lorawan_package_encode as _package_encode 62from pamoja._native import lorawan_package_parse as _package_parse 63from pamoja._native import lorawan_package_parse_all as _package_parse_all 64from pamoja._native import lorawan_package_status_item as _package_status_item 65from pamoja._native import lorawan_parse_header as _parse_header 66from pamoja._native import lorawan_parse_join_request as _parse_join_request 67 68from pamoja.lorawan import clock, firmware, fragment, multicast, relay 69 70__all__ = [ 71 "ADR_ACK_DELAY", 72 "ADR_ACK_LIMIT", 73 "AckWindow", 74 "Acknowledgment", 75 "Backoff", 76 "BackoffStep", 77 "CfList", 78 "Channel", 79 "Delivery", 80 "Device", 81 "DeviceError", 82 "DeviceSettings", 83 "Direction", 84 "EndDevice", 85 "Grant", 86 "Header", 87 "Heard", 88 "JOIN_ACCEPT_DELAY1_US", 89 "JOIN_ACCEPT_DELAY2_US", 90 "JoinAccept", 91 "JoinRequest", 92 "Listen", 93 "MAX_FCNT_GAP", 94 "MacCommand", 95 "MessageType", 96 "Next", 97 "PackageCommand", 98 "RECEIVE_DELAY1_US", 99 "RECEIVE_DELAY2_US", 100 "RECEIVE_WINDOW_TOLERANCE_US", 101 "RETRANSMIT_TIMEOUT_MAX_US", 102 "RETRANSMIT_TIMEOUT_MIN_US", 103 "ReceiveWindow", 104 "Relay", 105 "RelayActivation", 106 "RelayError", 107 "RelayExchange", 108 "RelayHeard", 109 "RelayStatus", 110 "RelaySync", 111 "RxData", 112 "RxrDownlink", 113 "SAVED_LEN", 114 "Scan", 115 "Session", 116 "Transmission", 117 "Version", 118 "Wake", 119 "WakeUp", 120 "Window", 121 "WorNext", 122 "clock", 123 "device", 124 "end_device", 125 "firmware", 126 "fragment", 127 "grant", 128 "mac_parse", 129 "multicast", 130 "package_encode", 131 "package_parse", 132 "package_parse_all", 133 "package_status_item", 134 "parse_header", 135 "parse_join_request", 136 "relay", 137 "session", 138] 139 140#: An activated session: a device address and its two session keys. 141Session = LorawanSession 142#: The root credentials over-the-air activation is built on. 143Device = LorawanDevice 144#: An accepted join: the network settings, and the session it grants. 145JoinAccept = LorawanJoinAccept 146#: A decoded data frame, with its payload decrypted. 147RxData = LorawanRxData 148#: What a frame says about itself before any key is involved. 149Header = LorawanHeader 150#: A join-request a device broadcast, with its integrity already verified. 151JoinRequest = LorawanJoinRequest 152#: What a network grants a device that joined. 153Grant = LorawanGrant 154#: The optional channel list at the end of a join accept. 155CfList = LorawanCfList 156#: A device's count of how long the network has been silent. 157Backoff = LorawanBackoff 158#: What a back-off says to do with one uplink. 159BackoffStep = LorawanBackoffStep 160#: A LoRaWAN Class A end device, without a radio. 161EndDevice = LorawanEndDevice 162#: What a device's radio can do, and how it takes part. 163DeviceSettings = LorawanDeviceSettings 164#: Raised when an end device cannot do what it was asked; ``kind`` names why. 165DeviceError = LorawanDeviceError 166#: A frame to put on the air, and where to listen afterward. 167Transmission = LorawanTransmission 168#: When and where to listen for a downlink. 169Window = LorawanWindow 170#: What a frame heard in a receive window turned out to be. 171Heard = LorawanHeard 172#: A downlink, read and acted on. 173Delivery = LorawanDelivery 174#: What to do once both receive windows closed with nothing for the device. 175Next = LorawanNext 176#: A channel a device may send on. 177Channel = LorawanChannel 178#: A LoRaWAN relay: an end device that also carries the uplinks of the devices around it. 179Relay = LorawanRelay 180#: Raised when a relay cannot do what it was asked; ``kind`` names why. 181RelayError = LorawanRelayError 182#: A scan for wake-on-radio frames, due next. 183Scan = LorawanScan 184#: What a wake-on-radio frame led a relay to do. 185Wake = LorawanWake 186#: The acknowledgment a relay answers a wake-on-radio frame with. 187Acknowledgment = LorawanAcknowledgment 188#: When and where a relay listens for the uplink a wake-on-radio frame announced. 189Listen = LorawanListen 190#: What a frame a relay's own device heard turned out to be. 191RelayHeard = LorawanRelayHeard 192#: A downlink for an end device, to send in its relay window. 193RxrDownlink = LorawanRxrDownlink 194#: The wake-on-radio exchange an uplink under a relay goes out behind. 195RelayExchange = LorawanRelayExchange 196#: The frame that wakes a relay, and where it goes. 197WakeUp = LorawanWakeUp 198#: When and where a relay's acknowledgment would arrive. 199AckWindow = LorawanAckWindow 200#: What a relay's acknowledgment said about itself. 201RelayStatus = LorawanRelayStatus 202#: What an end device does once its wake-on-radio frame went unanswered. 203WorNext = LorawanWorNext 204#: How many bytes a saved device state takes. 205SAVED_LEN = LORAWAN_SAVED_LEN 206 207#: How long after an uplink the first receive window opens, RP002-1.0.5 section 3.3. 208RECEIVE_DELAY1_US = LORAWAN_RECEIVE_DELAY1_US 209#: How long after an uplink the second receive window opens. 210RECEIVE_DELAY2_US = LORAWAN_RECEIVE_DELAY2_US 211#: How long after a join request the first join accept window opens. 212JOIN_ACCEPT_DELAY1_US = LORAWAN_JOIN_ACCEPT_DELAY1_US 213#: How long after a join request the second join accept window opens. 214JOIN_ACCEPT_DELAY2_US = LORAWAN_JOIN_ACCEPT_DELAY2_US 215#: How far a receive window may open either side of its time, LoRaWAN 1.0.3 section 3.3.1. 216RECEIVE_WINDOW_TOLERANCE_US = LORAWAN_RECEIVE_WINDOW_TOLERANCE_US 217#: The largest gap a frame counter may jump across and still be accepted. 218MAX_FCNT_GAP = LORAWAN_MAX_FCNT_GAP 219#: How many unanswered uplinks before a device asks the network to answer. 220ADR_ACK_LIMIT = LORAWAN_ADR_ACK_LIMIT 221#: How many more before a device starts giving back what adaptive data rate took. 222ADR_ACK_DELAY = LORAWAN_ADR_ACK_DELAY 223#: The shortest wait before a confirmed uplink is sent again. 224RETRANSMIT_TIMEOUT_MIN_US = LORAWAN_RETRANSMIT_TIMEOUT_MIN_US 225#: The longest wait before a confirmed uplink is sent again. 226RETRANSMIT_TIMEOUT_MAX_US = LORAWAN_RETRANSMIT_TIMEOUT_MAX_US 227 228 229class Version(str, enum.Enum): 230 """A revision of the LoRaWAN link layer, as :class:`Backoff` takes it. 231 232 >>> backoff = Backoff(Version.V1_0_3) 233 >>> backoff.version 234 '1.0.3' 235 """ 236 237 #: LoRaWAN 1.0.3. 238 V1_0_3 = "1.0.3" 239 #: TS001-1.0.4, the LoRaWAN 1.0.4 link layer. 240 V1_0_4 = "1.0.4" 241 242 243class ReceiveWindow(str, enum.Enum): 244 """Which receive window a frame arrived in, as :meth:`EndDevice.heard` takes it. 245 246 TS001-1.0.4 section 4.1 has a device discard a frame whose MACPayload is longer than 247 the data rate it was received at carries, so a device told the window holds each frame 248 to that window's limit. 249 """ 250 251 #: The first window, on the uplink's downlink channel. 252 RX1 = "rx1" 253 #: The second, on the fixed frequency and data rate. 254 RX2 = "rx2" 255 #: The relay window, which a device under a relay opens last, TS011-1.0.1 chapter 7. 256 RXR = "rxr" 257 258 259class RelayActivation(str, enum.Enum): 260 """How an end device decides whether to send through a relay, TS011-1.0.1 section 10.2.""" 261 262 #: Never, the default. 263 DISABLED = "disabled" 264 #: Always. 265 ENABLED = "enabled" 266 #: Only once a run of uplinks went unanswered, as the network's smart-enable level sets. 267 DYNAMIC = "dynamic" 268 #: However the device itself decides, which :meth:`EndDevice.use_relay` sets. 269 DEVICE_CONTROLLED = "device_controlled" 270 271 272class RelaySync(str, enum.Enum): 273 """What an end device knows of its relay's scans, TS011-1.0.1 section 3.9.""" 274 275 #: Nothing: no wake-on-radio frame has gone out yet. 276 INITIALIZED = "initialized" 277 #: One went out unanswered, so the next preamble spans a whole scan period. 278 UNSYNCHRONIZED = "unsynchronized" 279 #: It knows when the relay scans, so a short preamble reaches it. 280 SYNCHRONIZED = "synchronized" 281 282 283class MessageType(str, enum.Enum): 284 """What kind of message a frame is, read from its header.""" 285 286 #: A device asking to join a network. 287 JOIN_REQUEST = "JoinRequest" 288 #: A network admitting a device. 289 JOIN_ACCEPT = "JoinAccept" 290 #: Data from a device that does not need acknowledging. 291 UNCONFIRMED_UP = "UnconfirmedUp" 292 #: Data from a device that asks to be acknowledged. 293 CONFIRMED_UP = "ConfirmedUp" 294 #: Data to a device that does not need acknowledging. 295 UNCONFIRMED_DOWN = "UnconfirmedDown" 296 #: Data to a device that asks to be acknowledged. 297 CONFIRMED_DOWN = "ConfirmedDown" 298 299 300class Direction(str, enum.Enum): 301 """The direction a frame traveled, which its MIC and encryption fold in.""" 302 303 #: From an end device up to the network. 304 UPLINK = "Uplink" 305 #: From the network down to an end device. 306 DOWNLINK = "Downlink" 307 308 309def session(dev_addr: int, nwk_skey: bytes, app_skey: bytes) -> LorawanSession: 310 """Create a session for a device already activated by personalization. 311 312 :param dev_addr: The device address the network assigned. 313 :param nwk_skey: The 16-byte network session key, which authenticates frames. 314 :param app_skey: The 16-byte application session key, which encrypts payloads. 315 :returns: The session, ready to encode and decode data frames. 316 :raises PamojaError: If either key is not 16 bytes. 317 """ 318 return LorawanSession(dev_addr, bytes(nwk_skey), bytes(app_skey)) 319 320 321def device(dev_eui: bytes, app_eui: bytes, app_key: bytes) -> LorawanDevice: 322 """Create a device holding the root credentials for over-the-air activation. 323 324 :param dev_eui: The 8-byte device EUI. 325 :param app_eui: The 8-byte application (join) EUI. 326 :param app_key: The 16-byte application key the join exchange is secured with. 327 :returns: The device, ready to build a join request. 328 :raises PamojaError: If any credential is the wrong length. 329 """ 330 return LorawanDevice(bytes(dev_eui), bytes(app_eui), bytes(app_key)) 331 332 333def parse_header(data: bytes) -> Header: 334 """Read a frame far enough to route it, without any key. 335 336 A receiver holding many sessions uses this to find which one a frame belongs 337 to: the device address travels in the clear, so it can be read before the 338 session that would verify the frame is even known. 339 340 Nothing this reports is authenticated. Treat it as a routing hint until 341 :meth:`Session.decode` has verified the frame. 342 343 :param data: The raw frame as it came off the radio. 344 :returns: What the header says the frame is. 345 :raises PamojaError: If the frame is truncated or carries a message type this 346 build does not read. 347 """ 348 return _parse_header(bytes(data)) 349 350 351def parse_join_request(data: bytes, app_key: bytes) -> JoinRequest: 352 """Verify a join-request and read the identifiers out of it. 353 354 This is the network side of activation: it proves the request came from a 355 holder of the application key before reporting who sent it. 356 357 :param data: The raw join-request as it came off the radio. 358 :param app_key: The 16-byte application root key the device shares. 359 :returns: The verified request. 360 :raises PamojaError: If the MIC does not verify or the frame is not a 361 join-request. 362 """ 363 return _parse_join_request(bytes(data), bytes(app_key)) 364 365 366def grant( 367 app_nonce: int, 368 net_id: int, 369 dev_addr: int, 370 dl_settings: int = 0, 371 rx_delay: int = 0, 372 cflist: bytes | None = None, 373) -> Grant: 374 """Describe what this network grants a device that joined. 375 376 :param app_nonce: A nonce this network must not reuse for the device, since 377 the session keys are derived from it; low 24 bits only. 378 :param net_id: The network identifier; low 24 bits only. 379 :param dev_addr: The address to assign the device. 380 :param dl_settings: The downlink settings byte. 381 :param rx_delay: The delay before the first receive window, in seconds. 382 :param cflist: The optional 16-byte channel list. 383 :returns: The grant, which signs its own join-accept and derives the session. 384 :raises PamojaError: If the channel list is not 16 bytes. 385 """ 386 return Grant( 387 app_nonce, 388 net_id, 389 dev_addr, 390 dl_settings, 391 rx_delay, 392 None if cflist is None else bytes(cflist), 393 ) 394 395 396#: One of the commands a network and a device configure each other with. 397#: 398#: kind names the command and cid is the identifier it travels under. Only the 399#: fields that command carries are set; the rest are None. Writing one out is 400#: command.encode(). 401MacCommand = LorawanMacCommand 402 403 404def mac_parse(direction: Direction | str, data: bytes) -> list[MacCommand]: 405 """Read the commands packed into a frame options field, or a port 0 payload. 406 407 The same identifier means a different command in each direction, so the direction 408 decides what is read and there is no default: 0x03 going down is a request to 409 change data rate, and the same byte coming up answers one. 410 411 A command does not carry its own length, so one this build does not know cannot be 412 stepped over. Reading stops there and returns what came before it. 413 414 Args: 415 direction: Which way the frame carrying them travels. 416 data: The options field, or the payload. 417 418 Returns: 419 The commands that were readable, in order. 420 """ 421 name = direction.value if isinstance(direction, enum.Enum) else str(direction) 422 return _mac_parse(name, data) 423 424 425def end_device( 426 plan, 427 dev_eui: bytes, 428 join_eui: bytes, 429 app_key: bytes, 430 settings: LorawanDeviceSettings, 431 fcnt_up: int = 0, 432 fcnt_down: int | None = None, 433) -> LorawanEndDevice: 434 """Make an end device that joins over the air. 435 436 It owns no radio and no clock: every call takes the time in microseconds and hands 437 back what to put on the air, so the same device runs over any radio, or in a test 438 with none. :meth:`EndDevice.join` or :meth:`EndDevice.send` returns a transmission 439 and two receive windows; a frame heard in either goes to :meth:`EndDevice.heard`, 440 and :meth:`EndDevice.nothing_heard` says what to do when neither held one. 441 442 :param plan: A published channel plan, from ``pamoja.lora.plan_for`` or 443 ``pamoja.lora.cn470_plan``. 444 :param dev_eui: The 8-byte device EUI. 445 :param join_eui: The 8-byte join EUI. 446 :param app_key: The 16-byte root key. 447 :param settings: What the radio can do. 448 :param fcnt_up: The next uplink frame counter carried over a restart. 449 :param fcnt_down: The last downlink frame counter accepted, if any was. 450 :returns: The device, not yet joined. 451 :raises PamojaError: If the plan was built rather than published, or a credential is 452 the wrong length. 453 454 >>> from pamoja import lora 455 >>> node = end_device(lora.plan_for("EU868"), bytes(8), bytes(8), bytes(16), DeviceSettings(2, 14)) 456 >>> request = node.join(1, 0) 457 >>> request.rx1.delay_us 458 5000000 459 """ 460 return LorawanEndDevice.over_the_air( 461 plan, 462 LorawanDevice(bytes(dev_eui), bytes(join_eui), bytes(app_key)), 463 settings, 464 fcnt_up, 465 fcnt_down, 466 ) 467 468 469#: One command of an application layer package, whichever package it belongs to. 470#: 471#: ``port`` says which package and ``kind`` names the command within it; together they decide 472#: which of the other fields carry anything. The rest are ``None``. The same identifier means a 473#: different command in each direction, so ``uplink`` decides which one this is. 474PackageCommand = LorawanPackageCommand 475 476 477def package_parse(port: int, uplink: bool, data: bytes) -> PackageCommand: 478 """Read one command of an application layer package. 479 480 A data fragment takes the whole message, as TS004-2.0.0 asks, and its bytes come back 481 on ``data``. 482 483 Args: 484 port: Which package: one of :data:`clock.PORT`, :data:`fragment.PORT`, 485 :data:`multicast.PORT` or :data:`firmware.PORT`. 486 uplink: Whether the frame carrying it traveled up. 487 data: The message, from this command's identifier on. 488 489 Returns: 490 The command that was read. 491 492 Raises: 493 PamojaError: If the port names no package, the message ends inside the command, 494 or the identifier is not one the package defines. 495 """ 496 return _package_parse(port, uplink, data) 497 498 499def package_parse_all(port: int, uplink: bool, data: bytes) -> list[PackageCommand]: 500 """Read every command in one message. 501 502 A command does not carry its own length, so one this build does not know cannot be 503 stepped over. Reading stops there and returns what came before it. 504 505 Args: 506 port: Which package. 507 uplink: Whether the frame carrying them traveled up. 508 data: The message. 509 510 Returns: 511 The commands that were readable, in order. 512 513 Raises: 514 PamojaError: If the port names no package. 515 """ 516 return _package_parse_all(port, uplink, data) 517 518 519def package_status_item(data: bytes) -> PackageCommand: 520 """Read one group record of a multicast status answer. 521 522 The answer says how many groups follow; each is five bytes and carries no identifier 523 of its own, so they are read one at a time rather than by :func:`package_parse_all`. 524 525 Args: 526 data: The message, from the record on. 527 528 Returns: 529 The record, as an ``mc_group_status_item`` command. 530 531 Raises: 532 PamojaError: If the message ends inside the record. 533 """ 534 return _package_status_item(data) 535 536 537def package_encode(port: int, kind: str, uplink: bool = False, **fields: object) -> bytes: 538 """Write one command of an application layer package. 539 540 Args: 541 port: Which package. 542 kind: Which command within it. 543 uplink: Which way it travels. 544 **fields: The fields that command carries; see :class:`PackageCommand`. 545 546 Returns: 547 The bytes it goes out as. 548 549 Raises: 550 PamojaError: If the port names no package, the name is not a command that package 551 defines, or a field the command needs is absent. 552 """ 553 return _package_encode(PackageCommand(port, kind, uplink, **fields))
301class Direction(str, enum.Enum): 302 """The direction a frame traveled, which its MIC and encryption fold in.""" 303 304 #: From an end device up to the network. 305 UPLINK = "Uplink" 306 #: From the network down to an end device. 307 DOWNLINK = "Downlink"
The direction a frame traveled, which its MIC and encryption fold in.
284class MessageType(str, enum.Enum): 285 """What kind of message a frame is, read from its header.""" 286 287 #: A device asking to join a network. 288 JOIN_REQUEST = "JoinRequest" 289 #: A network admitting a device. 290 JOIN_ACCEPT = "JoinAccept" 291 #: Data from a device that does not need acknowledging. 292 UNCONFIRMED_UP = "UnconfirmedUp" 293 #: Data from a device that asks to be acknowledged. 294 CONFIRMED_UP = "ConfirmedUp" 295 #: Data to a device that does not need acknowledging. 296 UNCONFIRMED_DOWN = "UnconfirmedDown" 297 #: Data to a device that asks to be acknowledged. 298 CONFIRMED_DOWN = "ConfirmedDown"
What kind of message a frame is, read from its header.
244class ReceiveWindow(str, enum.Enum): 245 """Which receive window a frame arrived in, as :meth:`EndDevice.heard` takes it. 246 247 TS001-1.0.4 section 4.1 has a device discard a frame whose MACPayload is longer than 248 the data rate it was received at carries, so a device told the window holds each frame 249 to that window's limit. 250 """ 251 252 #: The first window, on the uplink's downlink channel. 253 RX1 = "rx1" 254 #: The second, on the fixed frequency and data rate. 255 RX2 = "rx2" 256 #: The relay window, which a device under a relay opens last, TS011-1.0.1 chapter 7. 257 RXR = "rxr"
Which receive window a frame arrived in, as EndDevice.heard() takes it.
TS001-1.0.4 section 4.1 has a device discard a frame whose MACPayload is longer than the data rate it was received at carries, so a device told the window holds each frame to that window's limit.
260class RelayActivation(str, enum.Enum): 261 """How an end device decides whether to send through a relay, TS011-1.0.1 section 10.2.""" 262 263 #: Never, the default. 264 DISABLED = "disabled" 265 #: Always. 266 ENABLED = "enabled" 267 #: Only once a run of uplinks went unanswered, as the network's smart-enable level sets. 268 DYNAMIC = "dynamic" 269 #: However the device itself decides, which :meth:`EndDevice.use_relay` sets. 270 DEVICE_CONTROLLED = "device_controlled"
How an end device decides whether to send through a relay, TS011-1.0.1 section 10.2.
273class RelaySync(str, enum.Enum): 274 """What an end device knows of its relay's scans, TS011-1.0.1 section 3.9.""" 275 276 #: Nothing: no wake-on-radio frame has gone out yet. 277 INITIALIZED = "initialized" 278 #: One went out unanswered, so the next preamble spans a whole scan period. 279 UNSYNCHRONIZED = "unsynchronized" 280 #: It knows when the relay scans, so a short preamble reaches it. 281 SYNCHRONIZED = "synchronized"
What an end device knows of its relay's scans, TS011-1.0.1 section 3.9.
230class Version(str, enum.Enum): 231 """A revision of the LoRaWAN link layer, as :class:`Backoff` takes it. 232 233 >>> backoff = Backoff(Version.V1_0_3) 234 >>> backoff.version 235 '1.0.3' 236 """ 237 238 #: LoRaWAN 1.0.3. 239 V1_0_3 = "1.0.3" 240 #: TS001-1.0.4, the LoRaWAN 1.0.4 link layer. 241 V1_0_4 = "1.0.4"
A revision of the LoRaWAN link layer, as Backoff takes it.
>>> backoff = Backoff(Version.V1_0_3)
>>> backoff.version
'1.0.3'
322def device(dev_eui: bytes, app_eui: bytes, app_key: bytes) -> LorawanDevice: 323 """Create a device holding the root credentials for over-the-air activation. 324 325 :param dev_eui: The 8-byte device EUI. 326 :param app_eui: The 8-byte application (join) EUI. 327 :param app_key: The 16-byte application key the join exchange is secured with. 328 :returns: The device, ready to build a join request. 329 :raises PamojaError: If any credential is the wrong length. 330 """ 331 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.
426def end_device( 427 plan, 428 dev_eui: bytes, 429 join_eui: bytes, 430 app_key: bytes, 431 settings: LorawanDeviceSettings, 432 fcnt_up: int = 0, 433 fcnt_down: int | None = None, 434) -> LorawanEndDevice: 435 """Make an end device that joins over the air. 436 437 It owns no radio and no clock: every call takes the time in microseconds and hands 438 back what to put on the air, so the same device runs over any radio, or in a test 439 with none. :meth:`EndDevice.join` or :meth:`EndDevice.send` returns a transmission 440 and two receive windows; a frame heard in either goes to :meth:`EndDevice.heard`, 441 and :meth:`EndDevice.nothing_heard` says what to do when neither held one. 442 443 :param plan: A published channel plan, from ``pamoja.lora.plan_for`` or 444 ``pamoja.lora.cn470_plan``. 445 :param dev_eui: The 8-byte device EUI. 446 :param join_eui: The 8-byte join EUI. 447 :param app_key: The 16-byte root key. 448 :param settings: What the radio can do. 449 :param fcnt_up: The next uplink frame counter carried over a restart. 450 :param fcnt_down: The last downlink frame counter accepted, if any was. 451 :returns: The device, not yet joined. 452 :raises PamojaError: If the plan was built rather than published, or a credential is 453 the wrong length. 454 455 >>> from pamoja import lora 456 >>> node = end_device(lora.plan_for("EU868"), bytes(8), bytes(8), bytes(16), DeviceSettings(2, 14)) 457 >>> request = node.join(1, 0) 458 >>> request.rx1.delay_us 459 5000000 460 """ 461 return LorawanEndDevice.over_the_air( 462 plan, 463 LorawanDevice(bytes(dev_eui), bytes(join_eui), bytes(app_key)), 464 settings, 465 fcnt_up, 466 fcnt_down, 467 )
Make an end device that joins over the air.
It owns no radio and no clock: every call takes the time in microseconds and hands
back what to put on the air, so the same device runs over any radio, or in a test
with none. EndDevice.join() or EndDevice.send() returns a transmission
and two receive windows; a frame heard in either goes to EndDevice.heard(),
and EndDevice.nothing_heard() says what to do when neither held one.
Parameters
- plan: A published channel plan, from
pamoja.lora.plan_fororpamoja.lora.cn470_plan. - dev_eui: The 8-byte device EUI.
- join_eui: The 8-byte join EUI.
- app_key: The 16-byte root key.
- settings: What the radio can do.
- fcnt_up: The next uplink frame counter carried over a restart.
- fcnt_down: The last downlink frame counter accepted, if any was. :returns: The device, not yet joined.
Raises
- PamojaError: If the plan was built rather than published, or a credential is the wrong length.
>>> from pamoja import lora
>>> node = end_device(lora.plan_for("EU868"), bytes(8), bytes(8), bytes(16), DeviceSettings(2, 14))
>>> request = node.join(1, 0)
>>> request.rx1.delay_us
5000000
367def grant( 368 app_nonce: int, 369 net_id: int, 370 dev_addr: int, 371 dl_settings: int = 0, 372 rx_delay: int = 0, 373 cflist: bytes | None = None, 374) -> Grant: 375 """Describe what this network grants a device that joined. 376 377 :param app_nonce: A nonce this network must not reuse for the device, since 378 the session keys are derived from it; low 24 bits only. 379 :param net_id: The network identifier; low 24 bits only. 380 :param dev_addr: The address to assign the device. 381 :param dl_settings: The downlink settings byte. 382 :param rx_delay: The delay before the first receive window, in seconds. 383 :param cflist: The optional 16-byte channel list. 384 :returns: The grant, which signs its own join-accept and derives the session. 385 :raises PamojaError: If the channel list is not 16 bytes. 386 """ 387 return Grant( 388 app_nonce, 389 net_id, 390 dev_addr, 391 dl_settings, 392 rx_delay, 393 None if cflist is None else bytes(cflist), 394 )
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.
405def mac_parse(direction: Direction | str, data: bytes) -> list[MacCommand]: 406 """Read the commands packed into a frame options field, or a port 0 payload. 407 408 The same identifier means a different command in each direction, so the direction 409 decides what is read and there is no default: 0x03 going down is a request to 410 change data rate, and the same byte coming up answers one. 411 412 A command does not carry its own length, so one this build does not know cannot be 413 stepped over. Reading stops there and returns what came before it. 414 415 Args: 416 direction: Which way the frame carrying them travels. 417 data: The options field, or the payload. 418 419 Returns: 420 The commands that were readable, in order. 421 """ 422 name = direction.value if isinstance(direction, enum.Enum) else str(direction) 423 return _mac_parse(name, data)
Read the commands packed into a frame options field, or a port 0 payload.
The same identifier means a different command in each direction, so the direction decides what is read and there is no default: 0x03 going down is a request to change data rate, and the same byte coming up answers one.
A command does not carry its own length, so one this build does not know cannot be stepped over. Reading stops there and returns what came before it.
Args: direction: Which way the frame carrying them travels. data: The options field, or the payload.
Returns: The commands that were readable, in order.
538def package_encode(port: int, kind: str, uplink: bool = False, **fields: object) -> bytes: 539 """Write one command of an application layer package. 540 541 Args: 542 port: Which package. 543 kind: Which command within it. 544 uplink: Which way it travels. 545 **fields: The fields that command carries; see :class:`PackageCommand`. 546 547 Returns: 548 The bytes it goes out as. 549 550 Raises: 551 PamojaError: If the port names no package, the name is not a command that package 552 defines, or a field the command needs is absent. 553 """ 554 return _package_encode(PackageCommand(port, kind, uplink, **fields))
Write one command of an application layer package.
Args:
port: Which package.
kind: Which command within it.
uplink: Which way it travels.
**fields: The fields that command carries; see PackageCommand.
Returns: The bytes it goes out as.
Raises: PamojaError: If the port names no package, the name is not a command that package defines, or a field the command needs is absent.
478def package_parse(port: int, uplink: bool, data: bytes) -> PackageCommand: 479 """Read one command of an application layer package. 480 481 A data fragment takes the whole message, as TS004-2.0.0 asks, and its bytes come back 482 on ``data``. 483 484 Args: 485 port: Which package: one of :data:`clock.PORT`, :data:`fragment.PORT`, 486 :data:`multicast.PORT` or :data:`firmware.PORT`. 487 uplink: Whether the frame carrying it traveled up. 488 data: The message, from this command's identifier on. 489 490 Returns: 491 The command that was read. 492 493 Raises: 494 PamojaError: If the port names no package, the message ends inside the command, 495 or the identifier is not one the package defines. 496 """ 497 return _package_parse(port, uplink, data)
Read one command of an application layer package.
A data fragment takes the whole message, as TS004-2.0.0 asks, and its bytes come back
on data.
Args:
port: Which package: one of clock.PORT, fragment.PORT,
multicast.PORT or firmware.PORT.
uplink: Whether the frame carrying it traveled up.
data: The message, from this command's identifier on.
Returns: The command that was read.
Raises: PamojaError: If the port names no package, the message ends inside the command, or the identifier is not one the package defines.
500def package_parse_all(port: int, uplink: bool, data: bytes) -> list[PackageCommand]: 501 """Read every command in one message. 502 503 A command does not carry its own length, so one this build does not know cannot be 504 stepped over. Reading stops there and returns what came before it. 505 506 Args: 507 port: Which package. 508 uplink: Whether the frame carrying them traveled up. 509 data: The message. 510 511 Returns: 512 The commands that were readable, in order. 513 514 Raises: 515 PamojaError: If the port names no package. 516 """ 517 return _package_parse_all(port, uplink, data)
Read every command in one message.
A command does not carry its own length, so one this build does not know cannot be stepped over. Reading stops there and returns what came before it.
Args: port: Which package. uplink: Whether the frame carrying them traveled up. data: The message.
Returns: The commands that were readable, in order.
Raises: PamojaError: If the port names no package.
520def package_status_item(data: bytes) -> PackageCommand: 521 """Read one group record of a multicast status answer. 522 523 The answer says how many groups follow; each is five bytes and carries no identifier 524 of its own, so they are read one at a time rather than by :func:`package_parse_all`. 525 526 Args: 527 data: The message, from the record on. 528 529 Returns: 530 The record, as an ``mc_group_status_item`` command. 531 532 Raises: 533 PamojaError: If the message ends inside the record. 534 """ 535 return _package_status_item(data)
Read one group record of a multicast status answer.
The answer says how many groups follow; each is five bytes and carries no identifier
of its own, so they are read one at a time rather than by package_parse_all().
Args: data: The message, from the record on.
Returns:
The record, as an mc_group_status_item command.
Raises: PamojaError: If the message ends inside the record.
334def parse_header(data: bytes) -> Header: 335 """Read a frame far enough to route it, without any key. 336 337 A receiver holding many sessions uses this to find which one a frame belongs 338 to: the device address travels in the clear, so it can be read before the 339 session that would verify the frame is even known. 340 341 Nothing this reports is authenticated. Treat it as a routing hint until 342 :meth:`Session.decode` has verified the frame. 343 344 :param data: The raw frame as it came off the radio. 345 :returns: What the header says the frame is. 346 :raises PamojaError: If the frame is truncated or carries a message type this 347 build does not read. 348 """ 349 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.
352def parse_join_request(data: bytes, app_key: bytes) -> JoinRequest: 353 """Verify a join-request and read the identifiers out of it. 354 355 This is the network side of activation: it proves the request came from a 356 holder of the application key before reporting who sent it. 357 358 :param data: The raw join-request as it came off the radio. 359 :param app_key: The 16-byte application root key the device shares. 360 :returns: The verified request. 361 :raises PamojaError: If the MIC does not verify or the frame is not a 362 join-request. 363 """ 364 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.
310def session(dev_addr: int, nwk_skey: bytes, app_skey: bytes) -> LorawanSession: 311 """Create a session for a device already activated by personalization. 312 313 :param dev_addr: The device address the network assigned. 314 :param nwk_skey: The 16-byte network session key, which authenticates frames. 315 :param app_skey: The 16-byte application session key, which encrypts payloads. 316 :returns: The session, ready to encode and decode data frames. 317 :raises PamojaError: If either key is not 16 bytes. 318 """ 319 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.