Skip to content

Commit a7725ec

Browse files
icyqwqlbuque
authored andcommitted
libs/module: Support Lora Module.
Signed-off-by: icyqwq <[email protected]>
1 parent f0d1825 commit a7725ec

File tree

9 files changed

+288
-14
lines changed

9 files changed

+288
-14
lines changed

docs/en/module/index.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ Module
88
display.rst
99
dualkmeter.rst
1010
hmi.rst
11+
lora.rst
1112
plus.rst
1213
pps.rst
1314
rca.rst

docs/en/module/lora.rst

Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
2+
LoraModule
3+
==========
4+
5+
.. include:: ../refs/module.loramodule.ref
6+
7+
The LoRa433_V1.1 Module is part of the M5Stack stackable module series. It is a LoRa communication module that operates at a 433MHz frequency and utilizes the Ra-02 module (SX1278 chip) solution.
8+
9+
Support the following products:
10+
11+
|LoraModule|
12+
13+
Micropython Example::
14+
15+
import os, sys, io
16+
import M5
17+
from M5 import *
18+
from module import LoraModule
19+
lora = LoraModule()
20+
lora.send("Hello, LoRa!")
21+
print(lora.recv())
22+
23+
24+
UIFLOW2 Example:
25+
26+
|example.svg|
27+
28+
.. only:: builder_html
29+
30+
class LoraModule
31+
----------------
32+
33+
Constructors
34+
------------
35+
36+
.. class:: LoraModule(pin_cs, pin_irq, pin_rst, freq_band, sf, bw, coding_rate, preamble_len, output_power)
37+
38+
Initialize the LoRa module.
39+
40+
:param int pin_cs: Chip select pin
41+
:param int pin_irq: Interrupt pin
42+
:param int pin_rst: Reset pin
43+
:param freq_band: LoRa RF frequency in kHz.
44+
:param int sf: Spreading factor, Higher spreading factors allow reception of weaker signals but have slower data rate.
45+
:param str bw: Bandwidth value in kHz. Must be exactly one of BANDWIDTHS
46+
:param int coding_rate: Forward Error Correction (FEC) coding rate is expressed as a ratio, &#x60;4/N&#x60;.
47+
:param int preamble_len: Length of the preamble sequence, in units of symbols.
48+
:param int output_power: Output power in dBm.
49+
50+
UIFLOW2:
51+
52+
|init.svg|
53+
54+
55+
Methods
56+
-------
57+
58+
.. method:: LoraModule.send(packet, tx_at_ms)
59+
60+
Send a data packet.
61+
62+
:param packet: The data packet to send.
63+
:param tx_at_ms: Time to transmit the packet in milliseconds. For precise timing of sent packets, there is an optional &#x60;tx_at_ms&#x60; argument which is a timestamp (as a &#x60;time.ticks_ms()&#x60; value). If set, the packet will be sent as close as possible to this timestamp and the function will block until that time arrives
64+
65+
UIFLOW2:
66+
67+
|send.svg|
68+
69+
.. method:: LoraModule.recv(timeout_ms, rx_length, rx_packet)
70+
71+
Receive a data packet.
72+
73+
:param timeout_ms: Optional, sets a receive timeout in milliseconds. If None (default value), then the function will block indefinitely until a packet is received.
74+
:param int rx_length: Necessary to set if &#x60;implicit_header&#x60; is set to &#x60;True&#x60; (see above). This is the length of the packet to receive. Ignored in the default LoRa explicit header mode, where the received radio header includes the length.
75+
:param RxPacket rx_packet: Optional, this can be an &#x60;RxPacket&#x60; object previously received from the modem. If the newly received packet has the same length, this object is reused and returned to save an allocation. If the newly received packet has a different length, a new &#x60;RxPacket&#x60; object is allocated and returned instead.
76+
77+
UIFLOW2:
78+
79+
|recv.svg|
80+
81+
82+
83+
Constants
84+
---------
85+
86+
.. data:: LoraModule.LORA_433
87+
.. data:: LoraModule.LORA_868
88+
89+
Select the LoRa frequency band.
90+
91+
92+
.. data:: LoraModule.BANDWIDTHS
93+
94+
Valid bandwidth
95+
96+

