Initial commit
This commit is contained in:
2
.gitignore
vendored
Normal file
2
.gitignore
vendored
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
__pycache__
|
||||||
|
*.swp
|
||||||
114
client.py
Normal file
114
client.py
Normal file
@@ -0,0 +1,114 @@
|
|||||||
|
''' Module that implements OSC '''
|
||||||
|
|
||||||
|
import asyncio
|
||||||
|
import struct
|
||||||
|
|
||||||
|
_TASK = None
|
||||||
|
_Q = asyncio.Queue()
|
||||||
|
|
||||||
|
def __string_to_osc(s: str) -> bytes:
|
||||||
|
''' Convert python string to OSC-string '''
|
||||||
|
data = s.encode('ascii')
|
||||||
|
to_append = 4 - (len(data) % 4)
|
||||||
|
data += b'\0' * to_append
|
||||||
|
return data
|
||||||
|
|
||||||
|
async def __serve() -> None:
|
||||||
|
''' Task that manages OSC sending '''
|
||||||
|
# log
|
||||||
|
print('[I] OSC: task is running')
|
||||||
|
# task loop
|
||||||
|
while True:
|
||||||
|
# get value from queue
|
||||||
|
val = await _Q.get()
|
||||||
|
# stop
|
||||||
|
if val == 0:
|
||||||
|
break
|
||||||
|
# host, port, OSC address and data
|
||||||
|
host = val['host']
|
||||||
|
port = val['port']
|
||||||
|
addr = val['addr']
|
||||||
|
data = val['data']
|
||||||
|
# OSC type tag string
|
||||||
|
type_tag = ','
|
||||||
|
# OSC data to append to the final packet
|
||||||
|
osc_data = bytes()
|
||||||
|
# check if data is correct
|
||||||
|
for d in data:
|
||||||
|
d_type = type(d)
|
||||||
|
# process the value
|
||||||
|
if d_type is int:
|
||||||
|
type_tag += 'i'
|
||||||
|
osc_data += struct.pack('>i', d)
|
||||||
|
elif d_type is str:
|
||||||
|
type_tag += 's'
|
||||||
|
osc_data += __string_to_osc(d)
|
||||||
|
# unsupported
|
||||||
|
else:
|
||||||
|
print('[!] OSC: unsupported data type was provided! The packet is discarded.')
|
||||||
|
print(' * host: %s' % host)
|
||||||
|
print(' * port: %s' % port)
|
||||||
|
print(' * address: %s' % addr)
|
||||||
|
print(' * data: %s' % data)
|
||||||
|
print(' (bad type is %s)' % d_type)
|
||||||
|
type_tag = None
|
||||||
|
break
|
||||||
|
# no type tag
|
||||||
|
if type_tag is None:
|
||||||
|
continue
|
||||||
|
# convert type tag to OSC-string
|
||||||
|
type_tag = __string_to_osc(type_tag)
|
||||||
|
# create OSC packet
|
||||||
|
packet = __string_to_osc(addr) + type_tag + osc_data
|
||||||
|
|
||||||
|
# send the packet
|
||||||
|
trans, prot = await asyncio.get_running_loop().create_datagram_endpoint(
|
||||||
|
asyncio.DatagramProtocol,
|
||||||
|
remote_addr=(host, port)
|
||||||
|
)
|
||||||
|
try:
|
||||||
|
trans.sendto(packet)
|
||||||
|
print('[I] OSC: sent data to %s:%s' % (host, port))
|
||||||
|
except:
|
||||||
|
print('[!] OSC: failed to send to %s:%s!' % (host, port))
|
||||||
|
print(' * address: %s' % addr)
|
||||||
|
print(' * data: %s' % data)
|
||||||
|
finally:
|
||||||
|
trans.close()
|
||||||
|
|
||||||
|
# log
|
||||||
|
print('[I] OSC: task is stopped')
|
||||||
|
|
||||||
|
async def start() -> None:
|
||||||
|
''' Starts OSC task '''
|
||||||
|
global _TASK
|
||||||
|
# already started
|
||||||
|
if _TASK is not None:
|
||||||
|
return
|
||||||
|
_TASK = asyncio.create_task(__serve())
|
||||||
|
|
||||||
|
async def stop() -> None:
|
||||||
|
''' Stops OSC task '''
|
||||||
|
global _TASK
|
||||||
|
# not started
|
||||||
|
if _TASK is None:
|
||||||
|
return
|
||||||
|
# stop
|
||||||
|
await _Q.put(0)
|
||||||
|
await _TASK
|
||||||
|
# reset
|
||||||
|
_TASK = None
|
||||||
|
|
||||||
|
async def send(host: str, port: str, addr: str, values: list) -> None:
|
||||||
|
''' Schedule packet transmission '''
|
||||||
|
# not started
|
||||||
|
if _TASK is None:
|
||||||
|
return
|
||||||
|
# create
|
||||||
|
d = {
|
||||||
|
'host': host,
|
||||||
|
'port': port,
|
||||||
|
'addr': addr,
|
||||||
|
'data': values
|
||||||
|
}
|
||||||
|
await _Q.put(d)
|
||||||
98
parser.py
Normal file
98
parser.py
Normal file
@@ -0,0 +1,98 @@
|
|||||||
|
''' Parse OSC packet into dict '''
|
||||||
|
|
||||||
|
# set True to enable log messages
|
||||||
|
DEBUG = True
|
||||||
|
|
||||||
|
import struct
|
||||||
|
import traceback
|
||||||
|
|
||||||
|
def _parse_str(body: bytearray) -> (str, int):
|
||||||
|
''' Parse OSC string '''
|
||||||
|
try:
|
||||||
|
result = ''
|
||||||
|
for b in body:
|
||||||
|
if b == 0:
|
||||||
|
break
|
||||||
|
result += chr(b)
|
||||||
|
else:
|
||||||
|
return None
|
||||||
|
l = ((len(result) // 4) + 1) * 4
|
||||||
|
if l > len(body):
|
||||||
|
return None
|
||||||
|
return (result, l)
|
||||||
|
except:
|
||||||
|
if DEBUG: traceback.print_exc()
|
||||||
|
return None
|
||||||
|
|
||||||
|
def _parse_int(body: bytearray) -> (int, int):
|
||||||
|
''' Parse OSC int '''
|
||||||
|
try:
|
||||||
|
return (struct.unpack('>i', body[0:4])[0], 4)
|
||||||
|
except:
|
||||||
|
if DEBUG: traceback.print_exc()
|
||||||
|
return None
|
||||||
|
|
||||||
|
def _parse_float(body: bytearray) -> (float, int):
|
||||||
|
''' Parse OSC float '''
|
||||||
|
try:
|
||||||
|
return (struct.unpack('>f', body[0:4])[0], 4)
|
||||||
|
except:
|
||||||
|
if DEBUG: traceback.print_exc()
|
||||||
|
return None
|
||||||
|
|
||||||
|
def parse(body: bytearray) -> dict | None:
|
||||||
|
''' Parse OSC datagram into dict. '''
|
||||||
|
# too short
|
||||||
|
if len(body) < 8:
|
||||||
|
if DEBUG: print('[OSC PARSER] Body is shorter than 8 bytes')
|
||||||
|
return None
|
||||||
|
# does not start with slash
|
||||||
|
if body[0] != ord('/'):
|
||||||
|
if DEBUG: print('[OSC PARSER] First byte of packet is not forward slash')
|
||||||
|
return None
|
||||||
|
# get the address
|
||||||
|
addr = _parse_str(body)
|
||||||
|
if not addr:
|
||||||
|
if DEBUG: print('[OSC PARSER] Failed to parse address (OSC-string)')
|
||||||
|
return None
|
||||||
|
# edit body
|
||||||
|
body = body[addr[1]:]
|
||||||
|
# get the type tag
|
||||||
|
type_tag = _parse_str(body)
|
||||||
|
if not type_tag:
|
||||||
|
if DEBUG: print('[OSC PARSER] Failed to parse type tag (OSC-string)')
|
||||||
|
return None
|
||||||
|
# edit body
|
||||||
|
body = body[type_tag[1]:]
|
||||||
|
# type is invalid
|
||||||
|
if type_tag[0][0] != ',':
|
||||||
|
if DEBUG: print('[OSC PARSER] The first symbol of type tag must be comma')
|
||||||
|
return None
|
||||||
|
# arguments
|
||||||
|
args = []
|
||||||
|
# type parsers
|
||||||
|
type_parsers = {
|
||||||
|
'i': _parse_int,
|
||||||
|
'f': _parse_float,
|
||||||
|
's': _parse_str
|
||||||
|
}
|
||||||
|
# parse the arguments
|
||||||
|
for arg_type in type_tag[0][1:]:
|
||||||
|
# parse body if parser exists
|
||||||
|
if arg_type not in type_parsers:
|
||||||
|
if DEBUG: print('[OSC PARSER] Argument of type \'%s\' can\'t be parsed by this library' % arg_type)
|
||||||
|
return None
|
||||||
|
parse_result = type_parsers[arg_type](body)
|
||||||
|
# failed to parse
|
||||||
|
if parse_result is None:
|
||||||
|
if DEBUG: print('[OSC PARSER] Failed to parse an argument of type \'%s\'' % arg_type)
|
||||||
|
return None
|
||||||
|
# success
|
||||||
|
args.append(parse_result[0])
|
||||||
|
body = body[parse_result[1]:]
|
||||||
|
# result
|
||||||
|
return {
|
||||||
|
'addr': addr[0],
|
||||||
|
'type_tag': type_tag[0],
|
||||||
|
'args': args
|
||||||
|
}
|
||||||
83
server.py
Normal file
83
server.py
Normal file
@@ -0,0 +1,83 @@
|
|||||||
|
import asyncio
|
||||||
|
import traceback
|
||||||
|
import parser
|
||||||
|
import json
|
||||||
|
|
||||||
|
class __OSCServer(asyncio.DatagramProtocol):
|
||||||
|
''' OSC server '''
|
||||||
|
def __init__(self, callback, event_loop):
|
||||||
|
super().__init__()
|
||||||
|
self.callback = callback
|
||||||
|
self.loop = event_loop
|
||||||
|
self.is_cb_async = asyncio.iscoroutinefunction(self.callback)
|
||||||
|
|
||||||
|
def datagram_received(self, data, addr):
|
||||||
|
try:
|
||||||
|
# try to parse
|
||||||
|
osc_data = parser.parse(data)
|
||||||
|
# failed to parse
|
||||||
|
if osc_data is None:
|
||||||
|
return
|
||||||
|
# no callback - debug
|
||||||
|
if self.callback is None:
|
||||||
|
print(osc_data)
|
||||||
|
return
|
||||||
|
# callback
|
||||||
|
if self.is_cb_async:
|
||||||
|
if self.loop is not None:
|
||||||
|
self.loop.create_task(self.callback(osc_data, addr))
|
||||||
|
else:
|
||||||
|
self.callback(osc_data, addr)
|
||||||
|
except:
|
||||||
|
traceback.print_exc()
|
||||||
|
pass
|
||||||
|
|
||||||
|
__transport, __protocol = None, None
|
||||||
|
|
||||||
|
async def start(host: str, port: int, callback) -> bool:
|
||||||
|
global __transport, __protocol
|
||||||
|
try:
|
||||||
|
# exists
|
||||||
|
if __transport is not None:
|
||||||
|
return False
|
||||||
|
# start
|
||||||
|
loop = asyncio.get_running_loop()
|
||||||
|
__transport, __protocol = await loop.create_datagram_endpoint(
|
||||||
|
lambda: __OSCServer(callback, loop),
|
||||||
|
local_addr=(host, port)
|
||||||
|
)
|
||||||
|
# success
|
||||||
|
return True
|
||||||
|
except:
|
||||||
|
traceback.print_exc()
|
||||||
|
# serious failure
|
||||||
|
return False
|
||||||
|
|
||||||
|
async def stop() -> None:
|
||||||
|
global __transport, __protocol
|
||||||
|
# does not exist
|
||||||
|
if __transport is None:
|
||||||
|
return
|
||||||
|
# close
|
||||||
|
__transport.close()
|
||||||
|
__transport, __protocol = None, None
|
||||||
|
|
||||||
|
# test
|
||||||
|
async def __test_cb(data, addr):
|
||||||
|
print('Received packet from %s:%s:\n%s' % (addr[0], addr[1], json.dumps(data, indent=4)))
|
||||||
|
|
||||||
|
async def __main__() -> None:
|
||||||
|
print('Starting OSC server on 0.0.0.0:23654')
|
||||||
|
if not await start('0.0.0.0', '23654', __test_cb):
|
||||||
|
print('Failed')
|
||||||
|
return
|
||||||
|
print('Started')
|
||||||
|
print('The server will print packet content on reception.')
|
||||||
|
print('GOING TO WORK FOREVER')
|
||||||
|
await asyncio.Future()
|
||||||
|
await stop()
|
||||||
|
print('Stopped')
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
asyncio.run(__main__())
|
||||||
Reference in New Issue
Block a user