Browse Source

start of the lorenode initiator

mbed-sx1276
John-Mark Gurney 3 years ago
parent
commit
ed0d08b126
1 changed files with 246 additions and 0 deletions
  1. +246
    -0
      lora.py

+ 246
- 0
lora.py View File

@@ -0,0 +1,246 @@
import asyncio
import functools
import os
import unittest

from Strobe.Strobe import Strobe
from Strobe.Strobe import AuthenticationFailed

domain = b'com.funkthat.lora.irrigation.shared.v0.0.1'

# Response to command will be the CMD and any arguments if needed.
# The command is encoded as an unsigned byte
CMD_TERMINATE = 1 # no args: terminate the sesssion, reply confirms

# The follow commands are queue up, but will be acknoledged when queued
CMD_WAITFOR = 2 # arg: (length): waits for length seconds
CMD_RUNFOR = 3 # arg: (chan, length): turns on chan for length seconds

class LORANode(object):
'''Implement a LORANode initiator.'''

def __init__(self, syncdatagram):
self.sd = syncdatagram
self.st = Strobe(domain)

async def start(self):
msg = self.st.send_enc(b'reqreset' + os.urandom(16)) + \
self.st.send_mac(8)

resp = await self.sd.sendtillrecv(msg, 1)

self.st.recv_enc(resp[:16])
self.st.recv_mac(resp[16:])

resp = await self.sd.sendtillrecv(
self.st.send_enc(b'confirm') + self.st.send_mac(8), 1)

pkt = self.st.recv_enc(resp[:9])
self.st.recv_mac(resp[9:])

if pkt != b'confirmed':
raise RuntimeError

@staticmethod
def _encodeargs(*args):
r = []
for i in args:
r.append(i.to_bytes(4, byteorder='little'))

return b''.join(r)

async def _sendcmd(self, cmd, *args):
cmdbyte = cmd.to_bytes(1, byteorder='little')
pkt = await self.sd.sendtillrecv(
self.st.send_enc(cmdbyte +
self._encodeargs(*args)) + self.st.send_mac(8), 1)

resp = self.st.recv_enc(pkt[:-8])
self.st.recv_mac(pkt[-8:])

if resp[0:1] != cmdbyte:
raise RuntimeError('response does not match, got: %s, expected: %s' % (repr(resp[0:1]), repr(cmdbyte)))

async def waitfor(self, length):
return await self._sendcmd(CMD_WAITFOR, length)

async def runfor(self, chan, length):
return await self._sendcmd(CMD_RUNFOR, chan, length)

async def terminate(self):
return await self._sendcmd(CMD_TERMINATE)

class SyncDatagram(object):
'''Base interface for a more simple synchronous interface.'''

def __init__(self): #pragma: no cover
pass

async def recv(self, timeout=None): #pragma: no cover
'''Receive a datagram. If timeout is not None, wait that many
seconds, and if nothing is received in that time, raise an TimeoutError
exception.'''

raise NotImplementedError

async def send(self, data): #pragma: no cover
'''Send a datagram.'''

raise NotImplementedError

async def sendtillrecv(self, data, freq):
'''Send the datagram in data, every freq seconds until a datagram
is received. If timeout seconds happen w/o receiving a datagram,
then raise an TimeoutError exception.'''

while True:
await self.send(data)
try:
return await self.recv(freq)
except TimeoutError:
pass

class MockSyncDatagram(SyncDatagram):
'''A testing version of SyncDatagram. Define a method runner which
implements part of the sequence. In the function, await on either
self.get, to wait for the other side to send something, or await
self.put w/ data to send.'''

def __init__(self):
self.sendq = asyncio.Queue()
self.recvq = asyncio.Queue()
self.task = None
self.task = asyncio.create_task(self.runner())

self.get = self.sendq.get
self.put = self.recvq.put

async def drain(self):
'''Wait for the runner thread to finish up.'''

return await self.task

async def runner(self): #pragma: no cover
raise NotImplementedError

async def recv(self, timeout=None):
return await self.recvq.get()

async def send(self, data):
return await self.sendq.put(data)

def __del__(self): #pragma: no cover
if self.task is not None and not self.task.done():
self.task.cancel()

class TestSyncData(unittest.IsolatedAsyncioTestCase):
async def test_syncsendtillrecv(self):
class MySync(SyncDatagram):
def __init__(self):
self.sendq = []
self.resp = [ TimeoutError(), b'a' ]

async def recv(self, timeout=None):
assert timeout == 1
r = self.resp.pop(0)
if isinstance(r, Exception):
raise r

return r

async def send(self, data):
self.sendq.append(data)

ms = MySync()

r = await ms.sendtillrecv(b'foo', 1)

self.assertEqual(r, b'a')
self.assertEqual(ms.sendq, [ b'foo', b'foo' ])

def timeout(timeout):
def timeout_wrapper(fun):
@functools.wraps(fun)
async def wrapper(*args, **kwargs):
return await asyncio.wait_for(fun(*args, **kwargs),
timeout)

return wrapper

return timeout_wrapper

class TestLORANode(unittest.IsolatedAsyncioTestCase):
@timeout(2)
async def test_lora(self):
class TestSD(MockSyncDatagram):
async def runner(self):
l = Strobe(domain)

# start handshake
r = await self.get()

pkt = l.recv_enc(r[:-8])
l.recv_mac(r[-8:])

assert pkt.startswith(b'reqreset')

await self.put(l.send_enc(os.urandom(16)) +
l.send_mac(8))

r = await self.get()
c = l.recv_enc(r[:-8])
l.recv_mac(r[-8:])

assert c == b'confirm'

await self.put(l.send_enc(b'confirmed') +
l.send_mac(8))

r = await self.get()
cmd = l.recv_enc(r[:-8])
l.recv_mac(r[-8:])

assert cmd[0] == CMD_WAITFOR
assert int.from_bytes(cmd[1:], byteorder='little') == 30

await self.put(l.send_enc(cmd[0:1]) +
l.send_mac(8))

r = await self.get()
cmd = l.recv_enc(r[:-8])
l.recv_mac(r[-8:])

assert cmd[0] == CMD_RUNFOR
assert int.from_bytes(cmd[1:5], byteorder='little') == 1
assert int.from_bytes(cmd[5:], byteorder='little') == 50

await self.put(l.send_enc(cmd[0:1]) +
l.send_mac(8))

r = await self.get()
cmd = l.recv_enc(r[:-8])
l.recv_mac(r[-8:])

assert cmd[0] == CMD_TERMINATE

await self.put(l.send_enc(cmd[0:1]) +
l.send_mac(8))

tsd = TestSD()
l = LORANode(tsd)

await l.start()

await l.waitfor(30)

await l.runfor(1, 50)

await l.terminate()

await tsd.drain()

# Make sure all messages have been processed
self.assertTrue(tsd.sendq.empty())
self.assertTrue(tsd.recvq.empty())

print('done')

Loading…
Cancel
Save