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