python-osc 1.10.2__py3-none-any.whl → 1.10.3__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {python_osc-1.10.2.dist-info → python_osc-1.10.3.dist-info}/METADATA +1 -1
- python_osc-1.10.3.dist-info/RECORD +36 -0
- {python_osc-1.10.2.dist-info → python_osc-1.10.3.dist-info}/WHEEL +1 -1
- pythonosc/dispatcher.py +8 -6
- pythonosc/osc_bundle.py +6 -0
- pythonosc/osc_message_builder.py +7 -4
- pythonosc/osc_server.py +3 -0
- pythonosc/osc_tcp_server.py +49 -165
- pythonosc/parsing/framing.py +166 -0
- pythonosc/parsing/osc_types.py +13 -7
- pythonosc/tcp_client.py +49 -126
- pythonosc/test/parsing/test_framing.py +158 -0
- pythonosc/test/parsing/test_osc_types.py +28 -0
- pythonosc/test/test_dispatcher.py +21 -0
- pythonosc/test/test_empty_string_messages.py +87 -0
- pythonosc/test/test_osc_bundle.py +21 -2
- pythonosc/test/test_osc_message.py +5 -0
- pythonosc/test/test_osc_server.py +31 -1
- pythonosc/test/test_osc_tcp_server.py +45 -27
- pythonosc/test/test_tcp_client.py +2 -1
- python_osc-1.10.2.dist-info/RECORD +0 -33
- /pythonosc/{slip.py → parsing/slip.py} +0 -0
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
pythonosc/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
2
|
+
pythonosc/dispatcher.py,sha256=L1d_jm33uwsSHrNlfaLhTXflc70apcK5FwRTJTuEf3Q,15051
|
|
3
|
+
pythonosc/osc_bundle.py,sha256=k8CR9K-MFr8iKj3pYSj72PE2nyhZsIle-GnVpqSrcvM,4151
|
|
4
|
+
pythonosc/osc_bundle_builder.py,sha256=1m1K1bC3NIbUNjNo8lRFQ-Hciyq4hzWEPQm48c1W8DA,2113
|
|
5
|
+
pythonosc/osc_message.py,sha256=GlWwFMhquvt6JuKo8hC4q_S9aSHYTGsfTUUPvO-5uaQ,4797
|
|
6
|
+
pythonosc/osc_message_builder.py,sha256=CHgpPdU9mfTZ52Dt10J0nrvG85OzP474lAhzKGSU_dE,8080
|
|
7
|
+
pythonosc/osc_packet.py,sha256=72LVRxaPaTwkkih7tsPWgWJlyeHxO7xt3SOOK8YMh50,2778
|
|
8
|
+
pythonosc/osc_server.py,sha256=j-cwrZYeiZJTrbYXaW01JiH0H0IFRQLqXrfv0BsdxTY,7445
|
|
9
|
+
pythonosc/osc_tcp_server.py,sha256=Bh_WJHjrtWMtU1gOpY7X1jZEpZNJN5hztllx_UoHT1M,7141
|
|
10
|
+
pythonosc/parsing/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
11
|
+
pythonosc/parsing/framing.py,sha256=P31sXt9_mO9bUUNjd6bBlS7T3EGUEh129PZ93QzeOhs,4492
|
|
12
|
+
pythonosc/parsing/ntp.py,sha256=9lpFonFynAVYtW5J2ttgnbjM0ej8mTl0BFsm82Ud8Ek,1877
|
|
13
|
+
pythonosc/parsing/osc_types.py,sha256=-YfmtM0q-WVliJwXJaYEcY5MfGYzp0xyTu2eZ6z9GSE,15008
|
|
14
|
+
pythonosc/parsing/slip.py,sha256=KhEBQ3v21A3liulQWMiBYw3oVY_BtEb1wZj1q3tnrIk,3676
|
|
15
|
+
pythonosc/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
16
|
+
pythonosc/tcp_client.py,sha256=ywJaAMWSF_a3kuNAYEe_of-jccwXsnOHM84G7cuHDmc,8039
|
|
17
|
+
pythonosc/test/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
18
|
+
pythonosc/test/parsing/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
19
|
+
pythonosc/test/parsing/test_framing.py,sha256=IYvt_FgLUkeFabbleSuxVqcMiRToy38ugxYaMBTcL9o,5048
|
|
20
|
+
pythonosc/test/parsing/test_ntp.py,sha256=KY4XiArsNZtni46Glq0F6dR3psVDbOROdOjFupTWVuI,605
|
|
21
|
+
pythonosc/test/parsing/test_osc_types.py,sha256=TQz0-yCT7FCN3DjpR3e22tiLpX829rGeogj1tuim-P8,13805
|
|
22
|
+
pythonosc/test/test_dispatcher.py,sha256=C0TsHqoino_2_Dnu10WCBROtB9zi4a2pjft4NvFO7Ps,9367
|
|
23
|
+
pythonosc/test/test_empty_string_messages.py,sha256=F6OpZP5xJP3RY_leWigJd6MdQF6Cdm_v5js3_U8Ba6Q,3768
|
|
24
|
+
pythonosc/test/test_osc_bundle.py,sha256=eXsIl_Jpo9lVmcDVG9o4rWlpTyo7XfBG294_uXp-6Vg,4837
|
|
25
|
+
pythonosc/test/test_osc_bundle_builder.py,sha256=7P2DPBun2TrT_4gUq2_haEJiNC8dbGHwzaRF5MJzJHs,1534
|
|
26
|
+
pythonosc/test/test_osc_message.py,sha256=sDU73FGbEKL5tLYyD04FUsC4aJ1jWxJJmoAaRoWTuuI,13250
|
|
27
|
+
pythonosc/test/test_osc_message_builder.py,sha256=RaFPH9biU6P7LZ-p3N7H5Cfg5w6d2BBxVrwGA9EWwCw,3982
|
|
28
|
+
pythonosc/test/test_osc_packet.py,sha256=GCK7mYKv0bzOwCuhwbA7eaxPOjsn-pTzTbpMzWTXo-s,2163
|
|
29
|
+
pythonosc/test/test_osc_server.py,sha256=MtPJ6ZdEIArRrZxkIPF2_0Dlic0Ct3beQlHZpFgIOaQ,6738
|
|
30
|
+
pythonosc/test/test_osc_tcp_server.py,sha256=Yhj5EGsYMpSwsxuHIMFyi5GEZIpPKxSA-LstWVJXhJs,13972
|
|
31
|
+
pythonosc/test/test_tcp_client.py,sha256=BxRhzaiSoiY5Iz_cYlO45WWKejpdLT7HFciMpu5zXX4,2605
|
|
32
|
+
pythonosc/test/test_udp_client.py,sha256=qLP5SPem6T40JbAoRlf5_N258pTvLzgu3BAqC6PkzMI,3487
|
|
33
|
+
pythonosc/udp_client.py,sha256=4E9GU8dPV3RilCskedzzx2jVcgK6EGp1UXrGjYV3ZaI,5105
|
|
34
|
+
python_osc-1.10.3.dist-info/WHEEL,sha256=3OmCxFA0ffBIsX4x0o8CdfSvZAOvO3mzEG8U_z5TvV4,80
|
|
35
|
+
python_osc-1.10.3.dist-info/METADATA,sha256=vpKkB59PbIqVCpXGHAI3iN0K05Ql7_TMyw6UPLndAAk,6387
|
|
36
|
+
python_osc-1.10.3.dist-info/RECORD,,
|
pythonosc/dispatcher.py
CHANGED
|
@@ -154,13 +154,15 @@ class Dispatcher(object):
|
|
|
154
154
|
|
|
155
155
|
The callback function must have one of the following signatures:
|
|
156
156
|
|
|
157
|
-
|
|
158
|
-
``def some_cb(address: str, fixed_args: List[Any], *osc_args: List[Any]) -> Union[None, AnyStr,
|
|
159
|
-
Tuple(str, ArgValue)]:``
|
|
157
|
+
.. code-block:: python
|
|
160
158
|
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
159
|
+
def some_cb(address: str, *osc_args: ArgValue) -> Union[None, str, Tuple[str, ArgValue]]: ...
|
|
160
|
+
def some_cb(address: str, fixed_args: List[Any], *osc_args: ArgValue) -> Union[None, str, Tuple[str, ArgValue]]: ...
|
|
161
|
+
def some_cb(client_address: Tuple[str, int], address: str, *osc_args: ArgValue) -> Union[None, str, Tuple[str, ArgValue]]: ...
|
|
162
|
+
def some_cb(client_address: Tuple[str, int], address: str, fixed_args: List[Any], *osc_args: ArgValue) -> Union[None, str, Tuple[str, ArgValue]]: ...
|
|
163
|
+
|
|
164
|
+
Each variadic OSC argument is a single ArgValue, not a list of all arguments.
|
|
165
|
+
Any fixed arguments passed to map() are collected into the separate fixed_args list.
|
|
164
166
|
|
|
165
167
|
The callback function can return None, or a string representing an OSC address to be returned to the client,
|
|
166
168
|
or a tuple that includes the address and ArgValue which will be converted to an OSC message and returned to
|
pythonosc/osc_bundle.py
CHANGED
|
@@ -50,6 +50,12 @@ class OscBundle(object):
|
|
|
50
50
|
while self._dgram[index:]:
|
|
51
51
|
# Get the sub content size.
|
|
52
52
|
content_size, index = osc_types.get_int(self._dgram, index)
|
|
53
|
+
if content_size <= 0 or content_size % 4:
|
|
54
|
+
raise ParseError(
|
|
55
|
+
"Invalid bundle element size: {}".format(content_size)
|
|
56
|
+
)
|
|
57
|
+
if content_size > len(self._dgram) - index:
|
|
58
|
+
raise ParseError("Bundle element exceeds the datagram length")
|
|
53
59
|
# Get the datagram for the sub content.
|
|
54
60
|
content_dgram = self._dgram[index : index + content_size]
|
|
55
61
|
# Increment our position index up to the next possible content.
|
pythonosc/osc_message_builder.py
CHANGED
|
@@ -200,13 +200,16 @@ class OscMessageBuilder(object):
|
|
|
200
200
|
|
|
201
201
|
|
|
202
202
|
def build_msg(
|
|
203
|
-
address: str, value: Union[ArgValue, Iterable[ArgValue]] =
|
|
203
|
+
address: str, value: Union[ArgValue, Iterable[ArgValue]] = ()
|
|
204
204
|
) -> osc_message.OscMessage:
|
|
205
|
+
"""Build a message from a scalar value or an iterable of arguments.
|
|
206
|
+
|
|
207
|
+
Omit the value or pass an empty iterable for a message with no arguments.
|
|
208
|
+
An explicitly supplied empty string is retained as a string argument.
|
|
209
|
+
"""
|
|
205
210
|
builder = OscMessageBuilder(address=address)
|
|
206
211
|
values: Iterable[Any]
|
|
207
|
-
if value
|
|
208
|
-
values = []
|
|
209
|
-
elif not isinstance(value, Iterable) or isinstance(value, (str, bytes)):
|
|
212
|
+
if not isinstance(value, Iterable) or isinstance(value, (str, bytes)):
|
|
210
213
|
values = [value]
|
|
211
214
|
else:
|
|
212
215
|
values = value
|
pythonosc/osc_server.py
CHANGED
|
@@ -59,6 +59,9 @@ def _is_valid_request(request: _RequestType) -> bool:
|
|
|
59
59
|
class OSCUDPServer(socketserver.UDPServer):
|
|
60
60
|
"""Superclass for different flavors of OSC UDP servers"""
|
|
61
61
|
|
|
62
|
+
# Receive complete UDP datagrams instead of socketserver's 8192-byte default.
|
|
63
|
+
max_packet_size = 65535
|
|
64
|
+
|
|
62
65
|
def __init__(
|
|
63
66
|
self,
|
|
64
67
|
server_address: Tuple[str, int],
|
pythonosc/osc_tcp_server.py
CHANGED
|
@@ -33,68 +33,37 @@ loop.run_forever()
|
|
|
33
33
|
# mypy: disable-error-code="attr-defined"
|
|
34
34
|
|
|
35
35
|
import asyncio
|
|
36
|
-
import logging
|
|
37
36
|
import os
|
|
38
37
|
import socket
|
|
39
38
|
import socketserver
|
|
40
|
-
import
|
|
41
|
-
from typing import List, Tuple
|
|
39
|
+
from typing import Tuple
|
|
42
40
|
|
|
43
|
-
from pythonosc import osc_message_builder
|
|
41
|
+
from pythonosc import osc_message_builder
|
|
44
42
|
from pythonosc.dispatcher import Dispatcher
|
|
43
|
+
from pythonosc.parsing.framing import (
|
|
44
|
+
BINARY_LENGTH_FRAMING,
|
|
45
|
+
SLIP_FRAMING,
|
|
46
|
+
Framing,
|
|
47
|
+
FramingParser,
|
|
48
|
+
)
|
|
45
49
|
|
|
46
|
-
LOG = logging.getLogger()
|
|
47
50
|
MODE_1_0 = "1.0"
|
|
48
51
|
MODE_1_1 = "1.1"
|
|
52
|
+
FRAMING_BY_MODE: dict[str, Framing] = {
|
|
53
|
+
MODE_1_0: BINARY_LENGTH_FRAMING,
|
|
54
|
+
MODE_1_1: SLIP_FRAMING,
|
|
55
|
+
}
|
|
49
56
|
|
|
50
57
|
|
|
51
|
-
|
|
52
|
-
|
|
58
|
+
def get_framing(mode: str) -> Framing:
|
|
59
|
+
try:
|
|
60
|
+
return FRAMING_BY_MODE[mode]
|
|
61
|
+
except KeyError:
|
|
62
|
+
raise ValueError(f"Unsupported OSC mode: {mode}") from None
|
|
53
63
|
|
|
54
|
-
Whether this will be run on its own thread, the server's or a whole new
|
|
55
|
-
process depends on the server you instantiated, look at their documentation.
|
|
56
64
|
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
if not the server won't even bother to call it and so no new
|
|
60
|
-
threads/processes will be spawned.
|
|
61
|
-
"""
|
|
62
|
-
|
|
63
|
-
def handle(self) -> None:
|
|
64
|
-
LOG.debug("handle OSC 1.0 protocol")
|
|
65
|
-
while True:
|
|
66
|
-
lengthbuf = self.recvall(4)
|
|
67
|
-
if lengthbuf == b"":
|
|
68
|
-
break
|
|
69
|
-
(length,) = struct.unpack("!I", lengthbuf)
|
|
70
|
-
data = self.recvall(length)
|
|
71
|
-
if data == b"":
|
|
72
|
-
break
|
|
73
|
-
|
|
74
|
-
resp = self.server.dispatcher.call_handlers_for_packet(
|
|
75
|
-
data, self.client_address
|
|
76
|
-
)
|
|
77
|
-
# resp = _call_handlers_for_packet(data, self.server.dispatcher)
|
|
78
|
-
for r in resp:
|
|
79
|
-
if not isinstance(r, tuple):
|
|
80
|
-
r = [r]
|
|
81
|
-
msg = osc_message_builder.build_msg(r[0], r[1:])
|
|
82
|
-
b = struct.pack("!I", len(msg.dgram))
|
|
83
|
-
self.request.sendall(b + msg.dgram)
|
|
84
|
-
|
|
85
|
-
def recvall(self, count: int) -> bytes:
|
|
86
|
-
buf = b""
|
|
87
|
-
while count > 0:
|
|
88
|
-
newbuf = self.request.recv(count)
|
|
89
|
-
if not newbuf:
|
|
90
|
-
return b""
|
|
91
|
-
buf += newbuf
|
|
92
|
-
count -= len(newbuf)
|
|
93
|
-
return buf
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
class _TCPHandler1_1(socketserver.BaseRequestHandler):
|
|
97
|
-
"""Handles correct OSC1.1 messages.
|
|
65
|
+
class TCPHandler(socketserver.BaseRequestHandler):
|
|
66
|
+
"""Handles correct OSC messages.
|
|
98
67
|
|
|
99
68
|
Whether this will be run on its own thread, the server's or a whole new
|
|
100
69
|
process depends on the server you instantiated, look at their documentation.
|
|
@@ -106,14 +75,12 @@ class _TCPHandler1_1(socketserver.BaseRequestHandler):
|
|
|
106
75
|
"""
|
|
107
76
|
|
|
108
77
|
def handle(self) -> None:
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
for p in packets:
|
|
116
|
-
# resp = _call_handlers_for_packet(p, self.server.dispatcher)
|
|
78
|
+
assert isinstance(self.server, OSCTCPServer)
|
|
79
|
+
|
|
80
|
+
parser = FramingParser(self.server.framing)
|
|
81
|
+
|
|
82
|
+
while chunk := self.request.recv(16384):
|
|
83
|
+
for p in parser.feed(chunk):
|
|
117
84
|
resp = self.server.dispatcher.call_handlers_for_packet(
|
|
118
85
|
p, self.client_address
|
|
119
86
|
)
|
|
@@ -121,27 +88,15 @@ class _TCPHandler1_1(socketserver.BaseRequestHandler):
|
|
|
121
88
|
if not isinstance(r, tuple):
|
|
122
89
|
r = [r]
|
|
123
90
|
msg = osc_message_builder.build_msg(r[0], r[1:])
|
|
124
|
-
self.request.sendall(
|
|
125
|
-
|
|
126
|
-
def recvall(self) -> List[bytes]:
|
|
127
|
-
buf = self.request.recv(4096)
|
|
128
|
-
if not buf:
|
|
129
|
-
return []
|
|
130
|
-
# If the last byte is not an END marker there could be more data coming
|
|
131
|
-
while buf[-1] != 192:
|
|
132
|
-
newbuf = self.request.recv(4096)
|
|
133
|
-
if not newbuf:
|
|
134
|
-
# Maybe should raise an exception here?
|
|
135
|
-
break
|
|
136
|
-
buf += newbuf
|
|
137
|
-
|
|
138
|
-
packets = [slip.decode(p) for p in buf.split(slip.END_END)]
|
|
139
|
-
return packets
|
|
91
|
+
self.request.sendall(self.server.framing.encode(msg.dgram))
|
|
140
92
|
|
|
141
93
|
|
|
142
94
|
class OSCTCPServer(socketserver.TCPServer):
|
|
143
95
|
"""Superclass for different flavors of OSCTCPServer"""
|
|
144
96
|
|
|
97
|
+
dispatcher: Dispatcher
|
|
98
|
+
framing: Framing
|
|
99
|
+
|
|
145
100
|
def __init__(
|
|
146
101
|
self,
|
|
147
102
|
server_address: Tuple[str | bytes | bytearray, int],
|
|
@@ -150,9 +105,8 @@ class OSCTCPServer(socketserver.TCPServer):
|
|
|
150
105
|
family: socket.AddressFamily | None = None,
|
|
151
106
|
):
|
|
152
107
|
self.request_queue_size = 300
|
|
153
|
-
self.
|
|
154
|
-
|
|
155
|
-
raise ValueError("OSC Mode must be '1.0' or '1.1'")
|
|
108
|
+
self.dispatcher = dispatcher
|
|
109
|
+
self.framing = get_framing(mode)
|
|
156
110
|
|
|
157
111
|
if family is not None:
|
|
158
112
|
self.address_family = family
|
|
@@ -171,16 +125,7 @@ class OSCTCPServer(socketserver.TCPServer):
|
|
|
171
125
|
# Fallback to default if resolution fails
|
|
172
126
|
pass
|
|
173
127
|
|
|
174
|
-
|
|
175
|
-
super().__init__(server_address, _TCPHandler1_0)
|
|
176
|
-
else:
|
|
177
|
-
super().__init__(server_address, _TCPHandler1_1)
|
|
178
|
-
self._dispatcher = dispatcher
|
|
179
|
-
|
|
180
|
-
@property
|
|
181
|
-
def dispatcher(self):
|
|
182
|
-
"""Dispatcher accessor for handlers to dispatch osc messages."""
|
|
183
|
-
return self._dispatcher
|
|
128
|
+
super().__init__(server_address, TCPHandler)
|
|
184
129
|
|
|
185
130
|
|
|
186
131
|
class BlockingOSCTCPServer(OSCTCPServer):
|
|
@@ -230,8 +175,8 @@ class AsyncOSCTCPServer:
|
|
|
230
175
|
"""
|
|
231
176
|
self._port = port
|
|
232
177
|
self._server_address = server_address
|
|
233
|
-
self.
|
|
234
|
-
self.
|
|
178
|
+
self.dispatcher = dispatcher
|
|
179
|
+
self.framing = get_framing(mode)
|
|
235
180
|
|
|
236
181
|
async def __aenter__(self):
|
|
237
182
|
return self
|
|
@@ -245,9 +190,6 @@ class AsyncOSCTCPServer:
|
|
|
245
190
|
self.handle, self._server_address, self._port
|
|
246
191
|
)
|
|
247
192
|
|
|
248
|
-
addrs = ", ".join(str(sock.getsockname()) for sock in self._server.sockets)
|
|
249
|
-
LOG.debug(f"Serving on {addrs}")
|
|
250
|
-
|
|
251
193
|
async with self._server:
|
|
252
194
|
await self._server.serve_forever()
|
|
253
195
|
|
|
@@ -255,86 +197,28 @@ class AsyncOSCTCPServer:
|
|
|
255
197
|
self._server.close()
|
|
256
198
|
await self._server.wait_closed()
|
|
257
199
|
|
|
258
|
-
@property
|
|
259
|
-
def dispatcher(self):
|
|
260
|
-
return self._dispatcher
|
|
261
|
-
|
|
262
200
|
async def handle(
|
|
263
201
|
self, reader: asyncio.StreamReader, writer: asyncio.StreamWriter
|
|
264
202
|
) -> None:
|
|
203
|
+
try:
|
|
204
|
+
await self._handle(reader, writer)
|
|
205
|
+
finally:
|
|
206
|
+
if writer.can_write_eof():
|
|
207
|
+
writer.write_eof()
|
|
208
|
+
writer.close()
|
|
209
|
+
await writer.wait_closed()
|
|
210
|
+
|
|
211
|
+
async def _handle(
|
|
212
|
+
self, reader: asyncio.StreamReader, writer: asyncio.StreamWriter
|
|
213
|
+
) -> None:
|
|
214
|
+
parser = FramingParser(self.framing)
|
|
265
215
|
client_address = ("", 0)
|
|
266
216
|
sock = writer.transport.get_extra_info("socket")
|
|
267
217
|
if sock is not None:
|
|
268
218
|
client_address = sock.getpeername()
|
|
269
219
|
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
else:
|
|
273
|
-
await self.handle1_0(reader, writer, client_address)
|
|
274
|
-
writer.write_eof()
|
|
275
|
-
LOG.debug("Close the connection")
|
|
276
|
-
writer.close()
|
|
277
|
-
await writer.wait_closed()
|
|
278
|
-
|
|
279
|
-
async def handle1_0(
|
|
280
|
-
self,
|
|
281
|
-
reader: asyncio.StreamReader,
|
|
282
|
-
writer: asyncio.StreamWriter,
|
|
283
|
-
client_address: Tuple[str, int],
|
|
284
|
-
) -> None:
|
|
285
|
-
LOG.debug("Incoming socket open 1.0")
|
|
286
|
-
while True:
|
|
287
|
-
try:
|
|
288
|
-
buf = await reader.read(4)
|
|
289
|
-
except Exception as e:
|
|
290
|
-
LOG.exception("Read error", e)
|
|
291
|
-
return
|
|
292
|
-
if buf == b"":
|
|
293
|
-
break
|
|
294
|
-
(length,) = struct.unpack("!I", buf)
|
|
295
|
-
buf = b""
|
|
296
|
-
while length > 0:
|
|
297
|
-
newbuf = await reader.read(length)
|
|
298
|
-
if not newbuf:
|
|
299
|
-
break
|
|
300
|
-
buf += newbuf
|
|
301
|
-
length -= len(newbuf)
|
|
302
|
-
|
|
303
|
-
result = await self.dispatcher.async_call_handlers_for_packet(
|
|
304
|
-
buf, client_address
|
|
305
|
-
)
|
|
306
|
-
for r in result:
|
|
307
|
-
if not isinstance(r, tuple):
|
|
308
|
-
r = [r]
|
|
309
|
-
msg = osc_message_builder.build_msg(r[0], r[1:])
|
|
310
|
-
b = struct.pack("!I", len(msg.dgram))
|
|
311
|
-
writer.write(b + msg.dgram)
|
|
312
|
-
await writer.drain()
|
|
313
|
-
|
|
314
|
-
async def handle_1_1(
|
|
315
|
-
self,
|
|
316
|
-
reader: asyncio.StreamReader,
|
|
317
|
-
writer: asyncio.StreamWriter,
|
|
318
|
-
client_address: Tuple[str, int],
|
|
319
|
-
) -> None:
|
|
320
|
-
LOG.debug("Incoming socket open 1.1")
|
|
321
|
-
while True:
|
|
322
|
-
try:
|
|
323
|
-
buf = await reader.read(4096)
|
|
324
|
-
except Exception as e:
|
|
325
|
-
LOG.exception("Read error", e)
|
|
326
|
-
return
|
|
327
|
-
if buf == b"":
|
|
328
|
-
break
|
|
329
|
-
while len(buf) > 0 and buf[-1] != 192:
|
|
330
|
-
newbuf = await reader.read(4096)
|
|
331
|
-
if not newbuf:
|
|
332
|
-
# Maybe should raise an exception here?
|
|
333
|
-
break
|
|
334
|
-
buf += newbuf
|
|
335
|
-
|
|
336
|
-
packets = [slip.decode(p) for p in buf.split(slip.END_END)]
|
|
337
|
-
for p in packets:
|
|
220
|
+
while chunk := await reader.read(16384):
|
|
221
|
+
for p in parser.feed(chunk):
|
|
338
222
|
result = await self.dispatcher.async_call_handlers_for_packet(
|
|
339
223
|
p, client_address
|
|
340
224
|
)
|
|
@@ -342,5 +226,5 @@ class AsyncOSCTCPServer:
|
|
|
342
226
|
if not isinstance(r, tuple):
|
|
343
227
|
r = [r]
|
|
344
228
|
msg = osc_message_builder.build_msg(r[0], r[1:])
|
|
345
|
-
writer.write(
|
|
229
|
+
writer.write(self.framing.encode(msg.dgram))
|
|
346
230
|
await writer.drain()
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
"""OSC message codecs for different framing strategies."""
|
|
2
|
+
|
|
3
|
+
from abc import ABCMeta, abstractmethod
|
|
4
|
+
from struct import pack, unpack_from
|
|
5
|
+
|
|
6
|
+
from pythonosc.parsing.slip import (
|
|
7
|
+
END,
|
|
8
|
+
decode as slip_decode,
|
|
9
|
+
encode as slip_encode,
|
|
10
|
+
)
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class Framing(metaclass=ABCMeta):
|
|
14
|
+
"""Base class for OSC stream framing strategies."""
|
|
15
|
+
|
|
16
|
+
@abstractmethod
|
|
17
|
+
def encode(self, dgram: bytes) -> bytes:
|
|
18
|
+
"""Encode a single OSC datagram for transmission on a byte stream.
|
|
19
|
+
|
|
20
|
+
Args:
|
|
21
|
+
dgram: Raw OSC datagram bytes.
|
|
22
|
+
|
|
23
|
+
Returns:
|
|
24
|
+
The framed byte sequence to write to the stream.
|
|
25
|
+
"""
|
|
26
|
+
|
|
27
|
+
@abstractmethod
|
|
28
|
+
def decode_stream(
|
|
29
|
+
self, buf: bytes | bytearray, start: int = 0
|
|
30
|
+
) -> tuple[list[bytes], int]:
|
|
31
|
+
"""Decode complete OSC datagrams from a byte buffer.
|
|
32
|
+
|
|
33
|
+
Args:
|
|
34
|
+
buf: Bytes received from a stream.
|
|
35
|
+
start: The index in ``buf`` to start decoding from.
|
|
36
|
+
|
|
37
|
+
Returns:
|
|
38
|
+
A tuple ``(messages, bytes consumed)``.
|
|
39
|
+
"""
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
class SLIPFraming(Framing):
|
|
43
|
+
"""SLIP (RFC 1055) framing for OSC 1.1.
|
|
44
|
+
|
|
45
|
+
See the "OSC Delivery Specification 1.1" section from the 2009 NIME paper for details.
|
|
46
|
+
"""
|
|
47
|
+
|
|
48
|
+
def encode(self, dgram: bytes) -> bytes:
|
|
49
|
+
return slip_encode(dgram)
|
|
50
|
+
|
|
51
|
+
def decode_stream(
|
|
52
|
+
self, buf: bytes | bytearray, start: int = 0
|
|
53
|
+
) -> tuple[list[bytes], int]:
|
|
54
|
+
pos = start
|
|
55
|
+
messages = []
|
|
56
|
+
|
|
57
|
+
scan = start
|
|
58
|
+
while (end := buf.find(END, scan)) >= 0:
|
|
59
|
+
if end > scan:
|
|
60
|
+
messages.append(slip_decode(bytes(buf[scan:end])))
|
|
61
|
+
pos = end + len(END)
|
|
62
|
+
scan = end + len(END)
|
|
63
|
+
|
|
64
|
+
# Consume trailing delimiters, but retain an incomplete packet.
|
|
65
|
+
if scan == len(buf):
|
|
66
|
+
pos = scan
|
|
67
|
+
|
|
68
|
+
return messages, pos - start
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
class BinaryLengthFraming(Framing):
|
|
72
|
+
"""Length-prefixed framing for OSC 1.0.
|
|
73
|
+
|
|
74
|
+
See the "OSC Packets" section from OSC 1.0 Specification for details.
|
|
75
|
+
"""
|
|
76
|
+
|
|
77
|
+
def encode(self, dgram: bytes) -> bytes:
|
|
78
|
+
if dgram:
|
|
79
|
+
return pack("!I", len(dgram)) + dgram
|
|
80
|
+
return b""
|
|
81
|
+
|
|
82
|
+
def decode_stream(
|
|
83
|
+
self, buf: bytes | bytearray, start: int = 0
|
|
84
|
+
) -> tuple[list[bytes], int]:
|
|
85
|
+
messages = []
|
|
86
|
+
pos = start
|
|
87
|
+
|
|
88
|
+
while (data_pos := pos + 4) < len(buf):
|
|
89
|
+
(length,) = unpack_from("!I", buf, pos)
|
|
90
|
+
end = data_pos + length
|
|
91
|
+
|
|
92
|
+
if end > len(buf):
|
|
93
|
+
break
|
|
94
|
+
|
|
95
|
+
messages.append(bytes(buf[data_pos:end]))
|
|
96
|
+
|
|
97
|
+
pos = end
|
|
98
|
+
|
|
99
|
+
return messages, pos - start
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
class FramingParser:
|
|
103
|
+
"""Stateful stream decoder for (framed) OSC byte stream.
|
|
104
|
+
|
|
105
|
+
Not safe for concurrent use.
|
|
106
|
+
|
|
107
|
+
Args:
|
|
108
|
+
framing: Strategy used to decode buffered stream data.
|
|
109
|
+
max_buffer_size: Maximum size of the internal buffer.
|
|
110
|
+
"""
|
|
111
|
+
|
|
112
|
+
__slots__ = ("buf", "start", "framing", "max_buffer_size")
|
|
113
|
+
|
|
114
|
+
def __init__(self, framing: Framing, max_buffer_size: int = 1048576) -> None:
|
|
115
|
+
self.buf = bytearray()
|
|
116
|
+
self.start = 0
|
|
117
|
+
self.framing = framing
|
|
118
|
+
self.max_buffer_size = max_buffer_size
|
|
119
|
+
|
|
120
|
+
def feed(self, chunk: bytes | bytearray | memoryview) -> list[bytes]:
|
|
121
|
+
"""Append stream data to the internal buffer and return any complete OSC datagrams.
|
|
122
|
+
|
|
123
|
+
Args:
|
|
124
|
+
chunk: Newly received bytes from the stream.
|
|
125
|
+
|
|
126
|
+
Returns:
|
|
127
|
+
A list of complete, decoded OSC datagrams.
|
|
128
|
+
"""
|
|
129
|
+
|
|
130
|
+
new_buf_len = len(self.buf) + len(chunk)
|
|
131
|
+
|
|
132
|
+
if (pending := new_buf_len - self.start) > self.max_buffer_size:
|
|
133
|
+
raise ValueError(f"Buffer is too large: {pending} > {self.max_buffer_size}")
|
|
134
|
+
|
|
135
|
+
if new_buf_len > self.max_buffer_size:
|
|
136
|
+
del self.buf[: self.start]
|
|
137
|
+
self.start = 0
|
|
138
|
+
|
|
139
|
+
self.buf.extend(chunk)
|
|
140
|
+
|
|
141
|
+
messages, consumed = self.framing.decode_stream(self.buf, self.start)
|
|
142
|
+
available = len(self.buf) - self.start
|
|
143
|
+
|
|
144
|
+
assert 0 <= consumed <= available, (
|
|
145
|
+
f"misbehaving framing decoder: {consumed=} {available=}"
|
|
146
|
+
)
|
|
147
|
+
|
|
148
|
+
self.start += consumed
|
|
149
|
+
|
|
150
|
+
return messages
|
|
151
|
+
|
|
152
|
+
@property
|
|
153
|
+
def remaining(self) -> bytes:
|
|
154
|
+
"""Return any unparsed bytes."""
|
|
155
|
+
|
|
156
|
+
return bytes(self.buf[self.start :])
|
|
157
|
+
|
|
158
|
+
def reset(self) -> None:
|
|
159
|
+
"""Clear the internal buffer (discard unparsed data)."""
|
|
160
|
+
|
|
161
|
+
del self.buf[:]
|
|
162
|
+
self.start = 0
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
SLIP_FRAMING: Framing = SLIPFraming()
|
|
166
|
+
BINARY_LENGTH_FRAMING: Framing = BinaryLengthFraming()
|
pythonosc/parsing/osc_types.py
CHANGED
|
@@ -86,6 +86,8 @@ def get_string(dgram: bytes, start_index: int) -> Tuple[str, int]:
|
|
|
86
86
|
|
|
87
87
|
data_str = dgram[start_index : start_index + offset]
|
|
88
88
|
return data_str.decode("utf-8"), start_index + total_len
|
|
89
|
+
except UnicodeDecodeError as error:
|
|
90
|
+
raise ParseError(f"Could not decode OSC string: {error}") from error
|
|
89
91
|
except IndexError as ie:
|
|
90
92
|
raise ParseError(f"Could not parse datagram {ie}")
|
|
91
93
|
except TypeError as te:
|
|
@@ -307,7 +309,7 @@ def get_blob(dgram: bytes, start_index: int) -> Tuple[bytes, int]:
|
|
|
307
309
|
|
|
308
310
|
Args:
|
|
309
311
|
dgram: A datagram packet.
|
|
310
|
-
start_index: An index where the
|
|
312
|
+
start_index: An index where the blob starts in the datagram.
|
|
311
313
|
|
|
312
314
|
Returns:
|
|
313
315
|
A tuple containing the blob and the new end index.
|
|
@@ -315,13 +317,17 @@ def get_blob(dgram: bytes, start_index: int) -> Tuple[bytes, int]:
|
|
|
315
317
|
Raises:
|
|
316
318
|
ParseError if the datagram could not be parsed.
|
|
317
319
|
"""
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
320
|
+
if start_index < 0:
|
|
321
|
+
raise ParseError("start_index < 0")
|
|
322
|
+
size, payload_start = get_int(dgram, start_index)
|
|
323
|
+
if size < 0:
|
|
324
|
+
raise ParseError("Blob length cannot be negative")
|
|
325
|
+
|
|
326
|
+
payload_end = payload_start + size
|
|
327
|
+
next_index = payload_end + (-size % _BLOB_DGRAM_PAD)
|
|
328
|
+
if next_index > len(dgram):
|
|
323
329
|
raise ParseError("Datagram is too short.")
|
|
324
|
-
return dgram[
|
|
330
|
+
return dgram[payload_start:payload_end], next_index
|
|
325
331
|
|
|
326
332
|
|
|
327
333
|
def write_blob(val: bytes) -> bytes:
|