diff --git a/dist/tools/coap-yolo/README.md b/dist/tools/coap-yolo/README.md new file mode 100644 index 000000000000..62db1ba564e5 --- /dev/null +++ b/dist/tools/coap-yolo/README.md @@ -0,0 +1,17 @@ +CoAP over YOLO Utils +==================== + +CoAP over YOLO is using the CoAP over WebSocket serialization but sending the +messages over UDP. The CoAP over WebSocket format depends on a reliable, +order-preserving, duplication-free message transport; which UDP clearly is not. +Hence the name YOLO. + +However, if the RIOT note is connected to a Linux host with a single reliable, +order-preserving and duplication-free link, this should work. + +This folder contains WebSocket to UDP proxy that forwards messages received +on the WebSocket to a UDP endpoint specified by the command line, and forwards +any replies received via UDP back to the WebSocket. This allows +exposing a CoAP over YOLO as a CoAP over WebSocket. With this you can use any +CoAP over WebSocket implementation such as e.g. `coap-client` from +[libcoap](https://libcoap.net/) to connect to CoAP over YOLO. diff --git a/dist/tools/coap-yolo/coap+ws2coap+yolo.py b/dist/tools/coap-yolo/coap+ws2coap+yolo.py new file mode 100755 index 000000000000..e5c42793c9b7 --- /dev/null +++ b/dist/tools/coap-yolo/coap+ws2coap+yolo.py @@ -0,0 +1,99 @@ +#!/usr/bin/python3 +""" +Bridge that translates CoAP over YOLO to CoAP over WebSocket. +""" + +import aiohttp +import aiohttp.web +import argparse +import asyncio +import sys + +udp_ep = None +udp_transport = None +ws = None + +class ForwardFromUdpProtocol: + """ + Forward received UDP datagrams via the currently connected WebSocket + """ + def connection_made(self, transport): + pass + + def datagram_received(self, data, addr): + global ws + if ws is not None: + asyncio.ensure_future(ws.send_bytes(data), loop=asyncio.get_event_loop()) + + +async def websocket_handler(request): + """ + Forward received WebSocket messages to the (statically) configured UDP + destination endpoint + """ + global udp_transport + global udp_ep + global ws + if ws is not None: + print("Someone already is connected") + return + ws = aiohttp.web.WebSocketResponse(protocols=("coap")) + print("WebSocket connection opened") + await ws.prepare(request) + + async for msg in ws: + if msg.type == aiohttp.WSMsgType.BINARY: + udp_transport.sendto(msg.data, udp_ep) + elif msg.type == aiohttp.WSMsgType.CLOSED: + udp_transport.sendto(b'', udp_ep) + ws = None + return + else: + print(f"Warning: Got unexpected WebSocket Message {msg}") + + udp_transport.sendto(b'', udp_ep) + ws = None + print("WebSocket connection closed") + + +async def ws2yolo(_udp_ep, ws_ep, udp_local_ep): + """ + Run a WebSocket 2 CoAP over YOLO bridge with the given endpoints + """ + global udp_transport + global udp_ep + udp_ep = _udp_ep + loop = asyncio.get_running_loop() + udp_transport, protocol = await loop.create_datagram_endpoint( + ForwardFromUdpProtocol, + local_addr=udp_local_ep) + + app = aiohttp.web.Application() + app.router.add_route('GET', '/.well-known/coap', websocket_handler) + runner = aiohttp.web.AppRunner(app) + await runner.setup() + site = aiohttp.web.TCPSite(runner) + await site.start() + await asyncio.Event().wait() + + +if __name__ == "__main__": + DESCRIPTION = "Forward WebSocket messages via UDP" + parser = argparse.ArgumentParser(description=DESCRIPTION) + parser.add_argument("--udp-host", default="::1", type=str, + help="UDP host to forward to") + parser.add_argument("--udp-port", default=1337, type=int, + help="UDP port to forward to") + parser.add_argument("--local-host", default=None, type=str, + help="UDP host to forward from") + parser.add_argument("--local-port", default=0, type=int, + help="UDP port to forward from") + parser.add_argument("--ws-host", default="::1", type=str, + help="WebSocket host to listen at") + parser.add_argument("--ws-port", default=8080, type=int, + help="WebSocket port to listen at") + + args = parser.parse_args() + asyncio.run(ws2yolo((args.udp_host, args.udp_port), + (args.ws_host, args.ws_port), + (args.local_host, args.local_port)))