pamoja.gateway

Idiomatic LoRaWAN gateway facade.

A gateway hears packets from every node in range and hands them to a network server, which hands back the packets to transmit. The protocol between them is a plain exchange of UDP datagrams, and this speaks it from both sides: build a datagram, encode() it onto a socket of your own, and parse() whatever arrives.

Frequencies are in hertz, payloads are bytes rather than base64, and a reception time is a count of microseconds, so nothing has to be formatted by hand.

The Basics Station protocol is here from both sides too. A server reads a station's discovery request with station_discovery_parse() and answers it with station_router_accepted(); a station reports a frame it heard with station_heard(); and any other message is a StationMessage built from the fields of its kind and written with station_encode().

Example::

from pamoja.gateway import Packet, PacketKind, Rxpk, encode
from pamoja.lora import link

heard = Rxpk(868_100_000, b"hello", link=link(7, 125_000), rssi_dbm=-35.0, snr_db=5.1)
datagram = encode(Packet(PacketKind.PUSH_DATA, 0x1234, gateway="b827ebfffe010203",
                         packets=[heard]))
  1"""Idiomatic LoRaWAN gateway facade.
  2
  3A gateway hears packets from every node in range and hands them to a network server, which
  4hands back the packets to transmit. The protocol between them is a plain exchange of UDP
  5datagrams, and this speaks it from both sides: build a datagram, :func:`encode` it onto a
  6socket of your own, and :func:`parse` whatever arrives.
  7
  8Frequencies are in hertz, payloads are ``bytes`` rather than base64, and a reception time is a
  9count of microseconds, so nothing has to be formatted by hand.
 10
 11The Basics Station protocol is here from both sides too. A server reads a station's discovery
 12request with :func:`station_discovery_parse` and answers it with
 13:func:`station_router_accepted`; a station reports a frame it heard with :func:`station_heard`;
 14and any other message is a :class:`StationMessage` built from the fields of its kind and
 15written with :func:`station_encode`.
 16
 17Example::
 18
 19    from pamoja.gateway import Packet, PacketKind, Rxpk, encode
 20    from pamoja.lora import link
 21
 22    heard = Rxpk(868_100_000, b"hello", link=link(7, 125_000), rssi_dbm=-35.0, snr_db=5.1)
 23    datagram = encode(Packet(PacketKind.PUSH_DATA, 0x1234, gateway="b827ebfffe010203",
 24                             packets=[heard]))
 25"""
 26
 27from __future__ import annotations
 28
 29import enum
 30
 31from pamoja._native import CHIRPSTACK_UPLINK_TOPIC
 32from pamoja._native import ChirpstackReception
 33from pamoja._native import ChirpstackUplinkEvent
 34from pamoja._native import GatewayNetwork as Network
 35from pamoja._native import GatewayNetworkEvent as NetworkEvent
 36from pamoja._native import GatewayNotice as Notice
 37from pamoja._native import GatewayPacket as Packet
 38from pamoja._native import GatewayRelayed as Relayed
 39from pamoja._native import GatewayRxpk as Rxpk
 40from pamoja._native import GatewaySlot as Slot
 41from pamoja._native import GatewayStat as Stat
 42from pamoja._native import GatewayTxpk as Txpk
 43from pamoja._native import GatewayStationBroadcast as StationBroadcast
 44from pamoja._native import GatewayStationDataRate as StationDataRate
 45from pamoja._native import GatewayStationJoinRange as StationJoinRange
 46from pamoja._native import GatewayStationLevels as StationLevels
 47from pamoja._native import GatewayStationMessage as StationMessage
 48from pamoja._native import GatewayStationRouter as StationRouter
 49from pamoja._native import GatewayStationWindow as StationWindow
 50from pamoja._native import GatewayStationXtime as StationXtime
 51from pamoja._native import gateway_acknowledgment as acknowledgment
 52from pamoja._native import gateway_encode as encode
 53from pamoja._native import gateway_parse as parse
 54from pamoja._native import chirpstack_uplink_topic
 55from pamoja._native import station_discovery
 56from pamoja._native import station_discovery_parse
 57from pamoja._native import station_encode
 58from pamoja._native import station_eui_of
 59from pamoja._native import station_heard
 60from pamoja._native import station_id6
 61from pamoja._native import station_parse
 62from pamoja._native import station_router_accepted
 63from pamoja._native import station_router_parse
 64from pamoja._native import station_router_refused
 65from pamoja._native import station_xtime
 66from pamoja._native import station_xtime_parts
 67
 68__all__ = [
 69    "CHIRPSTACK_UPLINK_TOPIC",
 70    "ChirpstackReception",
 71    "ChirpstackUplinkEvent",
 72    "DEFAULT_PORT",
 73    "DISCOVERY_PATH",
 74    "STATION_PROTOCOL_VERSION",
 75    "Crc",
 76    "Network",
 77    "NetworkEvent",
 78    "Notice",
 79    "Packet",
 80    "PacketKind",
 81    "Relayed",
 82    "Rxpk",
 83    "Slot",
 84    "Stat",
 85    "StationBroadcast",
 86    "StationDataRate",
 87    "StationJoinRange",
 88    "StationKind",
 89    "StationLevels",
 90    "StationMessage",
 91    "StationRouter",
 92    "StationWindow",
 93    "StationXtime",
 94    "TxStatus",
 95    "Txpk",
 96    "acknowledgment",
 97    "chirpstack_uplink_topic",
 98    "encode",
 99    "parse",
100    "station_discovery",
101    "station_discovery_parse",
102    "station_encode",
103    "station_eui_of",
104    "station_heard",
105    "station_id6",
106    "station_parse",
107    "station_router_accepted",
108    "station_router_parse",
109    "station_router_refused",
110    "station_xtime",
111    "station_xtime_parts",
112]
113
114#: The port a packet forwarder sends to by convention, which the protocol itself does not fix.
115DEFAULT_PORT = 1700
116
117#: The path a station appends to its configured address to find its network server.
118DISCOVERY_PATH = "/router-info"
119
120#: The protocol version a station reports.
121STATION_PROTOCOL_VERSION = 2
122
123
124class PacketKind(str, enum.Enum):
125    """Which kind of datagram."""
126
127    #: The gateway forwarding what it heard.
128    PUSH_DATA = "PushData"
129    #: The server acknowledging a PUSH_DATA.
130    PUSH_ACK = "PushAck"
131    #: The gateway holding its route open through any address translation in front of it.
132    PULL_DATA = "PullData"
133    #: The server sending a packet to transmit.
134    PULL_RESP = "PullResp"
135    #: The server acknowledging a PULL_DATA.
136    PULL_ACK = "PullAck"
137    #: The gateway reporting what became of a PULL_RESP.
138    TX_ACK = "TxAck"
139
140
141class Crc(str, enum.Enum):
142    """What the CRC of a received packet said."""
143
144    #: The CRC checked.
145    OK = "Ok"
146    #: The CRC failed.
147    FAILED = "Failed"
148    #: The packet carried no CRC.
149    ABSENT = "Absent"
150
151
152class TxStatus(str, enum.Enum):
153    """What became of a downlink the server asked for, as the protocol names it."""
154
155    #: It was scheduled.
156    NONE = "NONE"
157    #: It arrived too late to schedule.
158    TOO_LATE = "TOO_LATE"
159    #: Its timestamp is too far ahead.
160    TOO_EARLY = "TOO_EARLY"
161    #: Another packet was already scheduled then.
162    COLLISION_PACKET = "COLLISION_PACKET"
163    #: A beacon was already scheduled then.
164    COLLISION_BEACON = "COLLISION_BEACON"
165    #: The radio chain cannot reach that frequency.
166    TX_FREQ = "TX_FREQ"
167    #: The gateway cannot transmit at that power.
168    TX_POWER = "TX_POWER"
169    #: A GPS timestamp was asked for while the GPS is unlocked.
170    GPS_UNLOCKED = "GPS_UNLOCKED"
171
172
173class StationKind(str, enum.Enum):
174    """Which kind of message a session carries, as the protocol writes it."""
175
176    #: What the station reports about itself when a session opens.
177    VERSION = "version"
178    #: How the server tells the station to configure its radios.
179    ROUTER_CONFIG = "router_config"
180    #: A join request the station heard.
181    JOIN_REQUEST = "jreq"
182    #: A data frame the station heard.
183    UPLINK = "updf"
184    #: A frame of a kind this protocol does not describe, carried whole.
185    PROPRIETARY = "propdf"
186    #: A frame the server asks the station to transmit.
187    DOWNLINK = "dnmsg"
188    #: Frames the server asks the station to transmit to a group.
189    SCHEDULE = "dnsched"
190    #: What became of a frame the station was asked to transmit.
191    TRANSMITTED = "dntxed"
192    #: The clock the two keep between them.
193    TIME_SYNC = "timesync"
class ChirpstackReception:

