Commit 7badfcdd authored by Dan Wilcox's avatar Dan Wilcox
Browse files

initial version commit

parents
# python
venv-baton
# macOS
.DS_Store
0.1.0: 2021 Jul 01
initial version from IM team hackathon
Simplified BSD License
Copyright (c) 2021 ZKM | Karlsruhe
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:
1. Redistributions of source code must retain the above copyright notice,
this list of conditions and the following disclaimer.
2. Redistributions in binary form must reproduce the above copyright notice,
this list of conditions and the following disclaimer in the documentation
and/or other materials provided with the distribution.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
baton
=====
Relay messages between UDP and a websocket. Faster together.
This code base has been developed by [ZKM | Hertz-Lab](https://zkm.de/en/about-the-zkm/organization/hertz-lab) as part of the project [»The Intelligent Museum«](#the-intelligent-museum).
Copyright (c) 2021 ZKM | Karlsruhe.
Copyright (c) 2021 Dan Wilcox.
BSD Simplified License.
Description
-----------
This script acts as a websocket relay server which creates a local websocket and forwards messages to/from a set of UDP ports. This is useful for creative coding tools which work with OSC (Open Sound Control) messages natively, but do not have built-in websocket support.
Dependencies
------------
* Python 3
* [simple-websocket-server](https://github.com/dpallot/simple-websocket-server)
Setup
-----
Install Python 3, if not already available:
```shell
brew install python3
```
Create a virtual environment for the script's dependencies and activate it:
```shell
python3 -m venv venv-baton
source venv-baton/bin/activate
```
Install the websocket server library via pip:
```shell
pip3 install git+https://github.com/dpallot/simple-websocket-server.git
```
Running
-------
Make sure to activate the virtual environment before the first run in a new commandline session:
source venv-baton/bin/activate
Next, start the server on the commandline via:
./baton.py
It can simply sit in the background and automatically handles the websocket connection. Websocket clients can then connect to send/receive messages whiel baton is active.
To configure the send/receive address and ports, see the commandline argument help for baton by running:
./baton.py -h
To stop baton, use CTRL+C to issue an interrupt signal. You need to do this a couple of times until it exits completely.
When finished, deactivate the virtual environment with:
deactivate
Example Clients
------------
A couple of example clients are included:
* html/client: HTML+JS websocket client, open index.html in your browser
* pd/osclient.pd: Pure Data patch which sends and receives OSC messages over UDP
Both examples should work together with the default address & ports on the same localhost:
pd/client.pd <-UDP-> baton.py <-WS-> html/client/index.html
First start baton, then start the clients.
To connect clients running on different computers, you may need to change the websocket and/or UDP address and port values.
The Intelligent Museum
----------------------
An artistic-curatorial field of experimentation for deep learning and visitor participation
The [ZKM | Center for Art and Media](https://zkm.de/en) and the [Deutsches Museum Nuremberg](https://www.deutsches-museum.de/en/nuernberg/information/) cooperate with the goal of implementing an AI-supported exhibition. Together with researchers and international artists, new AI-based works of art will be realized during the next four years (2020-2023). They will be embedded in the AI-supported exhibition in both houses. The Project „The Intelligent Museum” is funded by the Digital Culture Programme of the [Kulturstiftung des Bundes](https://www.kulturstiftung-des-bundes.de/en) (German Federal Cultural Foundation) and funded by the [Beauftragte der Bundesregierung für Kultur und Medien](https://www.bundesregierung.de/breg-de/bundesregierung/staatsministerin-fuer-kultur-und-medien) (Federal Government Commissioner for Culture and the Media).
As part of the project, digital curating will be critically examined using various approaches of digital art. Experimenting with new digital aesthetics and forms of expression enables new museum experiences and thus new ways of museum communication and visitor participation. The museum is transformed to a place of experience and critical exchange.
![Logo](media/Logo_ZKM_DMN_KSB.png)
#! /usr/bin/env python3
#
# Copyright (c) 2021 ZKM | Hertz-Lab
# Dan Wilcox <dan.wilcox@zkm.de>
#
# BSD Simplified License.
# For information on usage and redistribution, and for a DISCLAIMER OF ALL
# WARRANTIES, see the file, "LICENSE.txt," in this distribution.
#
# This code has been developed at ZKM | Hertz-Lab as part of „The Intelligent
# Museum“ generously funded by the German Federal Cultural Foundation.
#
# TODO:
# * add signal handler to exit gracefully? probably needs nonblocking io...
# * verbose event messaging instead of commenting print() out
# * replace SimpleWebSocketServer with websockets lib?
# * replace UDP code with asyncio versions
from SimpleWebSocketServer import SimpleWebSocketServer, WebSocket
import socket, threading, argparse
##### parser
parser = argparse.ArgumentParser(description="udp <-> websocket relay server")
parser.add_argument(
"--wshost", action="store", dest="wshost",
default="localhost", help="websocket host ie. ws://####:8081 default: localhost")
parser.add_argument(
"--wsport", action="store", dest="wsport",
default=8081, type=int, help="websocket port ie. ws://localhost:####, default: 8081")
parser.add_argument(
"--recvaddr", action="store", dest="recvaddr",
default="localhost", help="udp receive addr, default: localhost")
parser.add_argument(
"--recvport", action="store", dest="recvport",
default=6000, type=int, help="udp receive port, default: 6000")
parser.add_argument(
"--sendaddr", action="store", dest="sendaddr",
default='localhost', help="udp send port, default: localhost")
parser.add_argument(
"--sendport", action="store", dest="sendport",
default=9990, type=int, help="udp send addr, default: 9990")
##### UDP
# simple UDP sender socket wrapper
class UDPSender(object):
# init with address, port, and optional brodcast (aka multicast)
def __init__(self, address, port, broadcast=False):
self._sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
self._sock.setblocking(0)
if broadcast:
self._sock.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1)
self.address = address
self.port = port
# send raw data
def send(self, data):
self._sock.sendto(data, (self.address, self.port))
# simple UDP receiver socket wrapper
class UDPReceiver(object):
def __init__(self, port, address="localhost"):
self._sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
self._sock.bind((address, port))
self.running = True
def listenforever(self, wsserver):
print("udp: starting")
while self.running:
data, addr = self._sock.recvfrom(1024)
#print("udp: received ", data)
for client in wsserver.connections:
print(wsserver.connections[client], "sending")
wsserver.connections[client].sendMessage(data)
##### WebSocket
# websocket -> UDP relay implementation
class WSServer(WebSocket):
# setup UDP sender object
def __init__(self, server, sock, address):
WebSocket.__init__(self, server, sock, address)
self.udpsender = UDPSender(args.sendaddr, args.sendport)
# client connect callback
def handleConnected(self):
print("websocket: connected", self.address)
# simply relay raw messages to UDP client
def handleMessage(self):
#print("websocket: received ", self.data)
self.udpsender.send(self.data)
# client disconnect callback
def handleClose(self):
print("websocket: disconnected", self.address)
##### GO
args = parser.parse_args()
print(f"send -> udp {args.recvaddr}:{args.recvport} -> ws://{args.wshost}:{args.wsport}")
print(f"recv <- udp {args.sendaddr}:{args.sendport} <- ws://{args.wshost}:{args.wsport}")
# websocket server
wsserver = SimpleWebSocketServer(args.wshost, args.wsport, WSServer)
# UDP server
udpserver = UDPReceiver(args.recvport, args.recvaddr)
def listenUDP():
udpserver.listenforever(wsserver)
threading.Thread(target=listenUDP).start()
print("websocket: starting")
wsserver.serveforever()
<html>
<head>
<title>websocket client</title>
<meta charset="utf-8"/>
<meta name="title" content="websocket client">
<script src="js/reconnecting-websocket.min.js" type="text/javascript"></script>
<script src="js/osc-browser.min.js" type="text/javascript"></script>
<script src="js/script.js" type="text/javascript"></script>
</head>
<body>
<main>
<p> Received: <span id="message"></span></p>
<button type="button" onclick="sendMessage()">Send</button>
</main>
</body>
</html>
This diff is collapsed.
// https://github.com/joewalnes/reconnecting-websocket/
function ReconnectingWebSocket(a){function f(g){c=new WebSocket(a);if(b.debug||ReconnectingWebSocket.debugAll){console.debug("ReconnectingWebSocket","attempt-connect",a)}var h=c;var i=setTimeout(function(){if(b.debug||ReconnectingWebSocket.debugAll){console.debug("ReconnectingWebSocket","connection-timeout",a)}e=true;h.close();e=false},b.timeoutInterval);c.onopen=function(c){clearTimeout(i);if(b.debug||ReconnectingWebSocket.debugAll){console.debug("ReconnectingWebSocket","onopen",a)}b.readyState=WebSocket.OPEN;g=false;b.onopen(c)};c.onclose=function(h){clearTimeout(i);c=null;if(d){b.readyState=WebSocket.CLOSED;b.onclose(h)}else{b.readyState=WebSocket.CONNECTING;if(!g&&!e){if(b.debug||ReconnectingWebSocket.debugAll){console.debug("ReconnectingWebSocket","onclose",a)}b.onclose(h)}setTimeout(function(){f(true)},b.reconnectInterval)}};c.onmessage=function(c){if(b.debug||ReconnectingWebSocket.debugAll){console.debug("ReconnectingWebSocket","onmessage",a,c.data)}b.onmessage(c)};c.onerror=function(c){if(b.debug||ReconnectingWebSocket.debugAll){console.debug("ReconnectingWebSocket","onerror",a,c)}b.onerror(c)}}this.debug=false;this.reconnectInterval=1e3;this.timeoutInterval=2e3;var b=this;var c;var d=false;var e=false;this.url=a;this.readyState=WebSocket.CONNECTING;this.URL=a;this.onopen=function(a){};this.onclose=function(a){};this.onmessage=function(a){};this.onerror=function(a){};f(a);this.send=function(d){if(c){if(b.debug||ReconnectingWebSocket.debugAll){console.debug("ReconnectingWebSocket","send",a,d)}return c.send(d)}else{throw"INVALID_STATE_ERR : Pausing to reconnect websocket"}};this.close=function(){if(c){d=true;c.close()}};this.refresh=function(){if(c){c.close()}}}ReconnectingWebSocket.debugAll=false
// ref: https://github.com/colinbdclark/osc.js/
// ----- OSC -----
let oscPort = new osc.WebSocketPort({
url: "ws://localhost:8081",
metadata: true
})
oscPort.on("message", function (message) {
console.log("received osc: ", message)
document.getElementById("message").innerText = message.address + " " + JSON.stringify(message.args)
})
oscPort.open()
// ----- UI -----
function sendMessage() {
oscPort.send({
address: "/bar",
args: [
{type: "s", value: "helloworld"},
{type: "i", value: 1234},
{type: "f", value: 567.89}
]
})
}
<html>
<head>
<title>OSC relay client</title>
<meta charset="utf-8"/>
<meta name="title" content="">
<meta name="description" content="">
<meta name="keywords" content="">
<meta name="author" content="">
<script src="js/reconnecting-websocket.min.js" type="text/javascript"></script>
<script src="js/osc-browser.min.js" type="text/javascript"></script>
<script src="js/script.js" type="text/javascript"></script>
<link href='css/style.css' rel='stylesheet' type='text/css'>
<style>
#detected {display: none;}
</style>
</head>
<body>
<main>
<p>Language: <span id="langid">-1</span> <span id="langname">?</span></p>
<p id="detected">Listening...</p>
</main>
</body>
</html>
MIT License:
Copyright (c) 2010-2012, Joe Walnes
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
This diff is collapsed.
// https://github.com/joewalnes/reconnecting-websocket/
function ReconnectingWebSocket(a){function f(g){c=new WebSocket(a);if(b.debug||ReconnectingWebSocket.debugAll){console.debug("ReconnectingWebSocket","attempt-connect",a)}var h=c;var i=setTimeout(function(){if(b.debug||ReconnectingWebSocket.debugAll){console.debug("ReconnectingWebSocket","connection-timeout",a)}e=true;h.close();e=false},b.timeoutInterval);c.onopen=function(c){clearTimeout(i);if(b.debug||ReconnectingWebSocket.debugAll){console.debug("ReconnectingWebSocket","onopen",a)}b.readyState=WebSocket.OPEN;g=false;b.onopen(c)};c.onclose=function(h){clearTimeout(i);c=null;if(d){b.readyState=WebSocket.CLOSED;b.onclose(h)}else{b.readyState=WebSocket.CONNECTING;if(!g&&!e){if(b.debug||ReconnectingWebSocket.debugAll){console.debug("ReconnectingWebSocket","onclose",a)}b.onclose(h)}setTimeout(function(){f(true)},b.reconnectInterval)}};c.onmessage=function(c){if(b.debug||ReconnectingWebSocket.debugAll){console.debug("ReconnectingWebSocket","onmessage",a,c.data)}b.onmessage(c)};c.onerror=function(c){if(b.debug||ReconnectingWebSocket.debugAll){console.debug("ReconnectingWebSocket","onerror",a,c)}b.onerror(c)}}this.debug=false;this.reconnectInterval=1e3;this.timeoutInterval=2e3;var b=this;var c;var d=false;var e=false;this.url=a;this.readyState=WebSocket.CONNECTING;this.URL=a;this.onopen=function(a){};this.onclose=function(a){};this.onmessage=function(a){};this.onerror=function(a){};f(a);this.send=function(d){if(c){if(b.debug||ReconnectingWebSocket.debugAll){console.debug("ReconnectingWebSocket","send",a,d)}return c.send(d)}else{throw"INVALID_STATE_ERR : Pausing to reconnect websocket"}};this.close=function(){if(c){d=true;c.close()}};this.refresh=function(){if(c){c.close()}}}ReconnectingWebSocket.debugAll=false
// ref: https://github.com/colinbdclark/osc.js/
//document.getElementById("detected").style.display = "none"
// OSC
var oscPort = new osc.WebSocketPort({
url: "ws://localhost:8081",
metadata: true
})
oscPort.on("message", function (message) {
console.log("received osc: ", message)
if(message.address == "/detecting") {
if(message.args[0].value == 0) {
document.getElementById("detected").style.display = "none"
}
else {
document.getElementById("detected").style.display = "inline"
}
}
else if(message.address == "/lang") {
document.getElementById("langid").innerHTML = message.args[0].value
document.getElementById("langname").innerHTML = message.args[1].value
}
})
oscPort.open()
<html>
<head>
<title>OSC relay client</title>
<meta charset="utf-8"/>
<meta name="title" content="">
<meta name="description" content="">
<meta name="keywords" content="">
<meta name="author" content="">
<script src="js/osc-browser.min.js" type="text/javascript"></script>
<script src="js/script.js" type="text/javascript"></script>
<link href='style.css' rel='stylesheet' type='text/css'>
</head>
<body>
<main>
<div id="detecting">Listening...</div>
<div id="greeting">Hallo</div>
</main>
</body>
</html>
MIT License:
Copyright (c) 2010-2012, Joe Walnes
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
This diff is collapsed.
// https://github.com/joewalnes/reconnecting-websocket/
function ReconnectingWebSocket(a){function f(g){c=new WebSocket(a);if(b.debug||ReconnectingWebSocket.debugAll){console.debug("ReconnectingWebSocket","attempt-connect",a)}var h=c;var i=setTimeout(function(){if(b.debug||ReconnectingWebSocket.debugAll){console.debug("ReconnectingWebSocket","connection-timeout",a)}e=true;h.close();e=false},b.timeoutInterval);c.onopen=function(c){clearTimeout(i);if(b.debug||ReconnectingWebSocket.debugAll){console.debug("ReconnectingWebSocket","onopen",a)}b.readyState=WebSocket.OPEN;g=false;b.onopen(c)};c.onclose=function(h){clearTimeout(i);c=null;if(d){b.readyState=WebSocket.CLOSED;b.onclose(h)}else{b.readyState=WebSocket.CONNECTING;if(!g&&!e){if(b.debug||ReconnectingWebSocket.debugAll){console.debug("ReconnectingWebSocket","onclose",a)}b.onclose(h)}setTimeout(function(){f(true)},b.reconnectInterval)}};c.onmessage=function(c){if(b.debug||ReconnectingWebSocket.debugAll){console.debug("ReconnectingWebSocket","onmessage",a,c.data)}b.onmessage(c)};c.onerror=function(c){if(b.debug||ReconnectingWebSocket.debugAll){console.debug("ReconnectingWebSocket","onerror",a,c)}b.onerror(c)}}this.debug=false;this.reconnectInterval=1e3;this.timeoutInterval=2e3;var b=this;var c;var d=false;var e=false;this.url=a;this.readyState=WebSocket.CONNECTING;this.URL=a;this.onopen=function(a){};this.onclose=function(a){};this.onmessage=function(a){};this.onerror=function(a){};f(a);this.send=function(d){if(c){if(b.debug||ReconnectingWebSocket.debugAll){console.debug("ReconnectingWebSocket","send",a,d)}return c.send(d)}else{throw"INVALID_STATE_ERR : Pausing to reconnect websocket"}};this.close=function(){if(c){d=true;c.close()}};this.refresh=function(){if(c){c.close()}}}ReconnectingWebSocket.debugAll=false
// ref: https://github.com/colinbdclark/osc.js/
let greeting = ["...", "Hello", "Guten Tag", "Bonjour", "Hola"]
// OSC
var oscPort = new osc.WebSocketPort({
url: "ws://localhost:8081",
metadata: true
})
oscPort.on("message", function (message) {
console.log("received osc: ", message)
if(message.address == "/detecting") {
if(message.args[0].value == 0) {
document.getElementById("detecting").style.display = "none"
}
else {
document.getElementById("detecting").style.display = "inline"
}
}
else if(message.address == "/lang") {
document.getElementById("greeting").innerHTML = greeting[message.args[0].value]
document.getElementById("flag").innerHTML = flag[message.args[0].value]
}
})
oscPort.open()
body {
font-family: sans-serif;
margin: 0;
}
main {
width: 100%;
height: 100%;
/*background-color: red;*/
}
#detecting {
display: none;
position: fixed;
top: 1em;
left: 1em;
font-size: 12pt;
color: rgb(100, 200, 100);
}
#greeting {
transform: translateY(100%);
text-align: center;
font-size: 128pt;
color: rgb(64, 64, 64);
}
Supports Markdown
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment