pamoja.lora
Idiomatic LoRa link-budget facade.
LoRa buys kilometres of range on license-free bands at tiny power, and the price is time: a transmission occupies the channel for a duration the radio settings fix, and the regional rules cap how much of the time a node may transmit. This is the arithmetic that keeps a node inside that budget, with no radio involved.
1"""Idiomatic LoRa link-budget facade. 2 3LoRa buys kilometres of range on license-free bands at tiny power, and the price 4is time: a transmission occupies the channel for a duration the radio settings 5fix, and the regional rules cap how much of the time a node may transmit. This is 6the arithmetic that keeps a node inside that budget, with no radio involved. 7""" 8 9from __future__ import annotations 10 11from pamoja._native import ( 12 ChannelPlan, 13 ChannelPlanBuilder, 14 LoraBeacon, 15 LoraChannelBlock, 16 LoraDataRate, 17 LoraLink, 18 LoraMaxPayload, 19 LoraPlanInfo, 20 LoraSubBand, 21) 22 23__all__ = [ 24 "ChannelPlan", 25 "ChannelPlanBuilder", 26 "LoraBeacon", 27 "LoraChannelBlock", 28 "LoraDataRate", 29 "LoraLink", 30 "LoraMaxPayload", 31 "LoraPlanInfo", 32 "LoraSubBand", 33 "REGIONS", 34 "link", 35 "messages_per_hour", 36 "messages_per_hour_at", 37 "plan_for", 38] 39 40#: The bands with a published channel plan. 41REGIONS = ( 42 "EU868", 43 "US915", 44 "EU433", 45 "AU915", 46 "CN470", 47 "AS923", 48 "KR920", 49 "IN865", 50 "RU864", 51) 52 53 54def link( 55 spreading_factor: int, 56 bandwidth_hz: int, 57 coding_rate_denominator: int = 5, 58 preamble_symbols: int = 8, 59 explicit_header: bool = True, 60 crc: bool = True, 61) -> LoraLink: 62 """Describe a LoRa link, clamping every value to its LoRa range. 63 64 The defaults are coding rate 4/5, an eight-symbol preamble, an explicit 65 header, and CRC on, which is a typical uplink. 66 67 :param spreading_factor: The spreading factor, clamped to 5 (fastest) to 12 68 (longest range). 69 :param bandwidth_hz: The channel bandwidth in hertz, such as ``125_000``. 70 :param coding_rate_denominator: The coding-rate denominator, clamped to 5 to 8. 71 :param preamble_symbols: The preamble length in symbols. 72 :param explicit_header: Whether the frame carries an explicit header. 73 :param crc: Whether the frame carries a CRC. 74 :returns: The link, which answers for its own airtime and off time. 75 """ 76 return LoraLink( 77 spreading_factor, 78 bandwidth_hz, 79 coding_rate_denominator, 80 preamble_symbols, 81 explicit_header, 82 crc, 83 ) 84 85 86def messages_per_hour( 87 settings: LoraLink, payload_len: int, duty_cycle_permille: int 88) -> int: 89 """Return how many transmissions of a payload fit in an hour under a limit. 90 91 The airtime plus the silence it forces is what one transmission really costs, 92 so this is the message budget a deployment plans against. 93 94 :param settings: The link settings. 95 :param payload_len: The payload length in bytes. 96 :param duty_cycle_permille: The limit in parts per thousand, so ``10`` is 1%. 97 :returns: The number of whole transmissions per hour, or ``0`` when the limit 98 forbids transmitting. 99 """ 100 off_time = settings.min_off_time_us(payload_len, duty_cycle_permille) 101 if off_time is None: 102 return 0 103 return 3_600_000_000 // (settings.airtime_us(payload_len) + off_time) 104 105 106def plan_for(region: str) -> ChannelPlan: 107 """Return the published channel plan for a region. 108 109 A channel plan is what a regulator and the LoRa Alliance publish about one 110 band: which data rates exist, what each carries, how much of the time a node 111 may hold a frequency, and where it listens for a downlink. The plan reports 112 those facts and costs a transmission out against them; it never refuses one, 113 because a deployment may hold licensed spectrum or be working under emergency 114 provisions and only the operator knows which. 115 116 :param region: The band to describe, such as ``EU868``. See :data:`REGIONS`. 117 :returns: The plan, which answers every question about that band. 118 :raises ValueError: If no published region goes by that name. 119 120 >>> plan = plan_for("EU868") 121 >>> plan.name 122 'EU863-870' 123 >>> plan.link_settings(0).spreading_factor 124 12 125 >>> plan.duty_cycle_permille(868_100_000) 126 10 127 """ 128 return ChannelPlan.for_region(region) 129 130 131def messages_per_hour_at( 132 plan: ChannelPlan, data_rate: int, payload_len: int, frequency_hz: int 133) -> int | None: 134 """Return how many transmissions fit in an hour at a data rate the plan defines. 135 136 This is the budget question a deployment actually asks: not what the radio 137 can do, but how often it may speak on this band at this setting. The duty 138 cycle of the frequency it transmits on decides the answer. 139 140 :param plan: The channel plan to read. 141 :param data_rate: The uplink data-rate number. 142 :param payload_len: The payload length in bytes. 143 :param frequency_hz: The frequency the node transmits on. 144 :returns: The number of whole transmissions per hour, or ``None`` when the 145 plan does not describe that data rate or frequency. 146 147 >>> plan = plan_for("EU868") 148 >>> messages_per_hour_at(plan, 5, 20, 868_100_000) > 0 149 True 150 """ 151 settings = plan.link_settings(data_rate) 152 permille = plan.duty_cycle_permille(frequency_hz) 153 if settings is None or permille is None: 154 return None 155 return messages_per_hour(settings, payload_len, permille)
A regional channel plan, published or private.
Returns the published plan for a region.
Raises ValueError if no published region goes by that name.
Returns the short code of every published region, as for_region takes.
These are the codes a deployment is configured with. The band name the
specification uses, such as EU863-870, is the plan's name.
Returns the data rate a number selects, or None past the end of the
plan's table.
A number the region reserves is a data rate of kind reserved, which is
different from a number the plan never defines.
Returns the radio settings an uplink data rate selects, ready to hand to
airtime_us, or None if the number is reserved or not carried by LoRa.
Returns what a data rate may carry in one frame, or None where the plan
publishes no limit for it.
Returns the share of time a transmitter may hold a frequency, in parts per
thousand, or None if the frequency falls in no sub-band this plan
describes.
This reports the limit; it does not impose it. Pair it with
min_off_time_us to turn the limit into the silence a frame costs.
Returns the power ceiling that applies at a frequency, in dBm EIRP, falling back to the plan's default where no sub-band says otherwise.
Returns the radiated power a transmit-power index selects, in dBm, or
None if the index is past the highest the plan defines.
Returns the downlink data rate the first receive window listens at, or
None if the uplink data rate or offset is outside what the plan defines.
Returns where the second receive window listens, as a frequency in hertz and a data rate.
Returns the next lower data rate to fall back to during adaptive back-off,
or None at the slowest rate the plan has.
A device that has lost the network steps down this chain, trading airtime for range until it is heard again.
Returns the centre frequency of one of the plan's default channels, or
None past the last one the plan starts a device with.
A channel plan under construction.
Tables are indexed by position, so entries are added in data-rate order and a
number the plan does not use is added as a reserved data rate. What a region
would share between directions is filled in by build.
Adds the next data rate in a direction.
A plan that never adds a downlink rate uses its uplink table in both directions, which is what every region but the 900 MHz plans does.
Adds the next entry of one payload table.
Pass no payload for a data rate that carries nothing. A downlink table left empty mirrors the matching uplink one.
Adds a sub-band and the transmit limits inside it.
A deployment on licensed spectrum gives its sub-band a duty cycle of
1000, which reports as unrestricted.
Adds the RX1 downlink data rates for the next uplink data rate.
Every row must be as wide as the plan's highest RX1 offset allows.
Adds the next entry of the adaptive back-off chain.
Pass no data rate at the slowest, which has nothing below it. A chain left empty steps down one data rate at a time.
Sets the receive windows.
max_rx1_offset fixes how wide every RX1 row must be.
The Class B beacon settings of a plan.
A run of evenly spaced channels.
One data rate: what a number on the wire means for the radio.
Only the attributes belonging to kind are set; the rest are None.
The radio settings of a LoRa link.
The time on air of a payload, in microseconds.
This is the channel occupancy a transmission costs, which sets both the duty-cycle budget and most of the energy the transmission spends.
What one data rate may carry in a single frame.
The scalar facts of a plan, read in one call.
A slice of a band with its own transmit limits.
55def link( 56 spreading_factor: int, 57 bandwidth_hz: int, 58 coding_rate_denominator: int = 5, 59 preamble_symbols: int = 8, 60 explicit_header: bool = True, 61 crc: bool = True, 62) -> LoraLink: 63 """Describe a LoRa link, clamping every value to its LoRa range. 64 65 The defaults are coding rate 4/5, an eight-symbol preamble, an explicit 66 header, and CRC on, which is a typical uplink. 67 68 :param spreading_factor: The spreading factor, clamped to 5 (fastest) to 12 69 (longest range). 70 :param bandwidth_hz: The channel bandwidth in hertz, such as ``125_000``. 71 :param coding_rate_denominator: The coding-rate denominator, clamped to 5 to 8. 72 :param preamble_symbols: The preamble length in symbols. 73 :param explicit_header: Whether the frame carries an explicit header. 74 :param crc: Whether the frame carries a CRC. 75 :returns: The link, which answers for its own airtime and off time. 76 """ 77 return LoraLink( 78 spreading_factor, 79 bandwidth_hz, 80 coding_rate_denominator, 81 preamble_symbols, 82 explicit_header, 83 crc, 84 )
Describe a LoRa link, clamping every value to its LoRa range.
The defaults are coding rate 4/5, an eight-symbol preamble, an explicit header, and CRC on, which is a typical uplink.
Parameters
- spreading_factor: The spreading factor, clamped to 5 (fastest) to 12 (longest range).
- bandwidth_hz: The channel bandwidth in hertz, such as
125_000. - coding_rate_denominator: The coding-rate denominator, clamped to 5 to 8.
- preamble_symbols: The preamble length in symbols.
- explicit_header: Whether the frame carries an explicit header.
- crc: Whether the frame carries a CRC. :returns: The link, which answers for its own airtime and off time.
87def messages_per_hour( 88 settings: LoraLink, payload_len: int, duty_cycle_permille: int 89) -> int: 90 """Return how many transmissions of a payload fit in an hour under a limit. 91 92 The airtime plus the silence it forces is what one transmission really costs, 93 so this is the message budget a deployment plans against. 94 95 :param settings: The link settings. 96 :param payload_len: The payload length in bytes. 97 :param duty_cycle_permille: The limit in parts per thousand, so ``10`` is 1%. 98 :returns: The number of whole transmissions per hour, or ``0`` when the limit 99 forbids transmitting. 100 """ 101 off_time = settings.min_off_time_us(payload_len, duty_cycle_permille) 102 if off_time is None: 103 return 0 104 return 3_600_000_000 // (settings.airtime_us(payload_len) + off_time)
Return how many transmissions of a payload fit in an hour under a limit.
The airtime plus the silence it forces is what one transmission really costs, so this is the message budget a deployment plans against.
Parameters
- settings: The link settings.
- payload_len: The payload length in bytes.
- duty_cycle_permille: The limit in parts per thousand, so
10is 1%. :returns: The number of whole transmissions per hour, or0when the limit forbids transmitting.
132def messages_per_hour_at( 133 plan: ChannelPlan, data_rate: int, payload_len: int, frequency_hz: int 134) -> int | None: 135 """Return how many transmissions fit in an hour at a data rate the plan defines. 136 137 This is the budget question a deployment actually asks: not what the radio 138 can do, but how often it may speak on this band at this setting. The duty 139 cycle of the frequency it transmits on decides the answer. 140 141 :param plan: The channel plan to read. 142 :param data_rate: The uplink data-rate number. 143 :param payload_len: The payload length in bytes. 144 :param frequency_hz: The frequency the node transmits on. 145 :returns: The number of whole transmissions per hour, or ``None`` when the 146 plan does not describe that data rate or frequency. 147 148 >>> plan = plan_for("EU868") 149 >>> messages_per_hour_at(plan, 5, 20, 868_100_000) > 0 150 True 151 """ 152 settings = plan.link_settings(data_rate) 153 permille = plan.duty_cycle_permille(frequency_hz) 154 if settings is None or permille is None: 155 return None 156 return messages_per_hour(settings, payload_len, permille)
Return how many transmissions fit in an hour at a data rate the plan defines.
This is the budget question a deployment actually asks: not what the radio can do, but how often it may speak on this band at this setting. The duty cycle of the frequency it transmits on decides the answer.
Parameters
- plan: The channel plan to read.
- data_rate: The uplink data-rate number.
- payload_len: The payload length in bytes.
- frequency_hz: The frequency the node transmits on.
:returns: The number of whole transmissions per hour, or
Nonewhen the plan does not describe that data rate or frequency.
>>> plan = plan_for("EU868")
>>> messages_per_hour_at(plan, 5, 20, 868_100_000) > 0
True
107def plan_for(region: str) -> ChannelPlan: 108 """Return the published channel plan for a region. 109 110 A channel plan is what a regulator and the LoRa Alliance publish about one 111 band: which data rates exist, what each carries, how much of the time a node 112 may hold a frequency, and where it listens for a downlink. The plan reports 113 those facts and costs a transmission out against them; it never refuses one, 114 because a deployment may hold licensed spectrum or be working under emergency 115 provisions and only the operator knows which. 116 117 :param region: The band to describe, such as ``EU868``. See :data:`REGIONS`. 118 :returns: The plan, which answers every question about that band. 119 :raises ValueError: If no published region goes by that name. 120 121 >>> plan = plan_for("EU868") 122 >>> plan.name 123 'EU863-870' 124 >>> plan.link_settings(0).spreading_factor 125 12 126 >>> plan.duty_cycle_permille(868_100_000) 127 10 128 """ 129 return ChannelPlan.for_region(region)
Return the published channel plan for a region.
A channel plan is what a regulator and the LoRa Alliance publish about one band: which data rates exist, what each carries, how much of the time a node may hold a frequency, and where it listens for a downlink. The plan reports those facts and costs a transmission out against them; it never refuses one, because a deployment may hold licensed spectrum or be working under emergency provisions and only the operator knows which.
Parameters
- region: The band to describe, such as
EU868. SeeREGIONS. :returns: The plan, which answers every question about that band.
Raises
- ValueError: If no published region goes by that name.
>>> plan = plan_for("EU868")
>>> plan.name
'EU863-870'
>>> plan.link_settings(0).spreading_factor
12
>>> plan.duty_cycle_permille(868_100_000)
10