One gateway that heard an uplink.

rssi_dbm

The received signal strength, in dBm.

snr_db

The signal-to-noise ratio, in dB.

gateway

The gateway's EUI, as lowercase hex.

class ChirpstackUplinkEvent:

An uplink as ChirpStack reports it.

def from_json(text):

Reads an uplink event from the JSON ChirpStack published.

Fields protobuf's JSON mapping leaves out when they hold their default read as that default: a frame counter of zero, ADR off, unconfirmed. Raises PamojaError for text that is not a JSON object, an event with no device EUI, or a field that does not read as what it should.

def best_reception(self, /):

The gateway that heard it with the highest signal-to-noise ratio, or None when none did.

receptions

Every gateway that heard it.

data

The application payload, decoded from base64.

fcnt

The uplink frame counter.

device_name

The name the device was given in ChirpStack.

dev_addr

The device's address, or None when the event names none.

deduplication_id

The identifier ChirpStack gave the uplink once it deduplicated the gateways' copies.

application_id

The application the device belongs to.

dev_eui

The device EUI, as lowercase hex.

adr

Whether the device had adaptive data rate on.

data_rate

The data rate, as the region numbers them.

fport

The application port, or None for a frame that carried none.

time

When the uplink was received, as ChirpStack wrote it, or None.