docs/en/refs/module.lora.ref

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
2+
.. |LoraModule| image:: https://static-cdn.m5stack.com/resource/docs/products/module/Module-LoRa433_V1.1/img-dac09b0a-7367-4ed9-9374-b604f646ec3b.webp
3+
:target: https://docs.m5stack.com/en/module/Module-LoRa433_V1.1
4+
:height: 200px
5+
:width: 200px
6+
7+
.. |init.svg| image:: https://static-cdn.m5stack.com/mpy_docs/module/loramodule/init.svg
8+
.. |_validate_range.svg| image:: https://static-cdn.m5stack.com/mpy_docs/module/loramodule/_validate_range.svg
9+
.. |send.svg| image:: https://static-cdn.m5stack.com/mpy_docs/module/loramodule/send.svg
10+
.. |recv.svg| image:: https://static-cdn.m5stack.com/mpy_docs/module/loramodule/recv.svg
11+
12+
.. |example.svg| image:: https://static-cdn.m5stack.com/mpy_docs/module/loramodule/example.svg
13+

m5stack/libs/module/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
"Encoder4MotorModule": "encoder4_motor",
1212
"HMIModule": "hmi",
1313
"IotBaseCatmModule": "iot_base_catm",
14+
"LoraModule": "lora",
1415
"PLUSModule": "plus",
1516
"PPSModule": "pps",
1617
"RCAModule": "rca",

m5stack/libs/module/lora.py

