Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add Semtech SX1272 support. Add basic LoRaWAN node and gateway support #194

Open
wants to merge 3 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions software/glasgow/applet/all.py
Original file line number Diff line number Diff line change
Expand Up @@ -50,3 +50,5 @@
from .video.ws2812_output import VideoWS2812OutputApplet

from .radio.nrf24l01 import RadioNRF24L01Applet
from .radio.sx1272 import RadioSX1272Applet
from .radio.lora import LoRaWANApplet
190 changes: 190 additions & 0 deletions software/glasgow/applet/radio/lora/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,190 @@
import math
import asyncio
import logging
import argparse
from time import sleep
from nmigen.compat import *

from ....support.logging import *
from ....support.endpoint import *
from ....support.bits import *
from ....arch.sx1272 import regs_common
from ....arch.sx1272 import regs_xxk
from ....arch.sx1272 import regs_lora
from ....arch.sx1272.apis import *
from ....protocol.lora import *
from ...interface.spi_master import SPIMasterSubtarget, SPIMasterInterface
from ... import *
from ..sx1272 import RadioSX1272Interface


class LoRaWANApplet(GlasgowApplet, name="radio-lorawan"):
logger = logging.getLogger(__name__)
help = "operate as a LoRaWAN node or gateway"
description = """
Set up a LoRaWAN node or gateway using SX1272 PHY

The gateway uses the Semtech UDP forwarder protocol. Uplink and downlink
operations are multiplexed using the same PHY. The node should join the
network before transmitting any data

RF parameters are set by specifying the region and channel.
"""

__pins = ("ss", "sck", "mosi", "miso")

@classmethod
def add_build_arguments(cls, parser, access):
access.add_build_arguments(parser)

access.add_pin_argument(parser, "ss", default=True)
access.add_pin_argument(parser, "mosi", default=True)
access.add_pin_argument(parser, "miso", default=True)
access.add_pin_argument(parser, "sck", default=True)

parser.add_argument(
"-f", "--frequency", metavar="FREQ", type=int, default=500,
help="set SPI frequency to FREQ kHz (default: %(default)s)")

def build(self, target, args):
self.mux_interface = iface = target.multiplexer.claim_interface(self, args)
pads = iface.get_pads(args, pins=self.__pins)
iface.add_subtarget(SPIMasterSubtarget(
pads=pads,
out_fifo=iface.get_out_fifo(),
in_fifo=iface.get_in_fifo(),
period_cyc=math.ceil(target.sys_clk_freq / (args.frequency * 1000)),
delay_cyc=math.ceil(target.sys_clk_freq / 1e6),
sck_idle=0,
sck_edge="rising",
ss_active=0,
))

async def run(self, device, args):
iface = await device.demultiplexer.claim_interface(self, self.mux_interface, args)
spi_iface = SPIMasterInterface(iface, self.logger)
sx1272_iface = RadioSX1272Interface(spi_iface, self.logger)

return sx1272_iface

@classmethod
def add_interact_arguments(cls, parser):
p_role = parser.add_subparsers(dest="role", metavar="ROLE", required=True)
p_node = p_role.add_parser("node", help="LoRaWAN Node")
p_gateway = p_role.add_parser("gateway", help="LoRaWAN Gateway (Semtech UDP forwarder)")
ServerEndpoint.add_argument(p_node, "endpoint", default='tcp:localhost:9999')

def eui(value):
eui = int(value, 16)
return eui

def port(value):
value = int(value, 10)
if value not in range(1, 224):
raise argparse.ArgumentTypeError(
"invalid port: {} (choose from 1..223)".format(value))
return value

def add_common_args(p):
p.add_argument(
"--region", metavar="REGION", type=str, default="EU863870",
help="set the LoRaWAN region"
)
p.add_argument(
"--chn", metavar="CHN", type=int, default=0,
help="set the region channel to use"
)
p.add_argument(
"--data-rate", metavar="DATR", type=int, default=0,
choices=(range(0, 6)),
help="set the datarate"
)

add_common_args(p_node)
add_common_args(p_gateway)

p_node.add_argument(
"--dev-eui", metavar="DEVEUI", type=eui, required=True,
help="set the device EUI"
)
p_node.add_argument(
"--app-eui", metavar="APPEUI", type=eui, required=True,
help="set the application EUI"
)
p_node.add_argument(
"-K", "--app-key", metavar="APPKEY", type=eui, required=True,
help="set the application key"
)
p_node.add_argument(
"-P", "--tx-port", metavar="TXPORT", type=port, default=1,
help="Set the transmission port"
)
p_node.add_argument(
"-C", "--tx-confirmed", default=False, action="store_true",
help="Set to true if transmissions are confirmed"
)