confirmed

Whether the uplink was confirmed.

frequency_hz

The carrier it was heard on, in hertz, or None.

DEFAULT_PORT = 1700
DISCOVERY_PATH = '/router-info'
STATION_PROTOCOL_VERSION = 2
class Crc(builtins.str, enum.Enum):
142class Crc(str, enum.Enum):
143    """What the CRC of a received packet said."""
144
145    #: The CRC checked.
146    OK = "Ok"
147    #: The CRC failed.
148    FAILED = "Failed"
149    #: The packet carried no CRC.
150    ABSENT = "Absent"

What the CRC of a received packet said.

OK = <Crc.OK: 'Ok'>
FAILED = <Crc.FAILED: 'Failed'>
ABSENT = <Crc.ABSENT: 'Absent'>
Network = <class 'builtins.GatewayNetwork'>
NetworkEvent = <class 'builtins.GatewayNetworkEvent'>
Notice = <class 'builtins.GatewayNotice'>
Packet = <class 'builtins.GatewayPacket'>
class PacketKind(builtins.str, enum.Enum):
125class PacketKind(str, enum.Enum):
126    """Which kind of datagram."""
127
128    #: The gateway forwarding what it heard.
129    PUSH_DATA = "PushData"
130    #: The server acknowledging a PUSH_DATA.
131    PUSH_ACK = "PushAck"
132    #: The gateway holding its route open through any address translation in front of it.
133    PULL_DATA = "PullData"
134    #: The server sending a packet to transmit.
135    PULL_RESP = "PullResp"
136    #: The server acknowledging a PULL_DATA.
137    PULL_ACK = "PullAck"
138    #: The gateway reporting what became of a PULL_RESP.
139    TX_ACK = "TxAck"

Which kind of datagram.

PUSH_DATA = <PacketKind.PUSH_DATA: 'PushData'>
PUSH_ACK = <PacketKind.PUSH_ACK: 'PushAck'>
PULL_DATA = <PacketKind.PULL_DATA: 'PullData'>
PULL_RESP = <PacketKind.PULL_RESP: 'PullResp'>
PULL_ACK = <PacketKind.PULL_ACK: 'PullAck'>
TX_ACK = <PacketKind.TX_ACK: 'TxAck'>
Relayed = <class 'builtins.GatewayRelayed'>
Rxpk = <class 'builtins.GatewayRxpk'>
Slot = <class 'builtins.GatewaySlot'>
Stat = <class 'builtins.GatewayStat'>
StationBroadcast = <class 'builtins.GatewayStationBroadcast'>
StationDataRate = <class 'builtins.GatewayStationDataRate'>
StationJoinRange = <class 'builtins.GatewayStationJoinRange'>
class StationKind(builtins.str, enum.Enum):
174class StationKind(str, enum.Enum):
175    """Which kind of message a session carries, as the protocol writes it."""
176
177    #: What the station reports about itself when a session opens.
178    VERSION = "version"
179    #: How the server tells the station to configure its radios.
180    ROUTER_CONFIG = "router_config"
181    #: A join request the station heard.
182    JOIN_REQUEST = "jreq"
183    #: A data frame the station heard.
184    UPLINK = "updf"
185    #: A frame of a kind this protocol does not describe, carried whole.
186    PROPRIETARY = "propdf"
187    #: A frame the server asks the station to transmit.
188    DOWNLINK = "dnmsg"
189    #: Frames the server asks the station to transmit to a group.
190    SCHEDULE = "dnsched"
191    #: What became of a frame the station was asked to transmit.
192    TRANSMITTED = "dntxed"
193    #: The clock the two keep between them.
194    TIME_SYNC = "timesync"

