pamoja-radios 0.2.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,13 @@
1
+ # Native extensions are built per platform, not committed.
2
+ *.so
3
+ *.pyd
4
+ *.dylib
5
+
6
+ # Build and packaging output.
7
+ dist/
8
+ wheels/
9
+ target/
10
+
11
+ # Local virtual environments.
12
+ .venv/
13
+ venv/
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Anthony Wiedman
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,116 @@
1
+ Metadata-Version: 2.5
2
+ Name: pamoja-radios
3
+ Version: 0.2.0
4
+ Summary: The Semtech SX126x and SX127x LoRa radios and the SX1302 and SX1303 gateway concentrators: their commands, registers, and decoders, the amplifier setting a regional EIRP ceiling allows, a duty-cycle guard, and simulated chips that stand in for a module.
5
+ Project-URL: Repository, https://github.com/molexxxx/pamoja
6
+ Project-URL: Documentation, https://pamoja.molex.cloud/docs/guides/radios.html
7
+ Author: molexxxx
8
+ License: MIT
9
+ License-File: LICENSE-MIT
10
+ Keywords: iot,pamoja,radios,robotics
11
+ Classifier: License :: OSI Approved :: MIT License
12
+ Classifier: Operating System :: OS Independent
13
+ Classifier: Programming Language :: Python :: 3
14
+ Classifier: Typing :: Typed
15
+ Requires-Python: >=3.10
16
+ Requires-Dist: pamoja-native==0.2.0
17
+ Description-Content-Type: text/markdown
18
+
19
+ # pamoja-radios
20
+
21
+ The Semtech SX126x and SX127x LoRa radios and the SX1302 and SX1303 gateway concentrators: their commands, registers, and decoders, the amplifier setting a regional EIRP ceiling allows, a duty-cycle guard, and simulated chips that stand in for a module. One capability of [pamoja](https://github.com/molexxxx/pamoja), one memory-safe Rust core with bindings for TypeScript, Python, and C#.
22
+
23
+ [![read the guide](https://raw.githubusercontent.com/molexxxx/pamoja/main/.github/badges/btn-guide.svg)](https://pamoja.molex.cloud/docs/guides/radios.html)
24
+ [![documentation](https://raw.githubusercontent.com/molexxxx/pamoja/main/.github/badges/btn-docs.svg)](https://pamoja.molex.cloud/docs/)
25
+ [![API reference](https://raw.githubusercontent.com/molexxxx/pamoja/main/.github/badges/btn-api.svg)](https://pamoja.molex.cloud/docs/reference/python/pamoja/radios.html)
26
+
27
+ ## Install
28
+
29
+ ```sh
30
+ pip install pamoja-radios
31
+ ```
32
+
33
+ ```python
34
+ from pamoja import radios
35
+ ```
36
+
37
+ This pulls in `pamoja-native`, the compiled engine. `pip install pamoja` is the whole framework in one package.
38
+
39
+ ## Example
40
+
41
+ The script the test suite runs, spliced here as it ran.
42
+
43
+ From [`bindings/python/guides/radios.py`](https://github.com/molexxxx/pamoja/blob/main/bindings/python/guides/radios.py):
44
+
45
+ ```python
46
+ from pamoja.lora import LinkBudget, plan_for
47
+ from pamoja.radios import DutyCycle, SimulatedLoraChip, sx126x
48
+
49
+ # An SX1262 node sends a ten-byte reading on 868.1 MHz at DR3, SF9 at 125 kHz, through a
50
+ # 2.15 dBi whip on half a decibel of pigtail. The plan caps the EIRP there, and the antenna
51
+ # and pigtail decide how hard the amplifier may drive under that cap.
52
+ eu868 = plan_for("EU868")
53
+ frequency = 868_100_000
54
+ link = eu868.link_settings(3)
55
+ whip = LinkBudget(transmit_antenna_gain_dbi=2.15, transmit_cable_loss_db=0.5)
56
+ ceiling = eu868.max_eirp_dbm(frequency)
57
+ power = sx126x.tx_power_under_ceiling(sx126x.Amplifier.HIGH_POWER, whip, ceiling)
58
+ print(f"power {power.setting_dbm} dBm under a {ceiling} dBm EIRP ceiling")
59
+
60
+ # A simulated SX1262 stands in for the chip on the node's board, driven by the same code that
61
+ # drives a real one, and it reports what that code told it.
62
+ chip = SimulatedLoraChip.sx126x(sx126x.Amplifier.HIGH_POWER)
63
+ bench = chip.radio()
64
+ bench.configure(frequency, link, power.setting_dbm)
65
+ tuned = chip.tuning()
66
+ print(
67
+ f"tuned {tuned.frequency_hz / 1e6:.1f} MHz, SF{tuned.link.spreading_factor} "
68
+ f"at {tuned.link.bandwidth_hz // 1000} kHz, {tuned.output_dbm} dBm"
69
+ )
70
+
71
+ # The reading goes out, and the airtime comes back for the duty-cycle guard. The sub-band that
72
+ # holds 868.1 MHz allows 1% of the time, so the frame buys ninety-nine times as long in silence
73
+ # before the next.
74
+ reading = b"level=0.42"
75
+ airtime = bench.transmit(reading)
76
+ print(f"sent {len(chip.sent()[0].payload)} bytes, {airtime} us on air")
77
+ guard = DutyCycle(eu868.duty_cycle_permille(frequency))
78
+ guard.transmitted(0, link, len(reading))
79
+ print(f"silence the next frame starts {guard.wait_us(0)} us after this one did")
80
+
81
+ # A gateway's answer arrives from the edge of range, 2.5 dB under the noise.
82
+ chip.hear(b"ack", -109, -2.5)
83
+ heard = bench.receive(1_000_000)
84
+ if heard.outcome == "Frame":
85
+ print(
86
+ f"received {heard.payload.decode()} at {heard.rssi_dbm:.2f} dBm, "
87
+ f"SNR {heard.snr_db:.2f} dB"
88
+ )
89
+
90
+ # With nothing on the air the reception times out, and a frame whose CRC fails is dropped
91
+ # rather than handed over.
92
+ quiet = bench.receive(1_000_000).outcome
93
+ chip.hear_corrupt(-121, -12)
94
+ broken = bench.receive(1_000_000).outcome
95
+ print(f"then {quiet}, then {broken}")
96
+ bench.close()
97
+ ```
98
+
99
+ ## The same capability in every language
100
+
101
+ | Language | Package | Reference |
102
+ | --- | --- | --- |
103
+ | Rust | [`pamoja-radios`](https://crates.io/crates/pamoja-radios) | [reference](https://pamoja.molex.cloud/docs/reference/rust/pamoja_radios/index.html), [docs.rs](https://docs.rs/pamoja-radios), [install](https://pamoja.molex.cloud/docs/reference/rust.html#rust-radios) |
104
+ | TypeScript | [`@pamoja/radios`](https://www.npmjs.com/package/@pamoja/radios) | [reference](https://pamoja.molex.cloud/docs/reference/node/modules/_pamoja_radios.html), [install](https://pamoja.molex.cloud/docs/reference/node.html#node-radios) |
105
+ | Python | [`pamoja-radios`](https://pypi.org/project/pamoja-radios/) | [reference](https://pamoja.molex.cloud/docs/reference/python/pamoja/radios.html), [install](https://pamoja.molex.cloud/docs/reference/python.html#python-radios) |
106
+ | C# | [`Pamoja.Radios`](https://www.nuget.org/packages/Pamoja.Radios) | [reference](https://pamoja.molex.cloud/docs/reference/dotnet/api/Pamoja.Radios.html), [install](https://pamoja.molex.cloud/docs/reference/dotnet.html#dotnet-radios) |
107
+
108
+ ## Documentation
109
+
110
+ - [`pamoja.radios` reference](https://pamoja.molex.cloud/docs/reference/python/pamoja/radios.html), every class and function in this module.
111
+ - [The LoRa radios guide](https://pamoja.molex.cloud/docs/guides/radios.html), with the same example in Rust, TypeScript, and C#.
112
+ - [Every capability](https://pamoja.molex.cloud/docs/), and the [install page](https://pamoja.molex.cloud/docs/install.html).
113
+
114
+ ## License
115
+
116
+ MIT
@@ -0,0 +1,98 @@
1
+ # pamoja-radios
2
+
3
+ The Semtech SX126x and SX127x LoRa radios and the SX1302 and SX1303 gateway concentrators: their commands, registers, and decoders, the amplifier setting a regional EIRP ceiling allows, a duty-cycle guard, and simulated chips that stand in for a module. One capability of [pamoja](https://github.com/molexxxx/pamoja), one memory-safe Rust core with bindings for TypeScript, Python, and C#.
4
+
5
+ [![read the guide](https://raw.githubusercontent.com/molexxxx/pamoja/main/.github/badges/btn-guide.svg)](https://pamoja.molex.cloud/docs/guides/radios.html)
6
+ [![documentation](https://raw.githubusercontent.com/molexxxx/pamoja/main/.github/badges/btn-docs.svg)](https://pamoja.molex.cloud/docs/)
7
+ [![API reference](https://raw.githubusercontent.com/molexxxx/pamoja/main/.github/badges/btn-api.svg)](https://pamoja.molex.cloud/docs/reference/python/pamoja/radios.html)
8
+
9
+ ## Install
10
+
11
+ ```sh
12
+ pip install pamoja-radios
13
+ ```
14
+
15
+ ```python
16
+ from pamoja import radios
17
+ ```
18
+
19
+ This pulls in `pamoja-native`, the compiled engine. `pip install pamoja` is the whole framework in one package.
20
+
21
+ ## Example
22
+
23
+ The script the test suite runs, spliced here as it ran.
24
+
25
+ From [`bindings/python/guides/radios.py`](https://github.com/molexxxx/pamoja/blob/main/bindings/python/guides/radios.py):
26
+
27
+ ```python
28
+ from pamoja.lora import LinkBudget, plan_for
29
+ from pamoja.radios import DutyCycle, SimulatedLoraChip, sx126x
30
+
31
+ # An SX1262 node sends a ten-byte reading on 868.1 MHz at DR3, SF9 at 125 kHz, through a
32
+ # 2.15 dBi whip on half a decibel of pigtail. The plan caps the EIRP there, and the antenna
33
+ # and pigtail decide how hard the amplifier may drive under that cap.
34
+ eu868 = plan_for("EU868")
35
+ frequency = 868_100_000
36
+ link = eu868.link_settings(3)
37
+ whip = LinkBudget(transmit_antenna_gain_dbi=2.15, transmit_cable_loss_db=0.5)
38
+ ceiling = eu868.max_eirp_dbm(frequency)
39
+ power = sx126x.tx_power_under_ceiling(sx126x.Amplifier.HIGH_POWER, whip, ceiling)
40
+ print(f"power {power.setting_dbm} dBm under a {ceiling} dBm EIRP ceiling")
41
+
42
+ # A simulated SX1262 stands in for the chip on the node's board, driven by the same code that
43
+ # drives a real one, and it reports what that code told it.
44
+ chip = SimulatedLoraChip.sx126x(sx126x.Amplifier.HIGH_POWER)
45
+ bench = chip.radio()
46
+ bench.configure(frequency, link, power.setting_dbm)
47
+ tuned = chip.tuning()
48
+ print(
49
+ f"tuned {tuned.frequency_hz / 1e6:.1f} MHz, SF{tuned.link.spreading_factor} "
50
+ f"at {tuned.link.bandwidth_hz // 1000} kHz, {tuned.output_dbm} dBm"
51
+ )
52
+
53
+ # The reading goes out, and the airtime comes back for the duty-cycle guard. The sub-band that
54
+ # holds 868.1 MHz allows 1% of the time, so the frame buys ninety-nine times as long in silence
55
+ # before the next.
56
+ reading = b"level=0.42"
57
+ airtime = bench.transmit(reading)
58
+ print(f"sent {len(chip.sent()[0].payload)} bytes, {airtime} us on air")
59
+ guard = DutyCycle(eu868.duty_cycle_permille(frequency))
60
+ guard.transmitted(0, link, len(reading))
61
+ print(f"silence the next frame starts {guard.wait_us(0)} us after this one did")
62
+
63
+ # A gateway's answer arrives from the edge of range, 2.5 dB under the noise.
64
+ chip.hear(b"ack", -109, -2.5)
65
+ heard = bench.receive(1_000_000)
66
+ if heard.outcome == "Frame":
67
+ print(
68
+ f"received {heard.payload.decode()} at {heard.rssi_dbm:.2f} dBm, "
69
+ f"SNR {heard.snr_db:.2f} dB"
70
+ )
71
+
72
+ # With nothing on the air the reception times out, and a frame whose CRC fails is dropped
73
+ # rather than handed over.
74
+ quiet = bench.receive(1_000_000).outcome
75
+ chip.hear_corrupt(-121, -12)
76
+ broken = bench.receive(1_000_000).outcome
77
+ print(f"then {quiet}, then {broken}")
78
+ bench.close()
79
+ ```
80
+
81
+ ## The same capability in every language
82
+
83
+ | Language | Package | Reference |
84
+ | --- | --- | --- |
85
+ | Rust | [`pamoja-radios`](https://crates.io/crates/pamoja-radios) | [reference](https://pamoja.molex.cloud/docs/reference/rust/pamoja_radios/index.html), [docs.rs](https://docs.rs/pamoja-radios), [install](https://pamoja.molex.cloud/docs/reference/rust.html#rust-radios) |
86
+ | TypeScript | [`@pamoja/radios`](https://www.npmjs.com/package/@pamoja/radios) | [reference](https://pamoja.molex.cloud/docs/reference/node/modules/_pamoja_radios.html), [install](https://pamoja.molex.cloud/docs/reference/node.html#node-radios) |
87
+ | Python | [`pamoja-radios`](https://pypi.org/project/pamoja-radios/) | [reference](https://pamoja.molex.cloud/docs/reference/python/pamoja/radios.html), [install](https://pamoja.molex.cloud/docs/reference/python.html#python-radios) |
88
+ | C# | [`Pamoja.Radios`](https://www.nuget.org/packages/Pamoja.Radios) | [reference](https://pamoja.molex.cloud/docs/reference/dotnet/api/Pamoja.Radios.html), [install](https://pamoja.molex.cloud/docs/reference/dotnet.html#dotnet-radios) |
89
+
90
+ ## Documentation
91
+
92
+ - [`pamoja.radios` reference](https://pamoja.molex.cloud/docs/reference/python/pamoja/radios.html), every class and function in this module.
93
+ - [The LoRa radios guide](https://pamoja.molex.cloud/docs/guides/radios.html), with the same example in Rust, TypeScript, and C#.
94
+ - [Every capability](https://pamoja.molex.cloud/docs/), and the [install page](https://pamoja.molex.cloud/docs/install.html).
95
+
96
+ ## License
97
+
98
+ MIT
@@ -0,0 +1,38 @@
1
+ """Idiomatic LoRa radio facade.
2
+
3
+ ``pamoja.lora`` works out what a link costs and how far it reaches; this puts a radio chip
4
+ on the air to match. For the Semtech SX1261, SX1262, and LLCC68, :mod:`pamoja.radios.sx126x`
5
+ builds the bytes of every command and decodes every answer, and chooses the amplifier
6
+ setting a regional EIRP ceiling allows behind an antenna. For the SX1276 family,
7
+ :mod:`pamoja.radios.sx127x` gives the register values and decodes the readings.
8
+ :class:`DutyCycle` holds the radio
9
+ silent for the off time a duty-cycle limit owes after each frame. On a Linux board,
10
+ :class:`LoraRadio` opens a module of either family through the kernel's spidev and GPIO
11
+ character devices and drives it with the same calls. On any platform,
12
+ :class:`SimulatedLoraChip` stands in for a module: the radio it gives is the same class,
13
+ and the chip says what it was tuned to and what it sent.
14
+ """
15
+
16
+ from __future__ import annotations
17
+
18
+ from pamoja._native import (
19
+ LoraRadio,
20
+ LoraReception,
21
+ LoraSentFrame,
22
+ LoraTuning,
23
+ SimulatedLoraChip,
24
+ )
25
+ from pamoja._native import RadioDutyCycle as DutyCycle
26
+
27
+ from . import sx126x, sx127x
28
+
29
+ __all__ = [
30
+ "DutyCycle",
31
+ "LoraRadio",
32
+ "LoraReception",
33
+ "LoraSentFrame",
34
+ "LoraTuning",
35
+ "SimulatedLoraChip",
36
+ "sx126x",
37
+ "sx127x",
38
+ ]
File without changes
@@ -0,0 +1,523 @@
1
+ """The Semtech SX1261, SX1262, and LLCC68, as the bytes they take and the answers they give.
2
+
3
+ Every command goes out in one SPI transaction framed by NSS, sent once the chip's BUSY line
4
+ is low, and a query's answer is read in the same transaction after its bytes. These
5
+ functions build the bytes and decode the answers from the SX1261/2 datasheet (Rev 2.2), so a
6
+ program with its own SPI access can drive the chip with nothing else.
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ from enum import Enum, IntFlag
12
+
13
+ from pamoja._native import (
14
+ LinkBudget,
15
+ LoraLink,
16
+ Sx126xPacketStatus as PacketStatus,
17
+ Sx126xQuery as Query,
18
+ Sx126xRxBufferStatus as RxBufferStatus,
19
+ Sx126xStatus as Status,
20
+ Sx126xTxPower as TxPower,
21
+ )
22
+ from pamoja._native import sx126x_calibrate_image as _calibrate_image
23
+ from pamoja._native import sx126x_clear_irq_status as _clear_irq_status
24
+ from pamoja._native import sx126x_constants as _constants
25
+ from pamoja._native import sx126x_device_errors as _device_errors
26
+ from pamoja._native import sx126x_frequency_word as _frequency_word
27
+ from pamoja._native import sx126x_get_device_errors as _get_device_errors
28
+ from pamoja._native import sx126x_get_irq_status as _get_irq_status
29
+ from pamoja._native import sx126x_get_packet_status as _get_packet_status
30
+ from pamoja._native import sx126x_get_rssi_inst as _get_rssi_inst
31
+ from pamoja._native import sx126x_get_rx_buffer_status as _get_rx_buffer_status
32
+ from pamoja._native import sx126x_get_status as _get_status
33
+ from pamoja._native import sx126x_image_calibration as _image_calibration
34
+ from pamoja._native import sx126x_irq as _irq
35
+ from pamoja._native import sx126x_llcc68_supports as _llcc68_supports
36
+ from pamoja._native import sx126x_packet_status as _packet_status
37
+ from pamoja._native import sx126x_ramp_time_us as _ramp_time_us
38
+ from pamoja._native import sx126x_read_buffer as _read_buffer
39
+ from pamoja._native import sx126x_read_register as _read_register
40
+ from pamoja._native import sx126x_rssi_inst_dbm as _rssi_inst_dbm
41
+ from pamoja._native import sx126x_rx_buffer_status as _rx_buffer_status
42
+ from pamoja._native import sx126x_set_dio_irq_params as _set_dio_irq_params
43
+ from pamoja._native import sx126x_set_lora_modulation_params as _set_lora_modulation_params
44
+ from pamoja._native import sx126x_set_lora_packet_params as _set_lora_packet_params
45
+ from pamoja._native import sx126x_set_pa_config as _set_pa_config
46
+ from pamoja._native import sx126x_set_packet_type_lora as _set_packet_type_lora
47
+ from pamoja._native import sx126x_set_rf_frequency as _set_rf_frequency
48
+ from pamoja._native import sx126x_set_rx as _set_rx
49
+ from pamoja._native import sx126x_set_rx_continuous as _set_rx_continuous
50
+ from pamoja._native import sx126x_set_sleep as _set_sleep
51
+ from pamoja._native import sx126x_set_standby as _set_standby
52
+ from pamoja._native import sx126x_set_tx as _set_tx
53
+ from pamoja._native import sx126x_set_tx_params as _set_tx_params
54
+ from pamoja._native import sx126x_status as _status
55
+ from pamoja._native import sx126x_timeout_steps as _timeout_steps
56
+ from pamoja._native import sx126x_tx_power as _tx_power
57
+ from pamoja._native import sx126x_tx_power_under_ceiling as _tx_power_under_ceiling
58
+ from pamoja._native import sx126x_write_buffer as _write_buffer
59
+ from pamoja._native import sx126x_write_register as _write_register
60
+
61
+ __all__ = [
62
+ "REGISTER_LORA_SYNC_WORD",
63
+ "RX_CONTINUOUS",
64
+ "SYNC_WORD_PRIVATE",
65
+ "SYNC_WORD_PUBLIC",
66
+ "Amplifier",
67
+ "DeviceError",
68
+ "Irq",
69
+ "PacketStatus",
70
+ "Query",
71
+ "RxBufferStatus",
72
+ "Status",
73
+ "TxPower",
74
+ "calibrate_image",
75
+ "clear_irq_status",
76
+ "device_errors",
77
+ "frequency_word",
78
+ "get_device_errors",
79
+ "get_irq_status",
80
+ "get_packet_status",
81
+ "get_rssi_inst",
82
+ "get_rx_buffer_status",
83
+ "get_status",
84
+ "image_calibration",
85
+ "irq",
86
+ "llcc68_supports",
87
+ "packet_status",
88
+ "ramp_time_us",
89
+ "read_buffer",
90
+ "read_register",
91
+ "rssi_inst_dbm",
92
+ "rx_buffer_status",
93
+ "set_dio_irq_params",
94
+ "set_lora_modulation_params",
95
+ "set_lora_packet_params",
96
+ "set_pa_config",
97
+ "set_packet_type_lora",
98
+ "set_rf_frequency",
99
+ "set_rx",
100
+ "set_rx_continuous",
101
+ "set_sleep",
102
+ "set_standby",
103
+ "set_tx",
104
+ "set_tx_params",
105
+ "status",
106
+ "timeout_steps",
107
+ "tx_power",
108
+ "tx_power_under_ceiling",
109
+ "write_buffer",
110
+ "write_register",
111
+ ]
112
+
113
+ _CONSTANTS = _constants()
114
+
115
+ #: The receive timeout word that keeps the chip listening until another command stops it.
116
+ RX_CONTINUOUS = _CONSTANTS["RX_CONTINUOUS"]
117
+ #: The LoRa sync word of a public network such as LoRaWAN.
118
+ SYNC_WORD_PUBLIC = _CONSTANTS["SYNC_WORD_PUBLIC"]
119
+ #: The LoRa sync word of a private network, and the chip's reset value.
120
+ SYNC_WORD_PRIVATE = _CONSTANTS["SYNC_WORD_PRIVATE"]
121
+ #: The register that holds the most significant byte of the LoRa sync word.
122
+ REGISTER_LORA_SYNC_WORD = _CONSTANTS["REGISTER_LORA_SYNC_WORD"]
123
+
124
+
125
+ class Amplifier(str, Enum):
126
+ """Which power amplifier a chip has.
127
+
128
+ The SPI interface cannot tell the chips apart, so the caller names the amplifier.
129
+ """
130
+
131
+ #: The low power amplifier of the SX1261, up to +15 dBm.
132
+ LOW_POWER = "LowPower"
133
+ #: The high power amplifier of the SX1262 and the LLCC68, up to +22 dBm.
134
+ HIGH_POWER = "HighPower"
135
+
136
+
137
+ class Irq(IntFlag):
138
+ """The interrupt bits of the IRQ register, from Table 13-29 of the datasheet."""
139
+
140
+ #: A packet has been sent.
141
+ TX_DONE = _CONSTANTS["IRQ_TX_DONE"]
142
+ #: A packet has been received.
143
+ RX_DONE = _CONSTANTS["IRQ_RX_DONE"]
144
+ #: A preamble has been detected.
145
+ PREAMBLE_DETECTED = _CONSTANTS["IRQ_PREAMBLE_DETECTED"]
146
+ #: A valid (G)FSK sync word has been detected.
147
+ SYNC_WORD_VALID = _CONSTANTS["IRQ_SYNC_WORD_VALID"]
148
+ #: A valid LoRa header has been received.
149
+ HEADER_VALID = _CONSTANTS["IRQ_HEADER_VALID"]
150
+ #: A LoRa header failed its CRC.
151
+ HEADER_ERROR = _CONSTANTS["IRQ_HEADER_ERROR"]
152
+ #: A packet failed its CRC.
153
+ CRC_ERROR = _CONSTANTS["IRQ_CRC_ERROR"]
154
+ #: Channel activity detection has finished.
155
+ CAD_DONE = _CONSTANTS["IRQ_CAD_DONE"]
156
+ #: Channel activity detection heard LoRa.
157
+ CAD_DETECTED = _CONSTANTS["IRQ_CAD_DETECTED"]
158
+ #: A transmission or a reception timed out.
159
+ TIMEOUT = _CONSTANTS["IRQ_TIMEOUT"]
160
+ #: A long-range FHSS hop is due.
161
+ LR_FHSS_HOP = _CONSTANTS["IRQ_LR_FHSS_HOP"]
162
+
163
+
164
+ class DeviceError(IntFlag):
165
+ """The device error bits GetDeviceErrors answers with."""
166
+
167
+ #: The RC64k oscillator failed to calibrate.
168
+ RC64K_CALIBRATION = _CONSTANTS["ERROR_RC64K_CALIBRATION"]
169
+ #: The RC13M oscillator failed to calibrate.
170
+ RC13M_CALIBRATION = _CONSTANTS["ERROR_RC13M_CALIBRATION"]
171
+ #: The PLL failed to calibrate.
172
+ PLL_CALIBRATION = _CONSTANTS["ERROR_PLL_CALIBRATION"]
173
+ #: The ADC failed to calibrate.
174
+ ADC_CALIBRATION = _CONSTANTS["ERROR_ADC_CALIBRATION"]
175
+ #: Image rejection failed to calibrate.
176
+ IMAGE_CALIBRATION = _CONSTANTS["ERROR_IMAGE_CALIBRATION"]
177
+ #: The crystal oscillator failed to start, which a TCXO raises until it is powered.
178
+ XOSC_START = _CONSTANTS["ERROR_XOSC_START"]
179
+ #: The PLL failed to lock.
180
+ PLL_LOCK = _CONSTANTS["ERROR_PLL_LOCK"]
181
+ #: The power amplifier failed to ramp.
182
+ PA_RAMP = _CONSTANTS["ERROR_PA_RAMP"]
183
+
184
+
185
+ def frequency_word(frequency_hz: int) -> int:
186
+ """Return the word SetRfFrequency takes for a frequency.
187
+
188
+ :param frequency_hz: The carrier frequency in hertz.
189
+ :returns: The frequency times 2^25 over the 32 MHz crystal, rounded to the nearest step.
190
+
191
+ >>> hex(frequency_word(868_100_000))
192
+ '0x3641999a'
193
+ """
194
+ return _frequency_word(frequency_hz)
195
+
196
+
197
+ def timeout_steps(timeout_us: int) -> int:
198
+ """Return the 24-bit timeout word SetTx and SetRx take for a duration.
199
+
200
+ :param timeout_us: The duration in microseconds.
201
+ :returns: The number of 15.625 us steps; a nonzero duration never becomes the zero word
202
+ that disables the timeout.
203
+
204
+ >>> timeout_steps(1_000_000)
205
+ 64000
206
+ """
207
+ return _timeout_steps(timeout_us)
208
+
209
+
210
+ def image_calibration(low_hz: int, high_hz: int) -> bytes:
211
+ """Return the two CalibrateImage codes that cover a band.
212
+
213
+ :param low_hz: The lower edge of the band in hertz.
214
+ :param high_hz: The upper edge of the band in hertz.
215
+ :returns: ``freq1`` and ``freq2``, 4 MHz steps that always cover the band.
216
+
217
+ >>> image_calibration(863_000_000, 870_000_000).hex()
218
+ 'd7da'
219
+ """
220
+ return _image_calibration(low_hz, high_hz)
221
+
222
+
223
+ def ramp_time_us(at_least_us: int) -> int:
224
+ """Return the shortest amplifier ramp time the chip offers that lasts at least a duration.
225
+
226
+ :param at_least_us: The least ramp time wanted, in microseconds.
227
+ :returns: One of the eight ramp times of Table 13-41, in microseconds.
228
+ """
229
+ return _ramp_time_us(at_least_us)
230
+
231
+
232
+ def tx_power(amplifier: Amplifier | str, output_dbm: int) -> TxPower:
233
+ """Choose the amplifier settings for an output power.
234
+
235
+ :param amplifier: The chip's amplifier.
236
+ :param output_dbm: The output power wanted at the antenna port, in dBm.
237
+ :returns: The configuration and the setting, clamped to what the amplifier allows.
238
+ :raises ValueError: If no amplifier goes by that name.
239
+ """
240
+ return _tx_power(Amplifier(amplifier).value, output_dbm)
241
+
242
+
243
+ def tx_power_under_ceiling(
244
+ amplifier: Amplifier | str, budget: LinkBudget, eirp_ceiling_dbm: float
245
+ ) -> TxPower:
246
+ """Choose the amplifier settings that keep a link's EIRP at or under a ceiling.
247
+
248
+ :param amplifier: The chip's amplifier.
249
+ :param budget: The link budget, whose transmitting antenna and cable apply.
250
+ :param eirp_ceiling_dbm: The EIRP limit, such as a channel plan's ceiling for the
251
+ frequency.
252
+ :returns: The configuration and the setting, rounded down to whole decibels so the EIRP
253
+ stays under the ceiling.
254
+ :raises ValueError: If no amplifier goes by that name.
255
+ """
256
+ return _tx_power_under_ceiling(Amplifier(amplifier).value, budget, eirp_ceiling_dbm)
257
+
258
+
259
+ def set_standby() -> bytes:
260
+ """Return SetStandby into STDBY_RC, which stops a transmission or a reception."""
261
+ return _set_standby()
262
+
263
+
264
+ def set_packet_type_lora() -> bytes:
265
+ """Return SetPacketType for LoRa, the first radio setting a configuration sends."""
266
+ return _set_packet_type_lora()
267
+
268
+
269
+ def set_rf_frequency(frequency_hz: int) -> bytes:
270
+ """Return SetRfFrequency for a carrier frequency.
271
+
272
+ :param frequency_hz: The carrier frequency in hertz.
273
+ :returns: The command bytes.
274
+ """
275
+ return _set_rf_frequency(frequency_hz)
276
+
277
+
278
+ def calibrate_image(low_hz: int, high_hz: int) -> bytes:
279
+ """Return CalibrateImage over a band.
280
+
281
+ :param low_hz: The lower edge of the band in hertz.
282
+ :param high_hz: The upper edge of the band in hertz.
283
+ :returns: The command bytes.
284
+ """
285
+ return _calibrate_image(low_hz, high_hz)
286
+
287
+
288
+ def set_pa_config(power: TxPower) -> bytes:
289
+ """Return SetPaConfig for a power setting.
290
+
291
+ :param power: The settings from :func:`tx_power` or :func:`tx_power_under_ceiling`.
292
+ :returns: The command bytes.
293
+ """
294
+ return _set_pa_config(power)
295
+
296
+
297
+ def set_tx_params(power: TxPower, ramp_us: int) -> bytes:
298
+ """Return SetTxParams for a power setting and a ramp time.
299
+
300
+ :param power: The power settings.
301
+ :param ramp_us: The least amplifier ramp time wanted, in microseconds.
302
+ :returns: The command bytes.
303
+ """
304
+ return _set_tx_params(power, ramp_us)
305
+
306
+
307
+ def set_lora_modulation_params(link: LoraLink) -> bytes:
308
+ """Return SetModulationParams for a LoRa link.
309
+
310
+ :param link: The link settings, from :mod:`pamoja.lora`.
311
+ :returns: The command bytes.
312
+ :raises PamojaError: If the link's bandwidth is not one the SX126x offers.
313
+ """
314
+ return _set_lora_modulation_params(link)
315
+
316
+
317
+ def set_lora_packet_params(link: LoraLink, payload_len: int, invert_iq: bool) -> bytes:
318
+ """Return SetPacketParams for a LoRa link and a payload.
319
+
320
+ :param link: The link settings, whose preamble, header, and CRC the frame uses.
321
+ :param payload_len: The payload length to send, or the most a receiver accepts.
322
+ :param invert_iq: Whether the IQ polarity is inverted, as LoRaWAN downlinks use.
323
+ :returns: The command bytes.
324
+ """
325
+ return _set_lora_packet_params(link, payload_len, invert_iq)
326
+
327
+
328
+ def set_dio_irq_params(irq: int, dio1: int, dio2: int = 0, dio3: int = 0) -> bytes:
329
+ """Return SetDioIrqParams: which interrupts are enabled, and which DIO lines raise them.
330
+
331
+ :param irq: The interrupts to enable, as :class:`Irq` bits.
332
+ :param dio1: The interrupts routed to DIO1.
333
+ :param dio2: The interrupts routed to DIO2.
334
+ :param dio3: The interrupts routed to DIO3.
335
+ :returns: The command bytes.
336
+ """
337
+ return _set_dio_irq_params(int(irq), int(dio1), int(dio2), int(dio3))
338
+
339
+
340
+ def clear_irq_status(irq: int) -> bytes:
341
+ """Return ClearIrqStatus for a set of interrupts.
342
+
343
+ :param irq: The interrupts to clear, as :class:`Irq` bits.
344
+ :returns: The command bytes.
345
+ """
346
+ return _clear_irq_status(int(irq))
347
+
348
+
349
+ def set_tx(timeout_us: int) -> bytes:
350
+ """Return SetTx with a timeout.
351
+
352
+ :param timeout_us: How long the chip may transmit before it raises TIMEOUT, in
353
+ microseconds; ``0`` disables the timeout.
354
+ :returns: The command bytes.
355
+ """
356
+ return _set_tx(timeout_us)
357
+
358
+
359
+ def set_rx(timeout_us: int) -> bytes:
360
+ """Return SetRx with a timeout.
361
+
362
+ :param timeout_us: How long the chip listens for a packet to start, in microseconds;
363
+ ``0`` listens for one packet with no timeout.
364
+ :returns: The command bytes.
365
+ """
366
+ return _set_rx(timeout_us)
367
+
368
+
369
+ def set_rx_continuous() -> bytes:
370
+ """Return SetRx in continuous mode, receiving packet after packet until another command."""
371
+ return _set_rx_continuous()
372
+
373
+
374
+ def set_sleep(warm_start: bool) -> bytes:
375
+ """Return SetSleep, without an RTC wake-up.
376
+
377
+ :param warm_start: Whether to keep the configuration in retention while asleep.
378
+ :returns: The command bytes.
379
+ """
380
+ return _set_sleep(warm_start)
381
+
382
+
383
+ def write_register(address: int, values: bytes) -> bytes:
384
+ """Return a whole WriteRegister transaction.
385
+
386
+ :param address: The first register's address, such as :data:`REGISTER_LORA_SYNC_WORD`.
387
+ :param values: The register values, one byte each.
388
+ :returns: The opcode, the address, and the values.
389
+ """
390
+ return _write_register(address, bytes(values))
391
+
392
+
393
+ def write_buffer(offset: int, payload: bytes) -> bytes:
394
+ """Return a whole WriteBuffer transaction.
395
+
396
+ :param offset: Where in the data buffer the first byte goes.
397
+ :param payload: The bytes to write.
398
+ :returns: The opcode, the offset, and the payload.
399
+ """
400
+ return _write_buffer(offset, bytes(payload))
401
+
402
+
403
+ def get_status() -> Query:
404
+ """Return GetStatus, answered by the status byte; decode it with :func:`status`."""
405
+ return _get_status()
406
+
407
+
408
+ def get_irq_status() -> Query:
409
+ """Return GetIrqStatus, answered by two IRQ bytes; decode them with :func:`irq`."""
410
+ return _get_irq_status()
411
+
412
+
413
+ def get_rx_buffer_status() -> Query:
414
+ """Return GetRxBufferStatus, answered by two bytes; decode them with :func:`rx_buffer_status`."""
415
+ return _get_rx_buffer_status()
416
+
417
+
418
+ def get_packet_status() -> Query:
419
+ """Return GetPacketStatus, answered by three bytes; decode them with :func:`packet_status`."""
420
+ return _get_packet_status()
421
+
422
+
423
+ def get_rssi_inst() -> Query:
424
+ """Return GetRssiInst, answered by one byte; decode it with :func:`rssi_inst_dbm`."""
425
+ return _get_rssi_inst()
426
+
427
+
428
+ def get_device_errors() -> Query:
429
+ """Return GetDeviceErrors, answered by two bytes; decode them with :func:`device_errors`."""
430
+ return _get_device_errors()
431
+
432
+
433
+ def read_register(address: int, length: int) -> Query:
434
+ """Return ReadRegister for a run of consecutive registers.
435
+
436
+ :param address: The first register's address.
437
+ :param length: How many registers to read.
438
+ :returns: The query.
439
+ """
440
+ return _read_register(address, length)
441
+
442
+
443
+ def read_buffer(offset: int, length: int) -> Query:
444
+ """Return ReadBuffer for a run of the data buffer.
445
+
446
+ :param offset: Where in the buffer the first byte is.
447
+ :param length: How many bytes to read.
448
+ :returns: The query.
449
+ """
450
+ return _read_buffer(offset, length)
451
+
452
+
453
+ def status(byte: int) -> Status:
454
+ """Decode a status byte.
455
+
456
+ :param byte: The status byte.
457
+ :returns: The chip's mode, how its last command went, and whether that was an error.
458
+
459
+ >>> status(0x2C).chip_mode
460
+ 'StandbyRc'
461
+ """
462
+ return _status(byte)
463
+
464
+
465
+ def irq(answer: bytes) -> Irq:
466
+ """Decode a GetIrqStatus answer.
467
+
468
+ :param answer: The two answer bytes.
469
+ :returns: The pending interrupts.
470
+ :raises ValueError: If the answer is not two bytes.
471
+ """
472
+ return Irq(_irq(bytes(answer)))
473
+
474
+
475
+ def device_errors(answer: bytes) -> DeviceError:
476
+ """Decode a GetDeviceErrors answer.
477
+
478
+ :param answer: The two answer bytes.
479
+ :returns: The flagged errors.
480
+ :raises ValueError: If the answer is not two bytes.
481
+ """
482
+ return DeviceError(_device_errors(bytes(answer)))
483
+
484
+
485
+ def packet_status(answer: bytes) -> PacketStatus:
486
+ """Decode a LoRa GetPacketStatus answer.
487
+
488
+ :param answer: RssiPkt, SnrPkt, and SignalRssiPkt.
489
+ :returns: The three signal levels, exact to a hundredth of a decibel.
490
+ :raises ValueError: If the answer is not three bytes.
491
+ """
492
+ return _packet_status(bytes(answer))
493
+
494
+
495
+ def rx_buffer_status(answer: bytes) -> RxBufferStatus:
496
+ """Decode a GetRxBufferStatus answer.
497
+
498
+ :param answer: PayloadLengthRx and RxStartBufferPointer.
499
+ :returns: The payload length and where it starts in the data buffer.
500
+ :raises ValueError: If the answer is not two bytes.
501
+ """
502
+ return _rx_buffer_status(bytes(answer))
503
+
504
+
505
+ def rssi_inst_dbm(byte: int) -> float:
506
+ """Decode a GetRssiInst answer.
507
+
508
+ :param byte: RssiInst.
509
+ :returns: The signal power the receiver hears right now, in dBm.
510
+ """
511
+ return _rssi_inst_dbm(byte)
512
+
513
+
514
+ def llcc68_supports(link: LoraLink) -> bool:
515
+ """Report whether an LLCC68 supports a link's spreading factor at its bandwidth.
516
+
517
+ The LLCC68 takes the SX1262's commands but not all its settings: up to SF9 at 125 kHz,
518
+ SF10 at 250 kHz, and SF11 at 500 kHz, and no bandwidth below 125 kHz.
519
+
520
+ :param link: The link settings.
521
+ :returns: ``True`` when an LLCC68 can use the link.
522
+ """
523
+ return _llcc68_supports(link)
@@ -0,0 +1,493 @@
1
+ """The Semtech SX1276, SX1277, SX1278, and SX1279, as the register values they take.
2
+
3
+ These radios, and modules built on them such as the RFM95W, are driven through registers.
4
+ Each SPI transaction starts with an address byte whose top bit is set for a write, followed
5
+ by the data, and the address advances with each byte except at the FIFO. These functions
6
+ give the addresses, the values a LoRa link and an output power put in them, and the
7
+ readings decoded, from the SX1276/77/78/79 datasheet (Rev 7), so a program with its own SPI
8
+ access can drive the chip.
9
+ """
10
+
11
+ from __future__ import annotations
12
+
13
+ from enum import Enum, IntEnum, IntFlag
14
+
15
+ from pamoja._native import (
16
+ LinkBudget,
17
+ LoraLink,
18
+ Sx127xHighBwOptimize as HighBwOptimize,
19
+ Sx127xModem as Modem,
20
+ Sx127xModemStatus as ModemStatus,
21
+ Sx127xPacketStatus as PacketStatus,
22
+ Sx127xSpuriousReception as SpuriousReception,
23
+ Sx127xTxPower as TxPower,
24
+ )
25
+ from pamoja._native import sx127x_automatic_if as _automatic_if
26
+ from pamoja._native import sx127x_constants as _constants
27
+ from pamoja._native import sx127x_frequency_from_word as _frequency_from_word
28
+ from pamoja._native import sx127x_frequency_word as _frequency_word
29
+ from pamoja._native import sx127x_fsk_op_mode as _fsk_op_mode
30
+ from pamoja._native import sx127x_high_bw_optimize as _high_bw_optimize
31
+ from pamoja._native import sx127x_image_cal_start as _image_cal_start
32
+ from pamoja._native import sx127x_invert_iq as _invert_iq
33
+ from pamoja._native import sx127x_invert_iq_2 as _invert_iq_2
34
+ from pamoja._native import sx127x_irq_flags as _irq_flags
35
+ from pamoja._native import sx127x_lora_op_mode as _lora_op_mode
36
+ from pamoja._native import sx127x_mode_from_op_mode as _mode_from_op_mode
37
+ from pamoja._native import sx127x_modem as _modem
38
+ from pamoja._native import sx127x_modem_status as _modem_status
39
+ from pamoja._native import sx127x_ocp_register as _ocp_register
40
+ from pamoja._native import sx127x_packet_status as _packet_status
41
+ from pamoja._native import sx127x_read_address as _read_address
42
+ from pamoja._native import sx127x_registers as _registers
43
+ from pamoja._native import sx127x_rssi_dbm as _rssi_dbm
44
+ from pamoja._native import sx127x_spurious_reception as _spurious_reception
45
+ from pamoja._native import sx127x_symbol_timeout as _symbol_timeout
46
+ from pamoja._native import sx127x_tx_power as _tx_power
47
+ from pamoja._native import sx127x_tx_power_under_ceiling as _tx_power_under_ceiling
48
+ from pamoja._native import sx127x_write_address as _write_address
49
+
50
+ __all__ = [
51
+ "IMAGE_CAL_RUNNING",
52
+ "IMAGE_CAL_START",
53
+ "LNA_BOOSTED",
54
+ "PA_DAC_DEFAULT",
55
+ "PA_DAC_HIGH_POWER",
56
+ "SYNC_WORD_PRIVATE",
57
+ "SYNC_WORD_PUBLIC",
58
+ "TCXO_INPUT_ON",
59
+ "VERSION",
60
+ "WRITE",
61
+ "Dio0",
62
+ "HighBwOptimize",
63
+ "Irq",
64
+ "Mode",
65
+ "Modem",
66
+ "ModemStatus",
67
+ "PaOutput",
68
+ "PacketStatus",
69
+ "Register",
70
+ "SpuriousReception",
71
+ "TxPower",
72
+ "automatic_if",
73
+ "frequency_from_word",
74
+ "frequency_word",
75
+ "fsk_op_mode",
76
+ "high_bw_optimize",
77
+ "image_cal_start",
78
+ "invert_iq",
79
+ "invert_iq_2",
80
+ "lora_op_mode",
81
+ "mode_from_op_mode",
82
+ "modem",
83
+ "modem_status",
84
+ "ocp_register",
85
+ "packet_status",
86
+ "read_address",
87
+ "rssi_dbm",
88
+ "spurious_reception",
89
+ "symbol_timeout",
90
+ "tx_power",
91
+ "tx_power_under_ceiling",
92
+ "write_address",
93
+ ]
94
+
95
+ _REGISTERS = _registers()
96
+ _CONSTANTS = _constants()
97
+ _IRQ_FLAGS = _irq_flags()
98
+
99
+ #: The RegVersion value of an SX1276, SX1277, SX1278, or SX1279.
100
+ VERSION = _CONSTANTS["version"]
101
+ #: The bit of an address byte that makes an access a write.
102
+ WRITE = _CONSTANTS["write"]
103
+ #: The sync word the datasheet reserves for LoRaWAN networks.
104
+ SYNC_WORD_PUBLIC = _CONSTANTS["syncWordPublic"]
105
+ #: The private sync word, and the chip's reset value.
106
+ SYNC_WORD_PRIVATE = _CONSTANTS["syncWordPrivate"]
107
+ #: RegPaDac at its reset value.
108
+ PA_DAC_DEFAULT = _CONSTANTS["paDacDefault"]
109
+ #: RegPaDac with the +20 dBm setting on PA_BOOST.
110
+ PA_DAC_HIGH_POWER = _CONSTANTS["paDacHighPower"]
111
+ #: The RegImageCal bit that starts a calibration.
112
+ IMAGE_CAL_START = _CONSTANTS["imageCalStart"]
113
+ #: The RegImageCal bit set while a calibration runs.
114
+ IMAGE_CAL_RUNNING = _CONSTANTS["imageCalRunning"]
115
+ #: RegLna with maximum gain and the high frequency LNA boost.
116
+ LNA_BOOSTED = _CONSTANTS["lnaBoosted"]
117
+ #: RegTcxo for a module clocked by a TCXO.
118
+ TCXO_INPUT_ON = _CONSTANTS["tcxoInputOn"]
119
+
120
+
121
+ class Register(IntEnum):
122
+ """The register addresses, from Table 41 of the datasheet with the LoRa page selected."""
123
+
124
+ #: RegFifo, the LoRa data buffer, read or written at RegFifoAddrPtr.
125
+ FIFO = _REGISTERS["fifo"]
126
+ #: RegOpMode: LoRa or FSK, the register page, and the operating mode.
127
+ OP_MODE = _REGISTERS["opMode"]
128
+ #: RegFrfMsb, the top byte of the carrier word.
129
+ FRF_MSB = _REGISTERS["frfMsb"]
130
+ #: RegFrfMid, the middle byte of the carrier word.
131
+ FRF_MID = _REGISTERS["frfMid"]
132
+ #: RegFrfLsb, the low byte of the carrier word.
133
+ FRF_LSB = _REGISTERS["frfLsb"]
134
+ #: RegPaConfig: the amplifier output, its maximum, and the power.
135
+ PA_CONFIG = _REGISTERS["paConfig"]
136
+ #: RegPaRamp, the amplifier ramp time.
137
+ PA_RAMP = _REGISTERS["paRamp"]
138
+ #: RegOcp, the amplifier current limit.
139
+ OCP = _REGISTERS["ocp"]
140
+ #: RegLna, the LNA gain and current.
141
+ LNA = _REGISTERS["lna"]
142
+ #: RegFifoAddrPtr, where the next RegFifo access lands.
143
+ FIFO_ADDR_PTR = _REGISTERS["fifoAddrPtr"]
144
+ #: RegFifoTxBaseAddr, where a transmitted payload starts.
145
+ FIFO_TX_BASE_ADDR = _REGISTERS["fifoTxBaseAddr"]
146
+ #: RegFifoRxBaseAddr, where received payloads start.
147
+ FIFO_RX_BASE_ADDR = _REGISTERS["fifoRxBaseAddr"]
148
+ #: RegFifoRxCurrentAddr, where the last received packet starts.
149
+ FIFO_RX_CURRENT_ADDR = _REGISTERS["fifoRxCurrentAddr"]
150
+ #: RegIrqFlagsMask, the interrupts masked off.
151
+ IRQ_FLAGS_MASK = _REGISTERS["irqFlagsMask"]
152
+ #: RegIrqFlags, the interrupts raised, each cleared by writing it back as a 1.
153
+ IRQ_FLAGS = _REGISTERS["irqFlags"]
154
+ #: RegRxNbBytes, the payload length of the last packet received.
155
+ RX_NB_BYTES = _REGISTERS["rxNbBytes"]
156
+ #: RegModemStat, the live state of the modem.
157
+ MODEM_STAT = _REGISTERS["modemStat"]
158
+ #: RegPktSnrValue, the SNR of the last packet in quarters of a decibel.
159
+ PKT_SNR_VALUE = _REGISTERS["pktSnrValue"]
160
+ #: RegPktRssiValue, the RSSI of the last packet.
161
+ PKT_RSSI_VALUE = _REGISTERS["pktRssiValue"]
162
+ #: RegRssiValue, the RSSI the receiver hears right now.
163
+ RSSI_VALUE = _REGISTERS["rssiValue"]
164
+ #: RegHopChannel, the PLL lock and the CRC the last header announced.
165
+ HOP_CHANNEL = _REGISTERS["hopChannel"]
166
+ #: RegModemConfig1: bandwidth, coding rate, and header mode.
167
+ MODEM_CONFIG_1 = _REGISTERS["modemConfig1"]
168
+ #: RegModemConfig2: spreading factor, CRC, and the top bits of the symbol timeout.
169
+ MODEM_CONFIG_2 = _REGISTERS["modemConfig2"]
170
+ #: RegSymbTimeoutLsb, the low byte of the symbol timeout.
171
+ SYMB_TIMEOUT_LSB = _REGISTERS["symbTimeoutLsb"]
172
+ #: RegPreambleMsb, the high byte of the preamble length.
173
+ PREAMBLE_MSB = _REGISTERS["preambleMsb"]
174
+ #: RegPreambleLsb, the low byte of the preamble length.
175
+ PREAMBLE_LSB = _REGISTERS["preambleLsb"]
176
+ #: RegPayloadLength, the payload length to send.
177
+ PAYLOAD_LENGTH = _REGISTERS["payloadLength"]
178
+ #: RegMaxPayloadLength, the longest payload a received header may announce.
179
+ MAX_PAYLOAD_LENGTH = _REGISTERS["maxPayloadLength"]
180
+ #: RegModemConfig3: low data rate optimization and the automatic gain control.
181
+ MODEM_CONFIG_3 = _REGISTERS["modemConfig3"]
182
+ #: RegRssiWideband, a wideband RSSI sample.
183
+ RSSI_WIDEBAND = _REGISTERS["rssiWideband"]
184
+ #: RegIfFreq2, which the spurious reception erratum sets.
185
+ IF_FREQ_2 = _REGISTERS["ifFreq2"]
186
+ #: RegIfFreq1, which the spurious reception erratum clears.
187
+ IF_FREQ_1 = _REGISTERS["ifFreq1"]
188
+ #: RegDetectOptimize: the automatic IF and the detection optimization.
189
+ DETECT_OPTIMIZE = _REGISTERS["detectOptimize"]
190
+ #: RegInvertIQ, the IQ polarity of each path.
191
+ INVERT_IQ = _REGISTERS["invertIq"]
192
+ #: RegHighBwOptimize1, which the 500 kHz erratum sets.
193
+ HIGH_BW_OPTIMIZE_1 = _REGISTERS["highBwOptimize1"]
194
+ #: RegDetectionThreshold, the LoRa detection threshold.
195
+ DETECTION_THRESHOLD = _REGISTERS["detectionThreshold"]
196
+ #: RegSyncWord, the LoRa sync word.
197
+ SYNC_WORD = _REGISTERS["syncWord"]
198
+ #: RegHighBwOptimize2, which the 500 kHz erratum sets.
199
+ HIGH_BW_OPTIMIZE_2 = _REGISTERS["highBwOptimize2"]
200
+ #: RegInvertIQ2, which completes an IQ inversion.
201
+ INVERT_IQ_2 = _REGISTERS["invertIq2"]
202
+ #: RegImageCal, at the address of RegInvertIQ2 on the FSK page.
203
+ IMAGE_CAL = _REGISTERS["imageCal"]
204
+ #: RegDioMapping1, the events DIO0 to DIO3 signal.
205
+ DIO_MAPPING_1 = _REGISTERS["dioMapping1"]
206
+ #: RegDioMapping2, the events DIO4 and DIO5 signal.
207
+ DIO_MAPPING_2 = _REGISTERS["dioMapping2"]
208
+ #: RegVersion, the silicon revision.
209
+ VERSION = _REGISTERS["version"]
210
+ #: RegTcxo, a crystal or a TCXO on XTA.
211
+ TCXO = _REGISTERS["tcxo"]
212
+ #: RegPaDac, the +20 dBm setting of PA_BOOST.
213
+ PA_DAC = _REGISTERS["paDac"]
214
+
215
+
216
+ class Dio0(IntEnum):
217
+ """The RegDioMapping1 values that route an event to DIO0, from Table 18 of the datasheet."""
218
+
219
+ #: DIO0 signals RxDone.
220
+ RX_DONE = _CONSTANTS["dio0RxDone"]
221
+ #: DIO0 signals TxDone.
222
+ TX_DONE = _CONSTANTS["dio0TxDone"]
223
+ #: DIO0 signals CadDone.
224
+ CAD_DONE = _CONSTANTS["dio0CadDone"]
225
+
226
+
227
+ class Irq(IntFlag):
228
+ """The LoRa interrupt flags of RegIrqFlags."""
229
+
230
+ #: A single reception timed out before a preamble arrived.
231
+ RX_TIMEOUT = _IRQ_FLAGS["rxTimeout"]
232
+ #: A packet has been received.
233
+ RX_DONE = _IRQ_FLAGS["rxDone"]
234
+ #: The payload failed its CRC.
235
+ PAYLOAD_CRC_ERROR = _IRQ_FLAGS["payloadCrcError"]
236
+ #: A valid header was received.
237
+ VALID_HEADER = _IRQ_FLAGS["validHeader"]
238
+ #: The payload has been transmitted.
239
+ TX_DONE = _IRQ_FLAGS["txDone"]
240
+ #: Channel activity detection finished.
241
+ CAD_DONE = _IRQ_FLAGS["cadDone"]
242
+ #: Frequency hopping moved to the next channel.
243
+ FHSS_CHANGE_CHANNEL = _IRQ_FLAGS["fhssChangeChannel"]
244
+ #: Channel activity detection heard a LoRa signal.
245
+ CAD_DETECTED = _IRQ_FLAGS["cadDetected"]
246
+
247
+
248
+ class PaOutput(str, Enum):
249
+ """The amplifier output a module wires to its antenna.
250
+
251
+ The SPI interface cannot see which output a module uses, so the caller names it.
252
+ """
253
+
254
+ #: The high efficiency amplifier on RFO_LF or RFO_HF, -4 to +15 dBm.
255
+ RFO = "Rfo"
256
+ #: The regulated amplifier on PA_BOOST, +2 to +20 dBm, as on the RFM95W.
257
+ PA_BOOST = "PaBoost"
258
+
259
+
260
+ class Mode(str, Enum):
261
+ """The operating modes of RegOpMode."""
262
+
263
+ #: Only the SPI interface and the registers are powered; the only mode that may switch modems.
264
+ SLEEP = "Sleep"
265
+ #: The oscillator and the baseband are on.
266
+ STANDBY = "Standby"
267
+ #: The PLL is locked for transmit.
268
+ FS_TX = "FsTx"
269
+ #: One packet goes out, then the chip returns to standby.
270
+ TX = "Tx"
271
+ #: The PLL is locked for receive.
272
+ FS_RX = "FsRx"
273
+ #: The receiver takes packet after packet.
274
+ RX_CONTINUOUS = "RxContinuous"
275
+ #: The receiver waits for one packet or the symbol timeout.
276
+ RX_SINGLE = "RxSingle"
277
+ #: Channel activity detection looks for a LoRa preamble.
278
+ CAD = "Cad"
279
+
280
+
281
+ def frequency_word(frequency_hz: int) -> int:
282
+ """Return the 24-bit RegFrf word for a frequency.
283
+
284
+ :param frequency_hz: The carrier frequency in hertz.
285
+ :returns: The frequency times 2^19 over the 32 MHz crystal, rounded to the nearest step.
286
+
287
+ >>> hex(frequency_word(868_100_000))
288
+ '0xd90666'
289
+ """
290
+ return _frequency_word(frequency_hz)
291
+
292
+
293
+ def frequency_from_word(word: int) -> int:
294
+ """Return the frequency a RegFrf word selects.
295
+
296
+ :param word: The 24-bit frequency word.
297
+ :returns: The carrier frequency in hertz.
298
+ """
299
+ return _frequency_from_word(word)
300
+
301
+
302
+ def read_address(address: int) -> int:
303
+ """Return the address byte that reads a register.
304
+
305
+ :param address: The register address.
306
+ :returns: The address with the write bit clear.
307
+ """
308
+ return _read_address(address)
309
+
310
+
311
+ def write_address(address: int) -> int:
312
+ """Return the address byte that writes a register.
313
+
314
+ :param address: The register address.
315
+ :returns: The address with the write bit set.
316
+ """
317
+ return _write_address(address)
318
+
319
+
320
+ def lora_op_mode(mode: Mode | str) -> int:
321
+ """Return the RegOpMode value for a LoRa operating mode.
322
+
323
+ :param mode: The operating mode.
324
+ :returns: The register value, with the LoRa register page selected.
325
+ :raises ValueError: If no mode goes by that name.
326
+ """
327
+ return _lora_op_mode(Mode(mode).value)
328
+
329
+
330
+ def fsk_op_mode(mode: Mode | str) -> int:
331
+ """Return the RegOpMode value for an FSK operating mode, which image calibration needs.
332
+
333
+ :param mode: The operating mode.
334
+ :returns: The register value.
335
+ :raises ValueError: If no mode goes by that name.
336
+ """
337
+ return _fsk_op_mode(Mode(mode).value)
338
+
339
+
340
+ def mode_from_op_mode(op_mode: int) -> Mode:
341
+ """Return the operating mode a RegOpMode value holds.
342
+
343
+ :param op_mode: The register value.
344
+ :returns: The mode.
345
+ """
346
+ return Mode(_mode_from_op_mode(op_mode))
347
+
348
+
349
+ def modem(link: LoraLink, frequency_hz: int, symbol_timeout: int = 0) -> Modem:
350
+ """Return the LoRa modem registers for a link at a carrier.
351
+
352
+ :param link: The link settings.
353
+ :param frequency_hz: The carrier frequency, which rules out 250 and 500 kHz below 175 MHz.
354
+ :param symbol_timeout: A single reception timeout in symbols, whose top bits go in
355
+ RegModemConfig2.
356
+ :returns: RegModemConfig1 to 3 and the SF6 detection settings.
357
+ :raises PamojaError: If the SX127x cannot use the link at the carrier.
358
+ """
359
+ return _modem(link, frequency_hz, symbol_timeout)
360
+
361
+
362
+ def symbol_timeout(link: LoraLink, timeout_us: int) -> int:
363
+ """Return a single reception timeout in the link's symbols.
364
+
365
+ :param link: The link settings, whose symbol time counts the timeout.
366
+ :param timeout_us: How long to listen for a preamble, in microseconds.
367
+ :returns: The timeout rounded up to whole symbols, from 4 to 1023.
368
+ """
369
+ return _symbol_timeout(link, timeout_us)
370
+
371
+
372
+ def tx_power(output: PaOutput | str, output_dbm: int) -> TxPower:
373
+ """Choose the amplifier settings for an output power.
374
+
375
+ :param output: The amplifier output the module uses.
376
+ :param output_dbm: The output power wanted, in dBm.
377
+ :returns: The settings, clamped to what the output delivers.
378
+ :raises ValueError: If no output goes by that name.
379
+ """
380
+ return _tx_power(PaOutput(output).value, output_dbm)
381
+
382
+
383
+ def tx_power_under_ceiling(
384
+ output: PaOutput | str, budget: LinkBudget, eirp_ceiling_dbm: float
385
+ ) -> TxPower:
386
+ """Choose the amplifier settings that keep a link's EIRP at or under a ceiling.
387
+
388
+ :param output: The amplifier output the module uses.
389
+ :param budget: The link budget, whose transmitting antenna and cable apply.
390
+ :param eirp_ceiling_dbm: The EIRP limit in dBm.
391
+ :returns: The settings, rounded down to whole decibels.
392
+ :raises ValueError: If no output goes by that name.
393
+ """
394
+ return _tx_power_under_ceiling(PaOutput(output).value, budget, eirp_ceiling_dbm)
395
+
396
+
397
+ def ocp_register(milliamps: int) -> int:
398
+ """Return RegOcp for a current limit.
399
+
400
+ :param milliamps: The most current the amplifier may draw.
401
+ :returns: The register value with the protection on.
402
+ """
403
+ return _ocp_register(milliamps)
404
+
405
+
406
+ def invert_iq(receive: bool, transmit: bool) -> int:
407
+ """Return RegInvertIQ for the IQ polarity of each path.
408
+
409
+ :param receive: Whether to invert the receive path, as a LoRaWAN device does for downlinks.
410
+ :param transmit: Whether to invert the transmit path, as a gateway does.
411
+ :returns: The register value, with the transmit bit set for normal IQ as the reference
412
+ drivers have it.
413
+ """
414
+ return _invert_iq(receive, transmit)
415
+
416
+
417
+ def invert_iq_2(inverted: bool) -> int:
418
+ """Return RegInvertIQ2 for the path in use.
419
+
420
+ :param inverted: Whether that path is inverted.
421
+ :returns: 0x19 when inverted, else 0x1D.
422
+ """
423
+ return _invert_iq_2(inverted)
424
+
425
+
426
+ def high_bw_optimize(link: LoraLink, frequency_hz: int) -> HighBwOptimize:
427
+ """Return the writes of the 500 kHz sensitivity erratum.
428
+
429
+ :param link: The link settings, whose bandwidth decides.
430
+ :param frequency_hz: The carrier frequency in hertz.
431
+ :returns: RegHighBwOptimize1 and, where it is written, RegHighBwOptimize2.
432
+ :raises PamojaError: If the SX127x has no such bandwidth.
433
+ """
434
+ return _high_bw_optimize(link, frequency_hz)
435
+
436
+
437
+ def spurious_reception(link: LoraLink) -> SpuriousReception:
438
+ """Return the receive settings of the spurious reception erratum.
439
+
440
+ :param link: The link settings, whose bandwidth decides.
441
+ :returns: The automatic IF, the hand-set IF, and the carrier offset.
442
+ :raises PamojaError: If the SX127x has no such bandwidth.
443
+ """
444
+ return _spurious_reception(link)
445
+
446
+
447
+ def image_cal_start(current: int) -> int:
448
+ """Return RegImageCal to start a calibration.
449
+
450
+ :param current: The register's current value.
451
+ :returns: The value with ImageCalStart set and AutoImageCalOn clear.
452
+ """
453
+ return _image_cal_start(current)
454
+
455
+
456
+ def automatic_if(current: int, on: bool) -> int:
457
+ """Return RegDetectOptimize with AutomaticIFOn set or clear.
458
+
459
+ :param current: The register's current value.
460
+ :param on: Whether the automatic IF stays on.
461
+ :returns: The register value.
462
+ """
463
+ return _automatic_if(current, on)
464
+
465
+
466
+ def packet_status(answer: bytes, frequency_hz: int) -> PacketStatus:
467
+ """Decode RegPktSnrValue and RegPktRssiValue, read together.
468
+
469
+ :param answer: The two register values, SNR first.
470
+ :param frequency_hz: The carrier the packet was heard at, which picks the RF port's offset.
471
+ :returns: The three signal levels, exact to a hundredth of a decibel.
472
+ :raises ValueError: If the answer is not two bytes.
473
+ """
474
+ return _packet_status(bytes(answer), frequency_hz)
475
+
476
+
477
+ def rssi_dbm(byte: int, frequency_hz: int) -> float:
478
+ """Decode RegRssiValue.
479
+
480
+ :param byte: The register value.
481
+ :param frequency_hz: The carrier the receiver is tuned to.
482
+ :returns: The signal power the receiver hears right now, in dBm.
483
+ """
484
+ return _rssi_dbm(byte, frequency_hz)
485
+
486
+
487
+ def modem_status(byte: int) -> ModemStatus:
488
+ """Decode RegModemStat.
489
+
490
+ :param byte: The register value.
491
+ :returns: The modem's live state.
492
+ """
493
+ return _modem_status(byte)
@@ -0,0 +1,30 @@
1
+ [build-system]
2
+ requires = ["hatchling>=1.27"]
3
+ build-backend = "hatchling.build"
4
+
5
+ [project]
6
+ name = "pamoja-radios"
7
+ version = "0.2.0"
8
+ description = "The Semtech SX126x and SX127x LoRa radios and the SX1302 and SX1303 gateway concentrators: their commands, registers, and decoders, the amplifier setting a regional EIRP ceiling allows, a duty-cycle guard, and simulated chips that stand in for a module."
9
+ readme = "README.md"
10
+ license = { text = "MIT" }
11
+ license-files = ["LICENSE-MIT"]
12
+ requires-python = ">=3.10"
13
+ authors = [{ name = "molexxxx" }]
14
+ keywords = ["pamoja", "iot", "robotics", "radios"]
15
+ classifiers = [
16
+ "Programming Language :: Python :: 3",
17
+ "License :: OSI Approved :: MIT License",
18
+ "Operating System :: OS Independent",
19
+ "Typing :: Typed",
20
+ ]
21
+ dependencies = [
22
+ "pamoja-native==0.2.0",
23
+ ]
24
+
25
+ [project.urls]
26
+ Repository = "https://github.com/molexxxx/pamoja"
27
+ Documentation = "https://pamoja.molex.cloud/docs/guides/radios.html"
28
+
29
+ [tool.hatch.build.targets.wheel]
30
+ packages = ["pamoja"]