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.
- pamoja_radios-0.2.0/.gitignore +13 -0
- pamoja_radios-0.2.0/LICENSE-MIT +21 -0
- pamoja_radios-0.2.0/PKG-INFO +116 -0
- pamoja_radios-0.2.0/README.md +98 -0
- pamoja_radios-0.2.0/pamoja/radios/__init__.py +38 -0
- pamoja_radios-0.2.0/pamoja/radios/py.typed +0 -0
- pamoja_radios-0.2.0/pamoja/radios/sx126x.py +523 -0
- pamoja_radios-0.2.0/pamoja/radios/sx127x.py +493 -0
- pamoja_radios-0.2.0/pyproject.toml +30 -0
|
@@ -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
|
+
[](https://pamoja.molex.cloud/docs/guides/radios.html)
|
|
24
|
+
[](https://pamoja.molex.cloud/docs/)
|
|
25
|
+
[](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
|
+
[](https://pamoja.molex.cloud/docs/guides/radios.html)
|
|
6
|
+
[](https://pamoja.molex.cloud/docs/)
|
|
7
|
+
[](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"]
|