Which kind of message a session carries, as the protocol writes it.

VERSION = <StationKind.VERSION: 'version'>
ROUTER_CONFIG = <StationKind.ROUTER_CONFIG: 'router_config'>
JOIN_REQUEST = <StationKind.JOIN_REQUEST: 'jreq'>
PROPRIETARY = <StationKind.PROPRIETARY: 'propdf'>
SCHEDULE = <StationKind.SCHEDULE: 'dnsched'>
TRANSMITTED = <StationKind.TRANSMITTED: 'dntxed'>
TIME_SYNC = <StationKind.TIME_SYNC: 'timesync'>
StationLevels = <class 'builtins.GatewayStationLevels'>
StationMessage = <class 'builtins.GatewayStationMessage'>
StationRouter = <class 'builtins.GatewayStationRouter'>
StationWindow = <class 'builtins.GatewayStationWindow'>
StationXtime = <class 'builtins.GatewayStationXtime'>
class TxStatus(builtins.str, enum.Enum):
153class TxStatus(str, enum.Enum):
154    """What became of a downlink the server asked for, as the protocol names it."""
155
156    #: It was scheduled.
157    NONE = "NONE"
158    #: It arrived too late to schedule.
159    TOO_LATE = "TOO_LATE"
160    #: Its timestamp is too far ahead.
161    TOO_EARLY = "TOO_EARLY"
162    #: Another packet was already scheduled then.
163    COLLISION_PACKET = "COLLISION_PACKET"
164    #: A beacon was already scheduled then.
165    COLLISION_BEACON = "COLLISION_BEACON"
166    #: The radio chain cannot reach that frequency.
167    TX_FREQ = "TX_FREQ"
168    #: The gateway cannot transmit at that power.
169    TX_POWER = "TX_POWER"
170    #: A GPS timestamp was asked for while the GPS is unlocked.
171    GPS_UNLOCKED = "GPS_UNLOCKED"

What became of a downlink the server asked for, as the protocol names it.

NONE = <TxStatus.NONE: 'NONE'>
TOO_LATE = <TxStatus.TOO_LATE: 'TOO_LATE'>
TOO_EARLY = <TxStatus.TOO_EARLY: 'TOO_EARLY'>
COLLISION_PACKET = <TxStatus.COLLISION_PACKET: 'COLLISION_PACKET'>
COLLISION_BEACON = <TxStatus.COLLISION_BEACON: 'COLLISION_BEACON'>
TX_FREQ = <TxStatus.TX_FREQ: 'TX_FREQ'>
TX_POWER = <TxStatus.TX_POWER: 'TX_POWER'>
GPS_UNLOCKED = <TxStatus.GPS_UNLOCKED: 'GPS_UNLOCKED'>
Txpk = <class 'builtins.GatewayTxpk'>
def acknowledgment(packet):

Returns the acknowledgment a server owes a datagram, or None for one that needs none.

def encode(packet):

Writes a datagram to send over a socket.

def parse(datagram):

Reads a datagram that arrived.

def station_discovery(router):

Writes the request a station sends to find its network server.

def station_discovery_parse(text):

Reads the request a station sent to find its network server, as the server does, and returns the station asking as sixteen hexadecimal digits.

def station_encode(message):

Writes a message of any kind the way the websocket carries it.

def station_eui_of(text):

Reads an identifier written in any form the protocol accepts.

def station_heard(frame, data_rate, frequency_hz, levels=None):

Reads a frame the radio heard into the message that reports it.

def station_id6(eui):

Writes an identifier in the ID6 form the protocol prefers.

def station_parse(text):

Reads a message that arrived over the websocket.

def station_router_accepted(router, muxs, uri):

Writes the answer that sends a station to the websocket its session runs on.

def station_router_parse(text):

Reads the answer a discovery endpoint gives.

def station_router_refused(router, error):

Writes the answer that refuses a station, saying why.

def station_xtime(unit, session, micros):

Builds a station clock value from the radio it was read on, the run of the station, and the microseconds that run had counted.

def station_xtime_parts(xtime):

Takes a station clock value apart into its radio unit, run, and microseconds.