Everynet operates a Neutral-Host Cloud RAN, which is agnostic to the LoRaWAN Network Server. Everynet's main product is carrier-grade coverage that can be connected to any LNS available on the market.
Everynet coverage is available via Everynet RAN Routing API that let customer to control message routing table (subscribe to devices). It also allows to send and receive LoRaWAN messages.
This API client is designed to simplify the use of Everynet RAN Routing API and serve as an example for other client implementations.
Before we start it is important to mention that Everynet RAN main functionality is LoRaWAN traffic routing.
The RAN receives messages from gateways and then matches each message with the customer using either DevAddr or pair (DevEUI, JoinEUI). The relations between device details and customer details are stored in a routing table.
DevEUI | JoinEUI | DevAddr | Customer |
---|---|---|---|
0x0..1 | 0x0..3 | 0x0..5 | ACME Inc. |
0x0..2 | 0x0..4 | 0x0..8 | ACME Inc. |
Everynet RAN Routing API is designed to let customer control such a rounting table to subscribe to the end device traffic.
It also provides both upstream and downstream messaging capabilities.
Cloud RAN does not store any device-related cryptographic keys and is not capable of decrypting customer traffic.
Maintaining data ownership gurantees without an access to the device keys the RAN enabled with a purpose-built MIC challenge procedure described in details in the RAN Routing API specification.
You can install the client with pip
, using following command:
$ pip install everynet
Here is a very quick walkthrough the client usage example to illustrate how easy it is to subscribe and get messages:
import asyncio
from ran.routing.core import Core
async def main():
async with Core(access_token="...", url="...") as ran:
# Create routing table record
device = await ran.routing_table.insert(dev_eui=0x7ABE1B8C93D7174F, join_eui=0x3CEDCF624F8B68F4)
# Will print "Device(dev_eui=8844537008791951183, join_eui=4390393232904382708, created_at=...)"
print(device)
# You can gather data about existed devices from routing_table any time.
devices = await ran.routing_table.select()
assert devices[0].dev_eui == device.dev_eui
# Create upstream connection and downstream connection
async with ran.upstream() as upstream, ran.downstream() as downstream:
async for upstream_message in upstream.stream():
# will print "UpstreamMessage(protocol_version=1, transaction_id=1, dev_euis=[8844537008791951183], ...)"
await print(upstream_message)
# Prepare downlink radio and tx_window
lora_modulation = domains.LoRaModulation(spreading=12, bandwidth=125000)
radio = domains.DownstreamRadio(frequency=868300000, lora=lora_modulation)
tx_window = domains.TransmissionWindow(radio=radio, delay=1)
# Send downlink to the device
await downstream.send_downstream(
transaction_id=1,
dev_eui=0x7ABE1B8C93D7174F,
tx_window=tx_window,
phy_payload=b"some-phy-payload-from-lns",
)
downstream_ack = await downstream.recv(timeout=1)
downstream_result = await downstream.recv(timeout=1)
# Deleting device from routing table
await ran.routing_table.delete(dev_euis=[device.dev_eui])
loop = asyncio.get_event_loop()
loop.run_until_complete(main())
It is possible to get access to the RAN routing table by using the ran.routing.core.Core.routing_table
attribute, but before that it is needed to connect client to the RAN Routing API.
It is needed to create ran.routing.core.Core
object to create connect client to the RAN Routing API.
Core
is main object of ran-routing client that allows to manage routing table, work with upstream and downstream messages.
Object constructor takes two mandatory parameters:
access_token
- RAN Routing API access token that can be obtained from Everynet supporturl
- RAN Routing API url. That URL refers to one of the available API instances, which provides traffic from different territories such as Brazil, Indonesia, USA, Italy, Spain, UK, ...
Note, that it is necessary to create multiple Core
objects if you want to gain access to the coverage in several territories simultaneously.
RAN Routing API connections need to be opened and closed manually. In order to simplify that Core
provides context manager, so it is possible to use "async with" statement with it. It will automatically open and close connections to the RAN Routing API.
from ran.routing.core import Core
async def main():
async with Core(access_token="...", url="...") as ran:
# do something with core
pass
Also, it is possible to manage connections via the connect()
and close()
methods:
ran.routing.core.Core.connect()
ran.routing.core.Core.close()
It is required to close connection after use.
from ran.routing.core import Core
async def main():
ran = Core(access_token="...", url="...")
await ran.connect()
# do something with core
await ran.close()
After establishing a connection with the RAN Routing API, you will gain access to the following attributes:
ran.routing.core.Core.routing_table
- routing table management interfaceran.routing.core.RoutingTable
ran.routing.core.Core.multicast_groups
- multicast groups management interfaceran.routing.core.MulticastGroupsManagement
ran.routing.core.Core.upstream
- upstream message streaming interfaceran.routing.core.UpstreamConnectionManager
ran.routing.core.Core.downstream
- downstream message streaming interfaceran.routing.core.DownstreamConnectionManager
These interfaces are discussed in the next chapters.
You can access routing table by using the ran.routing.core.Core.routing_table
attribute.
from ran.routing.core import Core
async with Core(access_token="...", url="...") as ran:
devices = await ran.routing_table.select()
It provides several methods for managing the RAN routing table:
ran.routing.core.RoutingTable.insert
- insert new device into the routing tableran.routing.core.RoutingTable.update
- update specitfic device in the routing tableran.routing.core.RoutingTable.delete
- delete device from the routing tableran.routing.core.RoutingTable.delete_all
- delete all devices from the routing tableran.routing.core.RoutingTable.select
- fetch device information from the routing table
The methods are explained in details below...
Method ran.routing.core.RoutingTable.select
fetches existing devices from the routing table. It returns list of ran.routing.core.domains.Device
DTO-objects.
from ran.routing.core import Core
async with Core(access_token="...", url="...") as ran:
# You can select devices by any of the following ways:
# Select all devices (will return list of all devices)
devices = await ran.routing_table.select()
# Select devices with pagination
devices = await ran.routing_table.select(offset=10, limit=5)
# Select devices by dev_euis (will return list of devices with given dev_euis)
devices = await ran.routing_table.select(
dev_euis=[0x7abe1b8c93d7174f, 0x7bbe1b8c93d7174a],
)
# Pagination also works with passed dev_euis
devices = await ran.routing_table.select(
dev_euis=[0x7abe1b8c93d7174f, 0x7bbe1b8c93d7174a],
offset=1,
limit=1
)
Method ran.routing.core.RoutingTable.insert
inserts new device into the routing table. It returns ran.routing.core.domains.Device
DTO-object that contais information about newly created device.
Both dev_eui
and dev_addr
are mandatory parameters for ABP devices, while dev_eui
and join_eui
are mandatory parameters for OTAA devices.
Provided dev_eui
must be unique, while single dev_addr
may be assigned to several dev_eui
's simultaneously.
from ran.routing.core import Core
async with Core(access_token="...", url="...") as ran:
# Creating OTAA-device
device = await ran.routing_table.insert(
dev_eui=0x7abe1b8c93d7174f,
join_eui=0x3cedcf624f8b68f4
)
# Creating ABP-device
device = await ran.routing_table.insert(
dev_eui=0x7abe1b8c93d7174f,
dev_addr=0x627bb8bb
)
Updating device in the routing table is performed via the ran.routing.core.RoutingTable.update
method.
Update procedure changes dev_addr
for an existed device in the routing table, where device is referred by the device's dev_eui
.
This method is intended to be used by the client to set the new device address after the join request has been processed.
Updating of the dev_addr
is not allowed for ABP devices.
from ran.routing.core import Core
async with Core(access_token="...", url="...") as ran:
# Updating device's dev_addr
device = await ran.routing_table.update(
dev_eui=0x7abe1b8c93d7174f,
join_eui=0x3cedcf624f8b68f4,
active_dev_addr=0x627bb8bc
)
Deleting devices from the routing table is performed via ran.routing.core.RoutingTable.delete
method. It is needed to provide the list of dev_eui
selected for deletion.
from ran.routing.core import Core
async with Core(access_token="...", url="...") as ran:
# Updating device's dev_addr
device = await ran.routing_table.delete(
dev_euis=[0x7abe1b8c93d7174f, 0x7bbe1b8c93d7174a],
)
It is also possible to delete all devices in the routing table by calling ran.routing.core.RoutingTable.delete_all
method.
from ran.routing.core import Core
async with Core(access_token="...", url="...") as ran:
# Updating device's dev_addr
device = await ran.routing_table.delete_all()
It is possible to get access to the RAN multicast groups by using the ran.routing.core.Core.multicast_groups
attribute, but before that it is needed to connect client to the RAN Routing API.
It provides several methods for managing the RAN routing table:
ran.routing.core.MulticastGroupsManagement.create_multicast_group
- create new multicast groupran.routing.core.MulticastGroupsManagement.update_multicast_group
- update specific device multicast groupran.routing.core.MulticastGroupsManagement.get_multicast_groups
- fetch multicast groupsran.routing.core.MulticastGroupsManagement.delete_multicast_groups
- delete multicast groupran.routing.core.MulticastGroupsManagement.add_device_to_multicast_group
- add device to multicast groupran.routing.core.MulticastGroupsManagement.remove_device_from_multicast_group
- remove device from multicast group
The methods are explained in details below...
New multicast group can be created with ran.routing.core.MulticastGroupsManagement.create_multicast_group
method. This method requires group name and group address. It returns ran.routing.core.domains.MulticastGroup
DTO-object that contains information about newly created multicast group.
from ran.routing.core import Core
async with Core(access_token="...", url="...") as ran:
multicast_group = await ran.multicast_groups.create_multicast_group(
name="test-multicast-group",
addr=0xef046a1e
)
Method ran.routing.core.MulticastGroupsManagement.get_multicast_groups
fetches existing multicast groups from the routing table. It returns list of ran.routing.core.domains.MulticastGroup
DTO-objects. You can select all groups (if no args provided), or specific groups, by passing groups addresses into this method.
from ran.routing.core import Core
async with Core(access_token="...", url="...") as ran:
# Fetching all groups
all_multicast_groups = await ran.multicast_groups.get_multicast_groups()
# Fetching one group (also returns list, but with one element)
multicast_groups = await ran.multicast_groups.get_multicast_groups(0xef046a1e)
# Fetching specific groups
multicast_groups = await ran.multicast_groups.get_multicast_groups(0xef046a1e, 0xffed8719)
Method ran.routing.core.MulticastGroupsManagement.update_multicast_group
can perform multicast group update procedure.
You can update addr
(address) or name
of the multicast group (or both).
Update procedure changes fields for an existed multicast group, where multicast group is referred by the it's addr
(address). You need to provide at least one of new fields to make update.
from ran.routing.core import Core
async with Core(access_token="...", url="...") as ran:
# Updating name
multicast_group = await ran.multicast_groups.update_multicast_group(
addr=0xef046a1e,
new_name="new-multicast-group-name"
)
# Updating addr
multicast_group = await ran.multicast_groups.update_multicast_group(
addr=0xef046a1e,
new_addr=0xffffffff,
)
# Updating all fields
multicast_group = await ran.multicast_groups.update_multicast_group(
addr=0xef046a1e,
new_addr=0xffffffff,
new_name="new-multicast-group-name"
)
Devices can be added or removed from multicast group by using:
ran.routing.core.MulticastGroupsManagement.add_device_to_multicast_group
- for adding new device into multicast group.ran.routing.core.MulticastGroupsManagement.remove_device_from_multicast_group
- for removing existed device from multicast group.
Only devices, already added into routing table can be added into multicast group.
Only devices, already added into multicast group, can be removed from it.
from ran.routing.core import Core
async with Core(access_token="...", url="...") as ran:
# We need to create device first, before adding it into multicast group.
device = await ran.routing_table.insert(
dev_eui=0x7abe1b8c93d7174f,
dev_addr=0x627bb8bb
)
# Creating multicast group
multicast_group = await ran.multicast_groups.create_multicast_group(
name="test-multicast-group",
addr=0xef046a1e
)
# Adding device into multicast group
multicast_group = await ran.multicast_groups.add_device_to_multicast_group(
addr=multicast_group.addr,
dev_eui=device.dev_eui,
)
# Removing device from multicast group
multicast_group = await ran.multicast_groups.remove_device_from_multicast_group(
addr=multicast_group.addr,
dev_eui=device.dev_eui,
)
Deleting multicast group is performed via ran.routing.core.RoutingTable.delete_multicast_groups
method. It is needed to provide the list of addr
of multicast groups, selected for deletion.
from ran.routing.core import Core
async with Core(access_token="...", url="...") as ran:
multicast_group = await ran.multicast_groups.delete_multicast_groups(
addrs=[0xef046a1e],
)
It is needed to connect to the RAN Upstream API to start getting upstream messages.
Connection is established by creating of the ran.routing.core.UpstreamConnection
object, which is managed by ran.routing.core.UpstreamConnectionManager
class.
It is possible to access to UpstreamConnectionManager
object by using Core
attribute ran.routing.core.Core.upstream
.
from ran.routing.core import Core
async with Core(access_token="...", url="...") as ran:
async with ran.upstream() as upstream_connection:
# do something with upstream connection
pass
Preferred way to use UpstreamConnection
is via context-manager. This context manager will automatically close websocket connection and stop all underlying tasks, when context exited.
In case of the high number of messages it is possible to create multiple UpstreamConnection
objects, for the load balancing purposes. In this case each connection will receive unique messages from the RAN in random order.
Each UpstreamConnection
object uses the same TCP connection pool, that is managed by the Core
object. Once the Core
object is closed, all underlying UpstreamConnection
objects will be closed as well.
It is also possible to manage upstream connection state manually by instanciating of the UpstreamConnection
object manually via ran.routing.core.UpstreamConnectionManager.create_connection
method.
In this case you need to close connection manually as well. Unclosed connection may cause memory leak and data loss.
from ran.routing.core import Core
async with Core(access_token="...", url="...") as ran:
upstream_connection = await ran.upstream.create_connection()
# do something with upstream connection
pass
# Closing upstream connection, this operation is instant and will not block
upstream_connection.close()
# Waiting for closing upstream connection, this operation is blocking and will return when upstream connection is closed
await upstream_connection.wait_closed()
The main method, to receive upstream message is via the ran.routing.core.UpstreamConnection.stream()
method.
This method returns async iterator, which will yield received upstream messages.
Here is how to receive upstream messages:
from ran.routing.core import Core
async with Core(access_token="...", url="...") as ran:
async with ran.upstream() as upstream_connection:
async for upstream_message in upstream_connection.stream():
await handle_message(upstream_message)
Each upstream message is ran.routing.core.domains.UpstreamMessage
DTO-object. It contains all data, defined by RAN Routing API specification.
It is required by the RAN Routing API to acknowledge all upstream messages.
Please use UpstreamConnection
to send either UpstreamAck
or UpstreamReject
messages back to RAN:
ran.routing.core.UpstreamConnection.send_upstream_ack
sends UpstreamAck message.ran.routing.core.UpstreamConnection.send_upstream_reject
sends UpstreamReject message.
Following example has both UpstreamAck and UpstreamReject cases:
from ran.routing.core import Core
async with Core(access_token="...", url="...") as ran:
async with ran.upstream() as upstream_connection:
async for message in upstream_connection.stream():
try:
# Assuming This function handles MIC challenge and returns device EUI and correct MIC
# If MIC is incorrect, exception "MicChallengeError" will be raised
dev_eui, mic = await handle_message(message)
except MicChallengeError:
# We have could not solve MIC challenge, and now we need to send UpstreamReject message
await upstream_connection.send_upstream_reject(
transaction_id=message.transaction_id,
result_code=domains.UpstreamRejectResultCode.MICFailed
)
else:
# Everything is OK, so we can send correct ACK message
await upstream_connection.send_upstream_ack(
transaction_id=message.transaction_id,
dev_eui=dev_eui,
mic=mic,
)
Downstream API is pretty similar to the Upstream API.
Connection to the downstream API is established via creation of the ran.routing.core.DownstreamConnection
object, which is managed by the ran.routing.core.DownstreamConnectionManager
class.
It is possible to get access to the DownstreamConnectionManager
object by using Сore
attribute ran.routing.core.Core.downstream
.
from ran.routing.core import Core
async with Core(access_token="...", url="...") as ran:
async with ran.downstream() as downstream_connection:
# do something with downstream connection
pass
# or
async with Core(access_token="...", url="...") as ran:
downstream_connection = await ran.downstream.create_connection()
# do something with downstream connection here
pass
# Closing downstream connection, this operation is instant and will not block
downstream_connection.close()
# Waiting for closing downstream connection, this operation is blocking and will return when downstream connection is closed
await downstream_connection.wait_closed()
The main purpose of the DownstreamConnection
is to send messages back to devices via Downstream API. It can be done, using the ran.routing.core.DownstreamConnection.send_downstream
method. This method allows to send any type of message to device: downlinks, join-accepts, etc.
This method requires parameters, which are described in the RAN Routing API specification.
from ran.routing.core import Core
async with Core(access_token="...", url="...") as ran:
async with ran.downstream() as downstream_connection:
# do something with downstream connection
await downstream_connection.send_downstream(
# Client code must provide unique transaction_id, which will be sent in DownstreamAck and DownstreamResult messages.
transaction_id=next(counter),
# Device identifier.
dev_eui=dev_eui,
# Transmission window object, check example below.
tx_window=make_tx_window(),
# bytes of phy_payload, produced by network server.
phy_payload=phy_payload,
# Optional field. Must be provided, if this is join-response message.
# Used to automatically update device address in ran-routing table after join accept will be handled by device.
target_dev_addr=dev_addr_after_join,
)
To tell the downstream API when and how to send downstream message, you need to provide tx_window
parameter.
It may be instance of ran.routing.core.domains.TransmissionWindow
or dict object with same structure.
# TODO: We can come up with a better example in future...
def make_tx_window():
# It contains two parts: radio parameters and transmission window parameters.
# First, we need to create radio object
lora_modulation = domains.LoRaModulation(spreading=12, bandwidth=125000)
radio = domains.DownstreamRadio(frequency=868300000, lora=lora_modulation)
# Second part is transmission window parameters.
# You can provide one of following values:
# "delay" (for class A downstream)
# "tmms" (for class B downstream)
# "deadline" (for class C downstream)
tx_window = domains.TransmissionWindow(radio=radio, delay=1)
return tx_window
To obtain info messages from DownstreamConnection
, you can use interface similar to the UpstreamConnection
.
It is possible gain access to downstream messages by using ran.routing.core.DownstreamConnection.stream
method.
from ran.routing.core import Core
async with Core(access_token="...", url="...") as ran:
async with ran.downstream() as downstream_connection:
async for downstream_message in downstream_connection.stream():
await handle_downstream_message(downstream_message)
Downstream messages, obtained this way, can be of different types:
ran.routing.core.domains.DownstreamAckMessage
- downstream API server has received message, and scheduled it for processingran.routing.core.domains.DownstreamResultMessage
- downstream API server has finished processing of the message
Both of these messages have transaction_id
field, which is used to identify message. It is similar to the transaction_id
, you send in Downstream messages.
Multicast messages can be sent with same DownstreamConnection
, which used for regular downlink messages.
You can send multicast downlinks, by using the ran.routing.core.DownstreamConnection.send_multicast_downstream
method.
This method requires parameters, which are described in the RAN Routing API specification.
This parameters are pretty same, as in regular send_downstream
downlinks, but instead of dev_eui
of device using addr
of multicast group as target, and don't support target_dev_addr
field.
from ran.routing.core import Core
async with Core(access_token="...", url="...") as ran:
async with ran.downstream() as downstream_connection:
# do something with downstream connection
await downstream_connection.send_multicast_downstream(
# Client code must provide unique transaction_id, which will be sent in DownstreamAck and DownstreamResult messages.
transaction_id=next(counter),
# Multicast group address
addr=multicast_group_addr,
# Transmission window object, check example above.
tx_window=make_tx_window(),
# bytes of phy_payload, produced by network server.
phy_payload=phy_payload,
)
You need to use same transaction_id
counter for multicast downlinks, you use for regular downlinks.
You will receive DownstreamAckMessage
and DownstreamResultMessage
for multicast downstream, as for regular downlink.