Lines changed: 153 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,153 @@
1+
# SPDX-FileCopyrightText: 2024 M5Stack Technology CO LTD
2+
#
3+
# SPDX-License-Identifier: MIT
4+
5+
from .mbus import spi2
6+
7+
from machine import Pin, SPI
8+
from lora import SX1278
9+
from lora import SX1276
10+
from lora import RxPacket
11+
from micropython import const
12+
13+
14+
class LoraModule:
15+
"""
16+
note:
17+
cn: LoRa433_V1.1 模块是 M5Stack 堆叠模块系列的一部分,它是一个在 433MHz 频率下运行的 LoRa 通信模块,使用了 Ra-02 模块(SX1278 芯片)方案。
18+
en: The LoRa433_V1.1 Module is part of the M5Stack stackable module series. It is a LoRa communication module that operates at a 433MHz frequency and utilizes the Ra-02 module (SX1278 chip) solution.
19+
20+
details:
21+
color: "#0FE6D7"
22+
link: https://docs.m5stack.com/en/module/Module-LoRa433_V1.1
23+
image: https://static-cdn.m5stack.com/resource/docs/products/module/Module-LoRa433_V1.1/img-dac09b0a-7367-4ed9-9374-b604f646ec3b.webp
24+
category: Module
25+
26+
example: |
27+
from module import LoraModule
28+
lora = LoraModule()
29+
lora.send("Hello, LoRa!")
30+
print(lora.recv())
31+
32+
"""
33+
34+
"""
35+
constant: Select the LoRa frequency band.
36+
"""
37+
LORA_433 = const(1)
38+
LORA_868 = const(2)
39+
40+
"""
41+
constant: Valid bandwidth
42+
"""
43+
BANDWIDTHS = ("7.8", "10.4", "15.6", "20.8", "31.25", "41.7", "62.5", "125", "250", "500")
44+
45+
def __init__(
46+
self,
47+
pin_cs=0,
48+
pin_irq=35,
49+
pin_rst=25,
50+
freq_band=LORA_433,
51+
sf=8,
52+
bw: str = "500",
53+
coding_rate=8,
54+
preamble_len=12,
55+
output_power=0,
56+
):
57+
"""
58+
note: Initialize the LoRa module.
59+
60+
params:
61+
pin_cs:
62+
note: Chip select pin
63+
pin_irq:
64+
note: Interrupt pin
65+
pin_rst:
66+
note: Reset pin
67+
freq_band:
68+
field: dropdown
69+
note: LoRa RF frequency in kHz.
70+
options:
71+
433M: LORA_433
72+
868M: LORA_868
73+
sf:
74+
note: Spreading factor, Higher spreading factors allow reception of weaker signals but have slower data rate.
75+
bw:
76+
note: Bandwidth value in kHz. Must be exactly one of BANDWIDTHS
77+
coding_rate:
78+
note: Forward Error Correction (FEC) coding rate is expressed as a ratio, `4/N`.
79+
preamble_len:
80+
note: Length of the preamble sequence, in units of symbols.
81+
output_power:
82+
note: Output power in dBm.
83+
"""
84+
freq_khz = freq_band == self.LORA_433 and 433000 or 868000
85+
sx_instance = freq_band == self.LORA_433 and SX1278 or SX1276
86+
self._validate_range(sf, 6, 12)
87+
self._validate_range(coding_rate, 5, 8)
88+
89+
if bw not in self.BANDWIDTHS:
90+
raise ValueError(f"Invalid bandwidth {bw}")
91+
92+
lora_cfg = {
93+
"freq_khz": freq_khz,
94+
"sf": sf,
95+
"bw": bw, # kHz
96+
"coding_rate": coding_rate,
97+
"preamble_len": preamble_len,
98+
"output_power": output_power, # dBm
99+
}
100+
101+
self.modem = sx_instance(
102+
spi=spi2,
103+
cs=Pin(pin_cs),
104+
dio0=Pin(pin_irq),
105+
# dio1=Pin(35),
106+
reset=Pin(pin_rst),
107+
lora_cfg=lora_cfg,
108+
)
109+
110+
def _validate_range(self, value, min, max):
111+
if value < min or value > max:
112+
raise ValueError(f"Value {value} out of range {min} to {max}")
113+
114+
def send(self, packet, tx_at_ms=None) -> int:
115+
"""
116+
note: Send a data packet.
117+
118+
label:
119+
en: "%1 send packet %2 at time %3"
120+
cn: "%1 在 %3 时发送数据包 %2"
121+
122+
params:
123+
packet:
124+
note: The data packet to send.
125+
tx_at_ms:
126+
note: Time to transmit the packet in milliseconds. For precise timing of sent packets, there is an optional `tx_at_ms` argument which is a timestamp (as a `time.ticks_ms()` value). If set, the packet will be sent as close as possible to this timestamp and the function will block until that time arrives
127+
128+
return:
129+
note: The return value is the timestamp when transmission completed, as a`time.ticks_ms()` result. It will be more accurate if the modem was initialized to use interrupts.
130+
"""
131+
return self.modem.send(packet, tx_at_ms)
132+
133+
def recv(self, timeout_ms=None, rx_length=0xFF, rx_packet: RxPacket = None) -> RxPacket:
134+
"""
135+
136+
note: Receive a data packet.
137+
138+
label:
139+
en: "%1 receive packet with timeout %2, rx_length %3, rx_packet %4"
140+
cn: "%1 接收数据包,超时 %2, 接收长度 %3, 接收数据包 %4"
141+
142+
params:
143+
timeout_ms:
144+
note: Optional, sets a receive timeout in milliseconds. If None (default value), then the function will block indefinitely until a packet is received.
145+
rx_length:
146+
note: Necessary to set if `implicit_header` is set to `True` (see above). This is the length of the packet to receive. Ignored in the default LoRa explicit header mode, where the received radio header includes the length.
147+
rx_packet:
148+
note: Optional, this can be an `RxPacket` object previously received from the modem. If the newly received packet has the same length, this object is reused and returned to save an allocation. If the newly received packet has a different length, a new `RxPacket` object is allocated and returned instead.
149+
150+
return:
151+
note: Returns None on timeout, or an `RxPacket` instance with the packet on success.
152+
"""
153+
return self.modem.recv(timeout_ms, rx_length, rx_packet)

m5stack/libs/module/manifest.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
"encoder4_motor.py",
1313
"hmi.py",
1414
"iot_base_catm.py",
15+
"lora.py",
1516
"mbus.py",
1617
"module_helper.py",
1718
"plus.py",

m5stack/libs/module/mbus.py

Lines changed: 14 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -2,17 +2,19 @@
22
#
33
# SPDX-License-Identifier: MIT
44

5-
from machine import I2C, Pin
6-
from M5 import getBoard, BOARD
5+
from machine import I2C, Pin, SPI
6+
import M5
77
from collections import namedtuple
88

9-
MBusIO = namedtuple("MBusIO", ["sda0", "scl0", "sda1", "scl1"])
9+
MBusIO = namedtuple(
10+
"MBusIO", ["sda0", "scl0", "sda1", "scl1", "spi2_sck", "spi2_mosi", "spi2_miso"]
11+
)
1012