p_gateway.add_argument(
"--gw-eui", metavar="GWEUI", type=eui, required=True,
help="set the gateway EUI"
)
p_gateway.add_argument(
"-S", "--gw-server", metavar="GWSRV", type=str, default="router.eu.thethings.network",
help="set the gateway server"
)
p_gateway.add_argument(
"-P", "--gw-port", metavar="GWPORT", type=int, default=1700,
help="set the gateway server port"
)
p_gateway.add_argument(
"--downlink-only", default=False, action="store_true",
help="retransmit application packets, do not listen to devices"
)
p_gateway.add_argument(
"--uplink-only", default=False, action="store_true",
help="retransmit nodes packets, do not listen to gateway"
)

async def _interact_socket(self, args, dev):
endpoint = await ServerEndpoint("socket", self.logger, args.endpoint)
while True:
try:
data = await asyncio.shield(endpoint.recv())
self.logger.info("Sending payload {}".format(data))
await dev.transmit(args.tx_port, data, args.tx_confirmed)
except asyncio.CancelledError:
pass

def node_frame_cb(self, fport, fpl):
self.logger.info("Received payload {} from port {}".format(fpl, fport))

async def interact(self, device, args, sx1272_iface):
region_params = {
"EU863870": EU863870_PARAMETERS
}[args.region]
sx1272 = SX1272_LoRa_Device_API(sx1272_iface, self.logger)

if args.role == "node":
dev = node = LoRaWAN_Node(sx1272, region_params, args.app_key, args.dev_eui, args.app_eui, self.logger, self.node_frame_cb)
elif args.role == "gateway":
dev = gw = LoRaWAN_Gateway(sx1272, region_params, args.gw_server, args.gw_port, args.gw_eui, args.downlink_only, args.uplink_only, self.logger)

await dev.configure_by_channel(args.chn, args.data_rate)

if args.role == "node":
joined = await node.join_network()
if not joined:
self.logger.error("Error joinning network")
else:
self.logger.info("Joined network")
await node.configure_by_channel(args.chn, args.data_rate)
await self._interact_socket(args, node)
elif args.role == "gateway":
await gw.main()

# -------------------------------------------------------------------------------------------------

class LoRaWANAppletTestCase(GlasgowAppletTestCase, applet=LoRaWANApplet):
@synthesis_test
def test_build(self):
self.assertBuilds()
188 changes: 188 additions & 0 deletions software/glasgow/applet/radio/sx1272/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,188 @@
# Reference: https://semtech.my.salesforce.com/sfc/p/#E0000000JelG/a/440000001NCE/v_VBhk1IolDgxwwnOpcS_vTFxPfSEPQbuneK3mWsXlU
# Accession: G00051

import math
import asyncio
import logging
import argparse
from time import sleep
from nmigen.compat import *

from ....support.logging import *
from ....support.bits import *
from ....support.endpoint import *
from ....arch.sx1272 import regs_common
from ....arch.sx1272 import regs_xxk
from ....arch.sx1272 import regs_lora
from ....arch.sx1272.apis import *
from ....protocol.lora import *
from ...interface.spi_master import SPIMasterSubtarget, SPIMasterInterface
from ... import *


class RadioSX1272Interface:
def __init__(self, interface, logger):
self.lower = interface
self._logger = logger
self._level = logging.DEBUG if self._logger.name == __name__ else logging.TRACE

def _log(self, message, *args):
self._logger.log(self._level, message, *args)

async def read_register_wide(self, address, length):
assert address in range(0x70 + 1)
await self.lower.write([regs_common.OP_R_REGISTER|address], hold_ss=True)
value = await self.lower.read(length)
self._log("read register [%02x]=<%s>", address, dump_hex(value))
return value

async def write_register_wide(self, address, value):
assert address in range(0x70 + 1)
self._log("write register [%02x]=<%s>", address, dump_hex(value))
await self.lower.write([regs_common.OP_W_REGISTER|address, *value])

async def read_register(self, address):
value, = await self.read_register_wide(address, 1)
return value

async def write_register(self, address, value):
await self.write_register_wide(address, [value])


class RadioSX1272Applet(GlasgowApplet, name="radio-sx1272"):
logger = logging.getLogger(__name__)
help = "transmit and receive using SX1272 RF PHY"
description = """
Transmit and receive packets using the SX1272 RF PHY.

This applet allows setting only LoRa modulation parameters, FSK/OOK
modem is not implemented in high level APIs. Transmit and receive
operations are implemented. The monitor command will listen and dump all
messages received while running.

For LoRaWAN operation, see radio-lorawan applet
"""

