Initial commit

This commit is contained in:
2025-11-06 18:10:31 +03:00
commit a04988ab83
4 changed files with 297 additions and 0 deletions

2
.gitignore vendored Normal file
View File

@@ -0,0 +1,2 @@
__pycache__
*.swp

114
client.py Normal file
View 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
View 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
View 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__())