1113
iomap = {
12-
BOARD.M5Stack: MBusIO(2, 5, 21, 22),
13-
BOARD.M5StackCore2: MBusIO(32, 33, 21, 22),
14-
BOARD.M5StackCoreS3: MBusIO(2, 1, 12, 11),
15-
}.get(getBoard())
14+
M5.BOARD.M5Stack: MBusIO(2, 5, 21, 22, 18, 23, 19),
15+
M5.BOARD.M5StackCore2: MBusIO(32, 33, 21, 22, 18, 23, 38),
16+
M5.BOARD.M5StackCoreS3: MBusIO(2, 1, 12, 11, 36, 37, 35),
17+
}.get(M5.getBoard())
1618

1719

1820
def _i2c0_init():
@@ -23,9 +25,14 @@ def _i2c1_init():
2325
return I2C(1, scl=Pin(iomap.scl1), sda=Pin(iomap.sda1), freq=100000)
2426

2527

28+
def _spi2_init():
29+
return SPI(1, sck=Pin(iomap.spi2_sck), mosi=Pin(iomap.spi2_mosi), miso=Pin(iomap.spi2_miso))
30+
31+
2632
_attrs = {
2733
"i2c0": _i2c0_init,
2834
"i2c1": _i2c1_init,
35+
"spi2": _spi2_init,
2936
}
3037

3138

m5stack/machine_hw_spi.c

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -103,6 +103,7 @@ typedef struct _machine_hw_spi_obj_t {
103103
MACHINE_HW_SPI_STATE_INIT,
104104
MACHINE_HW_SPI_STATE_DEINIT
105105
} state;
106+
bool is_shared;
106107
} machine_hw_spi_obj_t;
107108

108109
// Default pin mappings for the hardware SPI instances
@@ -126,7 +127,7 @@ STATIC void machine_hw_spi_deinit_internal(machine_hw_spi_obj_t *self) {
126127
// NOTE:
127128
// core2和cores3的屏幕和sd卡复用一个spi,
128129
// 所以这里不需要对VSPI_HOST和SPI2_HOST进行初始化。
129-
if (self->host != SPI2_HOST) {
130+
if (!self->is_shared) {
130131
mp_raise_msg(&mp_type_OSError, MP_ERROR_TEXT("SPI device already freed"));
131132
return;
132133
}
@@ -135,7 +136,7 @@ STATIC void machine_hw_spi_deinit_internal(machine_hw_spi_obj_t *self) {
135136
// NOTE:
136137
// core2和cores3的屏幕和sd卡复用一个spi,
137138
// 所以这里不需要对VSPI_HOST和SPI2_HOST进行初始化。
138-
if (self->host != SPI2_HOST) {
139+
if (self->is_shared) {
139140
switch (spi_bus_free(self->host)) {
140141
case ESP_ERR_INVALID_ARG:
141142
mp_raise_msg(&mp_type_OSError, MP_ERROR_TEXT("invalid configuration"));
@@ -267,7 +268,7 @@ STATIC void machine_hw_spi_init_internal(
267268
}
268269
#endif
269270

270-
ret = spi_bus_initialize(self->host, &buscfg, dma_chan);
271+
ret = spi_bus_initialize(self->host, &buscfg, SPI_DMA_CH_AUTO);
271272
switch (ret) {
272273
case ESP_ERR_INVALID_ARG:
273274
mp_raise_msg(&mp_type_OSError, MP_ERROR_TEXT("invalid configuration"));
@@ -277,10 +278,8 @@ STATIC void machine_hw_spi_init_internal(
277278
// NOTE:
278279
// core2和cores3的屏幕和sd卡复用一个spi,
279280
// 所以这里不需要对VSPI_HOST和SPI2_HOST进行初始化。
280-
if (self->host != SPI2_HOST) {
281-
mp_raise_msg(&mp_type_OSError, MP_ERROR_TEXT("SPI host already in use"));
282-
return;
283-
}
281+
self->is_shared = true;
282+
mp_warning("hw_spi", "SPI bus already initialized");
284283
}
285284

286285
ret = spi_bus_add_device(self->host, &devcfg, &self->spi);
@@ -487,6 +486,7 @@ mp_obj_t machine_hw_spi_make_new(const mp_obj_type_t *type, size_t n_args, size_
487486
}
488487

489488
self->base.type = &machine_spi_type;
489+
self->is_shared = false;
490490

491491
int8_t sck, mosi, miso;
492492

m5stack/modules/manifest.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,3 +7,5 @@
77
module("inisetup.py")
88
include("widgets/manifest.py")
99
module("espnow.py")
10+
require("lora-sync")
11+
require("lora-sx127x")

0 commit comments

Comments
 (0)