|
|
@@ -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') |