__pins = ("ss", "sck", "mosi", "miso")

@classmethod
def add_build_arguments(cls, parser, access):
access.add_build_arguments(parser)

access.add_pin_argument(parser, "ss", default=True)
access.add_pin_argument(parser, "mosi", default=True)
access.add_pin_argument(parser, "miso", default=True)
access.add_pin_argument(parser, "sck", default=True)

parser.add_argument(
"--spi-freq", metavar="FREQ", type=int, default=500,
help="set SPI frequency to FREQ kHz (default: %(default)s)")

def build(self, target, args):
self.mux_interface = iface = target.multiplexer.claim_interface(self, args)
pads = iface.get_pads(args, pins=self.__pins)
iface.add_subtarget(SPIMasterSubtarget(
pads=pads,
out_fifo=iface.get_out_fifo(),
in_fifo=iface.get_in_fifo(),
period_cyc=math.ceil(target.sys_clk_freq / (args.spi_freq * 1000)),
delay_cyc=math.ceil(target.sys_clk_freq / 1e6),
sck_idle=0,
sck_edge="rising",
ss_active=0,
))

async def run(self, device, args):
iface = await device.demultiplexer.claim_interface(self, self.mux_interface, args)
spi_iface = SPIMasterInterface(iface, self.logger)
sx1272_iface = RadioSX1272Interface(spi_iface, self.logger)

return sx1272_iface

@classmethod
def add_interact_arguments(cls, parser):
p_operation = parser.add_subparsers(dest="operation", metavar="OP", required=True)
p_receive = p_operation.add_parser("receive", help="receive packets")
p_monitor = p_operation.add_parser("monitor", help="monitor packets")
p_transmit = p_operation.add_parser("transmit", help="transmit packets")
p_socket = p_operation.add_parser("socket", help="connect tx to a socket")
ServerEndpoint.add_argument(p_socket, "endpoint", default='tcp:localhost:9999')

def payload(value):
pl = int(value, 16)
pl = pl.to_bytes(math.ceil(pl.bit_length()/8), 'little')
return pl

def freq(value):
f = float(value)
return f

parser.add_argument(
"-f", "--freq", metavar="FREQ", type=freq, required=True,
help="set the transmission frequency in MHz"
)
parser.add_argument(
"--bw", metavar="BW", type=int, default=125,
choices=(125, 250, 500),
help="set the bandwidth in kHz"
)
parser.add_argument(
"--crate", metavar="CODR", type=int, default=5,
choices=(5, 6, 7, 8),
help="set the coding rate (in 4/x)"
)
parser.add_argument(
"--sf", metavar="SF", type=int, default=12,
choices=(6, 7, 8, 9, 10, 11, 12),
help="set the spreading factor"
)

p_transmit.add_argument(
"--pwr", metavar="PWR", type=int, default=13,
choices=range(1, 14),
help="set the transmit power in dBm"
)
p_transmit.add_argument(
"--payload", metavar="PAYLOAD", type=payload, required=True,
help="set the payload to be sent"
)

async def _interact_socket(self, dev, endpoint):
endpoint = await ServerEndpoint("socket", self.logger, endpoint)
while True:
try:
data = await asyncio.shield(endpoint.recv())
await dev.transmit(data)
except asyncio.CancelledError:
pass

async def interact(self, device, args, sx1272_iface):
dev = SX1272_LoRa_Device_API(sx1272_iface, self.logger)

if args.operation != "transmit":
args.pwr = 13

await dev.configure(args.freq*1e6, args.bw*1e3, args.sf, args.pwr, args.crate)

if args.operation == "transmit":
await dev.transmit(args.payload)
self.logger.info("Packet sent")
if args.operation == "receive":
data, _, snr, rssi, codr = await dev.receive()
if data != None:
self.logger.info("Received packet: {} with SNR = {}, RSSI = {}, coding rate = {}".format(data, snr, rssi, codr))
else:
self.logger.error("No packet received")
if args.operation == "monitor":
def onpayload(data, crcerr, snr, rssi, codr):
self.logger.info("Received packet: {} with SNR = {}, RSSI = {}, coding rate = {}".format(data, snr, rssi, codr))
await dev.listen(onpayload)
if args.operation == "socket":
await self._interact_socket(dev, args.endpoint)

# -------------------------------------------------------------------------------------------------

class RadioSX1272AppletTestCase(GlasgowAppletTestCase, applet=RadioSX1272Applet):
@synthesis_test
def test_build(self):
self.assertBuilds()
Empty file.
Loading