oscparser 1.1.0__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.
- oscparser/__init__.py +70 -0
- oscparser/ctx.py +17 -0
- oscparser/decode.py +82 -0
- oscparser/encode.py +78 -0
- oscparser/framing/__init__.py +6 -0
- oscparser/framing/framer.py +18 -0
- oscparser/framing/fullframer.py +42 -0
- oscparser/framing/osc10.py +70 -0
- oscparser/framing/osc11.py +208 -0
- oscparser/processing/args/args.py +550 -0
- oscparser/processing/args/proccessing.py +29 -0
- oscparser/processing/osc/handlers.py +165 -0
- oscparser/processing/osc/processing.py +46 -0
- oscparser/types.py +263 -0
- oscparser-1.1.0.dist-info/METADATA +60 -0
- oscparser-1.1.0.dist-info/RECORD +19 -0
- oscparser-1.1.0.dist-info/WHEEL +5 -0
- oscparser-1.1.0.dist-info/licenses/LICENSE +21 -0
- oscparser-1.1.0.dist-info/top_level.txt +1 -0
oscparser/__init__.py
ADDED
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
"""oscparser - Open Sound Control (OSC) 1.0/1.1 parser library.
|
|
2
|
+
|
|
3
|
+
This module provides encoding and decoding for OSC packets with support for:
|
|
4
|
+
- OSC 1.0 (UDP and TCP with length-prefixed framing)
|
|
5
|
+
- OSC 1.1 (TCP with SLIP framing)
|
|
6
|
+
|
|
7
|
+
Basic usage:
|
|
8
|
+
>>> from oscparser import OSCEncoder, OSCDecoder, OSCMessage, OSCInt, OSCModes, OSCFraming
|
|
9
|
+
>>> encoder = OSCEncoder(OSCModes.UDP, OSCFraming.OSC10)
|
|
10
|
+
>>> decoder = OSCDecoder(OSCModes.UDP, OSCFraming.OSC10)
|
|
11
|
+
>>> msg = OSCMessage(address="/test", args=(OSCInt(42),))
|
|
12
|
+
>>> encoded = encoder.encode(msg)
|
|
13
|
+
>>> decoded = list(decoder.feed(encoded))[0]
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
from oscparser.decode import OSCDecoder
|
|
17
|
+
from oscparser.encode import OSCEncoder, OSCFraming, OSCModes
|
|
18
|
+
from oscparser.types import (
|
|
19
|
+
OSC_IMPULSE,
|
|
20
|
+
OSCRGBA,
|
|
21
|
+
OSCArg,
|
|
22
|
+
OSCArray,
|
|
23
|
+
OSCAtomic,
|
|
24
|
+
OSCBlob,
|
|
25
|
+
OSCBundle,
|
|
26
|
+
OSCChar,
|
|
27
|
+
OSCDouble,
|
|
28
|
+
OSCFalse,
|
|
29
|
+
OSCFloat,
|
|
30
|
+
OSCImpulse,
|
|
31
|
+
OSCInt,
|
|
32
|
+
OSCInt64,
|
|
33
|
+
OSCMessage,
|
|
34
|
+
OSCMidi,
|
|
35
|
+
OSCNil,
|
|
36
|
+
OSCPacket,
|
|
37
|
+
OSCString,
|
|
38
|
+
OSCSymbol,
|
|
39
|
+
OSCTimeTag,
|
|
40
|
+
OSCTrue,
|
|
41
|
+
)
|
|
42
|
+
|
|
43
|
+
__all__ = [
|
|
44
|
+
"OSCRGBA",
|
|
45
|
+
"OSC_IMPULSE",
|
|
46
|
+
"OSCArg",
|
|
47
|
+
"OSCArray",
|
|
48
|
+
"OSCAtomic",
|
|
49
|
+
"OSCBlob",
|
|
50
|
+
"OSCBundle",
|
|
51
|
+
"OSCChar",
|
|
52
|
+
"OSCDecoder",
|
|
53
|
+
"OSCDouble",
|
|
54
|
+
"OSCEncoder",
|
|
55
|
+
"OSCFalse",
|
|
56
|
+
"OSCFloat",
|
|
57
|
+
"OSCFraming",
|
|
58
|
+
"OSCImpulse",
|
|
59
|
+
"OSCInt",
|
|
60
|
+
"OSCInt64",
|
|
61
|
+
"OSCMessage",
|
|
62
|
+
"OSCMidi",
|
|
63
|
+
"OSCModes",
|
|
64
|
+
"OSCNil",
|
|
65
|
+
"OSCPacket",
|
|
66
|
+
"OSCString",
|
|
67
|
+
"OSCSymbol",
|
|
68
|
+
"OSCTimeTag",
|
|
69
|
+
"OSCTrue",
|
|
70
|
+
]
|
oscparser/ctx.py
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
class DataBuffer:
|
|
2
|
+
def __init__(self, data: bytes):
|
|
3
|
+
self.data = data
|
|
4
|
+
|
|
5
|
+
def startswith(self, prefix: bytes) -> bool:
|
|
6
|
+
return self.data.startswith(prefix)
|
|
7
|
+
|
|
8
|
+
def read(self, n: int) -> bytes:
|
|
9
|
+
chunk = self.data[:n]
|
|
10
|
+
self.data = self.data[n:]
|
|
11
|
+
return chunk
|
|
12
|
+
|
|
13
|
+
def write(self, data: bytes) -> None:
|
|
14
|
+
self.data += data
|
|
15
|
+
|
|
16
|
+
def remaining(self) -> int:
|
|
17
|
+
return len(self.data)
|
oscparser/decode.py
ADDED
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
from typing import Iterator, cast
|
|
2
|
+
|
|
3
|
+
from oscparser.ctx import DataBuffer
|
|
4
|
+
from oscparser.encode import OSCFraming, OSCModes
|
|
5
|
+
from oscparser.framing.framer import Framer
|
|
6
|
+
from oscparser.framing.fullframer import FullFramer
|
|
7
|
+
from oscparser.framing.osc10 import OSC10Framer
|
|
8
|
+
from oscparser.framing.osc11 import OSC11Framer
|
|
9
|
+
from oscparser.processing.osc.handlers import register_osc_handlers
|
|
10
|
+
from oscparser.processing.osc.processing import OSCDispatcher
|
|
11
|
+
from oscparser.types import OSCPacket
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class OSCDecoder:
|
|
15
|
+
"""Decoder for OSC packets."""
|
|
16
|
+
|
|
17
|
+
def __init__(self, mode: OSCModes, framing: OSCFraming):
|
|
18
|
+
"""Initialize the decoder.
|
|
19
|
+
|
|
20
|
+
Args:
|
|
21
|
+
mode: Transport mode, either 'udp' or 'tcp'
|
|
22
|
+
framing: Framing type, either 'osc10' or 'osc11'
|
|
23
|
+
"""
|
|
24
|
+
self.framer = self.get_framer(mode, framing)
|
|
25
|
+
self.decoder = self.get_decoder()
|
|
26
|
+
|
|
27
|
+
@staticmethod
|
|
28
|
+
def get_decoder() -> OSCDispatcher:
|
|
29
|
+
"""Get an OSCDispatcher configured with standard handlers.
|
|
30
|
+
|
|
31
|
+
Returns:
|
|
32
|
+
An OSCDispatcher instance with registered handlers
|
|
33
|
+
"""
|
|
34
|
+
dispatcher = OSCDispatcher()
|
|
35
|
+
register_osc_handlers(dispatcher)
|
|
36
|
+
return dispatcher
|
|
37
|
+
|
|
38
|
+
@staticmethod
|
|
39
|
+
def get_framer(mode: OSCModes, framing: OSCFraming) -> Framer:
|
|
40
|
+
"""Get the appropriate framer class based on mode and framing.
|
|
41
|
+
|
|
42
|
+
Args:
|
|
43
|
+
mode: Transport mode (UDP or TCP)
|
|
44
|
+
framing: Framing type (OSC10 or OSC11)
|
|
45
|
+
|
|
46
|
+
Returns:
|
|
47
|
+
The corresponding framer instance
|
|
48
|
+
"""
|
|
49
|
+
if mode == OSCModes.UDP:
|
|
50
|
+
return FullFramer()
|
|
51
|
+
elif mode == OSCModes.TCP:
|
|
52
|
+
if framing == OSCFraming.OSC10:
|
|
53
|
+
return OSC10Framer()
|
|
54
|
+
elif framing == OSCFraming.OSC11:
|
|
55
|
+
return OSC11Framer()
|
|
56
|
+
raise ValueError("Unsupported mode or framing type")
|
|
57
|
+
|
|
58
|
+
def decode(self, data: bytes) -> Iterator[OSCPacket]:
|
|
59
|
+
"""Feed data into the decoder and yield decoded OSC packets.
|
|
60
|
+
|
|
61
|
+
For streaming TCP connections where data arrives in chunks.
|
|
62
|
+
|
|
63
|
+
Args:
|
|
64
|
+
data: Raw bytes received from socket
|
|
65
|
+
|
|
66
|
+
Yields:
|
|
67
|
+
Decoded OSC packets
|
|
68
|
+
"""
|
|
69
|
+
# Unframe the data to get complete OSC packets
|
|
70
|
+
for packet_data in self.framer.feed(data):
|
|
71
|
+
# Decode each complete packet
|
|
72
|
+
data_buffer = DataBuffer(packet_data)
|
|
73
|
+
handler = self.decoder.get_handler(data_buffer)
|
|
74
|
+
yield cast(OSCPacket, handler.decode(data_buffer))
|
|
75
|
+
|
|
76
|
+
def clear_buffer(self) -> None:
|
|
77
|
+
"""Clear the internal framer buffer.
|
|
78
|
+
|
|
79
|
+
Useful when resetting the connection or recovering from errors.
|
|
80
|
+
"""
|
|
81
|
+
if hasattr(self.framer, "clear_buffer"):
|
|
82
|
+
self.framer.clear_buffer()
|
oscparser/encode.py
ADDED
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
from enum import Enum
|
|
2
|
+
|
|
3
|
+
from oscparser.ctx import DataBuffer
|
|
4
|
+
from oscparser.framing.framer import Framer
|
|
5
|
+
from oscparser.framing.fullframer import FullFramer
|
|
6
|
+
from oscparser.framing.osc10 import OSC10Framer
|
|
7
|
+
from oscparser.framing.osc11 import OSC11Framer
|
|
8
|
+
from oscparser.processing.osc.handlers import register_osc_handlers
|
|
9
|
+
from oscparser.processing.osc.processing import OSCDispatcher
|
|
10
|
+
from oscparser.types import OSCPacket
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class OSCModes(Enum):
|
|
14
|
+
UDP = "udp"
|
|
15
|
+
TCP = "tcp"
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class OSCFraming(Enum):
|
|
19
|
+
OSC10 = "osc10"
|
|
20
|
+
OSC11 = "osc11"
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class OSCEncoder:
|
|
24
|
+
"""Encoder for OSC packets."""
|
|
25
|
+
|
|
26
|
+
def __init__(self, mode: OSCModes, framing: OSCFraming):
|
|
27
|
+
"""Initialize the framer.
|
|
28
|
+
|
|
29
|
+
Args:
|
|
30
|
+
mode: Transport mode, either 'udp' or 'tcp'
|
|
31
|
+
"""
|
|
32
|
+
self.framer = self.get_framer(mode, framing)
|
|
33
|
+
self.encoder = self.get_encoder()
|
|
34
|
+
|
|
35
|
+
@staticmethod
|
|
36
|
+
def get_encoder() -> OSCDispatcher:
|
|
37
|
+
"""Get an OSCDispatcher configured with standard handlers.
|
|
38
|
+
|
|
39
|
+
Returns:
|
|
40
|
+
An OSCDispatcher instance with registered handlers
|
|
41
|
+
"""
|
|
42
|
+
dispatcher = OSCDispatcher()
|
|
43
|
+
register_osc_handlers(dispatcher)
|
|
44
|
+
return dispatcher
|
|
45
|
+
|
|
46
|
+
@staticmethod
|
|
47
|
+
def get_framer(mode: OSCModes, framing: OSCFraming) -> Framer:
|
|
48
|
+
"""Get the appropriate framer class based on mode and framing.
|
|
49
|
+
|
|
50
|
+
Args:
|
|
51
|
+
mode: Transport mode (UDP or TCP)
|
|
52
|
+
framing: Framing type (OSC10 or OSC11)
|
|
53
|
+
|
|
54
|
+
Returns:
|
|
55
|
+
The corresponding framer class
|
|
56
|
+
"""
|
|
57
|
+
if mode == OSCModes.UDP:
|
|
58
|
+
return FullFramer()
|
|
59
|
+
elif mode == OSCModes.TCP:
|
|
60
|
+
if framing == OSCFraming.OSC10:
|
|
61
|
+
return OSC10Framer()
|
|
62
|
+
elif framing == OSCFraming.OSC11:
|
|
63
|
+
return OSC11Framer()
|
|
64
|
+
raise ValueError("Unsupported mode or framing type")
|
|
65
|
+
|
|
66
|
+
def encode(self, packet: OSCPacket) -> bytes:
|
|
67
|
+
"""Encode and frame an OSC packet.
|
|
68
|
+
|
|
69
|
+
Args:
|
|
70
|
+
packet: The OSC packet to encode
|
|
71
|
+
Returns:
|
|
72
|
+
Framed OSC packet bytes
|
|
73
|
+
"""
|
|
74
|
+
data_buffer = DataBuffer(b"")
|
|
75
|
+
handler = self.encoder.get_object_handler(type(packet))
|
|
76
|
+
handler.encode(packet, data_buffer)
|
|
77
|
+
framed_packet = self.framer.frame(data_buffer.data)
|
|
78
|
+
return framed_packet
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
from typing import Iterator, Protocol
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class Framer(Protocol):
|
|
5
|
+
"""Protocol for OSC framers."""
|
|
6
|
+
|
|
7
|
+
@staticmethod
|
|
8
|
+
def frame(packet: bytes) -> bytes:
|
|
9
|
+
"""Frame an OSC packet for transport."""
|
|
10
|
+
...
|
|
11
|
+
|
|
12
|
+
def feed(self, data: bytes) -> Iterator[bytes]:
|
|
13
|
+
"""Feed data into the framer and yield complete packets."""
|
|
14
|
+
...
|
|
15
|
+
|
|
16
|
+
def clear_buffer(self) -> None:
|
|
17
|
+
"""Clear the internal receive buffer."""
|
|
18
|
+
...
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
from typing import Iterator
|
|
2
|
+
|
|
3
|
+
from oscparser.framing.framer import Framer
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class FullFramer(Framer):
|
|
7
|
+
"""A framer that does not perform any framing.
|
|
8
|
+
|
|
9
|
+
This class is useful for protocols that do not require framing,
|
|
10
|
+
such as UDP transport of OSC packets.
|
|
11
|
+
|
|
12
|
+
Complies with the Framer protocol.
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
@staticmethod
|
|
16
|
+
def frame(packet: bytes) -> bytes:
|
|
17
|
+
"""Return the packet as-is without any framing.
|
|
18
|
+
|
|
19
|
+
Args:
|
|
20
|
+
packet: Raw OSC packet bytes (message or bundle)
|
|
21
|
+
|
|
22
|
+
Returns:
|
|
23
|
+
The same packet bytes without any framing
|
|
24
|
+
"""
|
|
25
|
+
return packet
|
|
26
|
+
|
|
27
|
+
def feed(self, data: bytes) -> Iterator[bytes]:
|
|
28
|
+
"""Yield the incoming data as a complete packet.
|
|
29
|
+
|
|
30
|
+
Since there is no framing, each call to feed yields the entire data.
|
|
31
|
+
|
|
32
|
+
Args:
|
|
33
|
+
data: Raw bytes received from transport
|
|
34
|
+
|
|
35
|
+
Yields:
|
|
36
|
+
The entire data as a single OSC packet
|
|
37
|
+
"""
|
|
38
|
+
yield data
|
|
39
|
+
|
|
40
|
+
def clear_buffer(self) -> None:
|
|
41
|
+
"""No internal buffer to clear in this framer."""
|
|
42
|
+
pass
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
"""OSC 1.0 framing for TCP transport.
|
|
2
|
+
|
|
3
|
+
OSC 1.0 over TCP uses length-prefixed framing: a 4-byte big-endian
|
|
4
|
+
integer indicating the packet size, followed by the packet data.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import struct
|
|
8
|
+
from typing import Iterator
|
|
9
|
+
|
|
10
|
+
from oscparser.framing.framer import Framer
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class OSC10Framer(Framer):
|
|
14
|
+
"""Framer for OSC 1.0 packets over TCP.
|
|
15
|
+
|
|
16
|
+
Each packet is prefixed with a 4-byte big-endian size header.
|
|
17
|
+
Complies with the Framer protocol.
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
def __init__(self):
|
|
21
|
+
"""Initialize the framer with an empty receive buffer."""
|
|
22
|
+
self._buffer = bytearray()
|
|
23
|
+
|
|
24
|
+
@staticmethod
|
|
25
|
+
def frame(packet: bytes) -> bytes:
|
|
26
|
+
"""Frame an OSC packet for TCP transport.
|
|
27
|
+
|
|
28
|
+
OSC 1.0 over TCP uses length-prefixed framing: a 4-byte big-endian
|
|
29
|
+
integer indicating the packet size, followed by the packet data.
|
|
30
|
+
|
|
31
|
+
Args:
|
|
32
|
+
packet: Raw OSC packet bytes (message or bundle)
|
|
33
|
+
|
|
34
|
+
Returns:
|
|
35
|
+
Length-prefixed packet (4 bytes size + packet data)
|
|
36
|
+
"""
|
|
37
|
+
size = len(packet)
|
|
38
|
+
return struct.pack(">I", size) + packet
|
|
39
|
+
|
|
40
|
+
def feed(self, data: bytes) -> Iterator[bytes]:
|
|
41
|
+
"""Feed data into the framer and yield complete packets.
|
|
42
|
+
|
|
43
|
+
Buffers partial packets and yields complete packets as they arrive.
|
|
44
|
+
|
|
45
|
+
Args:
|
|
46
|
+
data: Raw bytes received from TCP socket
|
|
47
|
+
|
|
48
|
+
Yields:
|
|
49
|
+
Complete OSC packets (without size prefix)
|
|
50
|
+
"""
|
|
51
|
+
self._buffer.extend(data)
|
|
52
|
+
|
|
53
|
+
while len(self._buffer) >= 4:
|
|
54
|
+
# Read the size prefix
|
|
55
|
+
size = struct.unpack(">I", bytes(self._buffer[:4]))[0]
|
|
56
|
+
|
|
57
|
+
# Check if we have the complete packet
|
|
58
|
+
if len(self._buffer) < 4 + size:
|
|
59
|
+
# Not enough data yet
|
|
60
|
+
break
|
|
61
|
+
|
|
62
|
+
# Extract the packet
|
|
63
|
+
packet = bytes(self._buffer[4 : 4 + size])
|
|
64
|
+
self._buffer = self._buffer[4 + size :]
|
|
65
|
+
|
|
66
|
+
yield packet
|
|
67
|
+
|
|
68
|
+
def clear_buffer(self) -> None:
|
|
69
|
+
"""Clear the internal receive buffer."""
|
|
70
|
+
self._buffer.clear()
|
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
"""OSC 1.1 framing for TCP transport using SLIP encoding.
|
|
2
|
+
|
|
3
|
+
OSC 1.1 adds support for stream-oriented transports like TCP. Since TCP is a
|
|
4
|
+
byte stream without inherent message boundaries, SLIP (Serial Line Internet
|
|
5
|
+
Protocol) encoding is used to frame OSC packets.
|
|
6
|
+
|
|
7
|
+
SLIP uses special bytes to delimit packets:
|
|
8
|
+
- END (0xC0): Packet boundary marker
|
|
9
|
+
- ESC (0xDB): Escape byte
|
|
10
|
+
- ESC_END (0xDC): Escaped END byte
|
|
11
|
+
- ESC_ESC (0xDD): Escaped ESC byte
|
|
12
|
+
|
|
13
|
+
Reference: RFC 1055, OSC 1.1 specification
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
from typing import Iterator
|
|
17
|
+
|
|
18
|
+
from oscparser.framing.framer import Framer
|
|
19
|
+
|
|
20
|
+
# SLIP protocol constants
|
|
21
|
+
END = b"\xc0"
|
|
22
|
+
ESC = b"\xdb"
|
|
23
|
+
ESC_END = b"\xdc"
|
|
24
|
+
ESC_ESC = b"\xdd"
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class SLIPError(ValueError):
|
|
28
|
+
"""Exception raised when SLIP protocol violations are detected."""
|
|
29
|
+
|
|
30
|
+
pass
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class OSC11Framer(Framer):
|
|
34
|
+
"""Framer for OSC 1.1 packets over TCP using SLIP encoding.
|
|
35
|
+
|
|
36
|
+
This class handles:
|
|
37
|
+
- Encoding OSC packets with SLIP framing for transmission
|
|
38
|
+
- Decoding SLIP-framed packets from received byte streams
|
|
39
|
+
- Buffering partial packets in stream-based reception
|
|
40
|
+
|
|
41
|
+
Complies with the Framer protocol.
|
|
42
|
+
"""
|
|
43
|
+
|
|
44
|
+
def __init__(self):
|
|
45
|
+
"""Initialize the framer with an empty receive buffer."""
|
|
46
|
+
self._buffer = bytearray()
|
|
47
|
+
|
|
48
|
+
@staticmethod
|
|
49
|
+
def frame(packet: bytes) -> bytes:
|
|
50
|
+
"""Frame an OSC packet for TCP transport using SLIP encoding.
|
|
51
|
+
|
|
52
|
+
The packet is escaped according to SLIP rules and wrapped with END bytes.
|
|
53
|
+
|
|
54
|
+
SLIP encoding rules:
|
|
55
|
+
- Replace ESC with ESC + ESC_ESC
|
|
56
|
+
- Replace END with ESC + ESC_END
|
|
57
|
+
- Wrap with END bytes (before and after)
|
|
58
|
+
|
|
59
|
+
Args:
|
|
60
|
+
packet: Raw OSC packet bytes (message or bundle)
|
|
61
|
+
|
|
62
|
+
Returns:
|
|
63
|
+
SLIP-encoded packet ready for transmission
|
|
64
|
+
"""
|
|
65
|
+
if not packet:
|
|
66
|
+
packet = b""
|
|
67
|
+
|
|
68
|
+
# Escape special bytes
|
|
69
|
+
encoded = packet.replace(ESC, ESC + ESC_ESC)
|
|
70
|
+
encoded = encoded.replace(END, ESC + ESC_END)
|
|
71
|
+
|
|
72
|
+
# Wrap with END bytes
|
|
73
|
+
return END + encoded + END
|
|
74
|
+
|
|
75
|
+
@staticmethod
|
|
76
|
+
def _unframe(slip_packet: bytes) -> bytes:
|
|
77
|
+
"""Unframe a single SLIP-encoded packet.
|
|
78
|
+
|
|
79
|
+
This decodes one complete SLIP packet. The packet must be a complete,
|
|
80
|
+
properly framed SLIP packet (starting and ending with END bytes).
|
|
81
|
+
|
|
82
|
+
Args:
|
|
83
|
+
slip_packet: SLIP-encoded packet bytes
|
|
84
|
+
|
|
85
|
+
Returns:
|
|
86
|
+
Decoded OSC packet bytes
|
|
87
|
+
|
|
88
|
+
Raises:
|
|
89
|
+
SLIPError: If the packet is malformed or contains invalid sequences
|
|
90
|
+
"""
|
|
91
|
+
if not OSC11Framer._is_valid_slip(slip_packet):
|
|
92
|
+
raise SLIPError(f"Invalid SLIP packet: {slip_packet!r}")
|
|
93
|
+
|
|
94
|
+
# Strip END bytes
|
|
95
|
+
decoded = slip_packet.strip(END)
|
|
96
|
+
|
|
97
|
+
# Replace escaped sequences
|
|
98
|
+
decoded = decoded.replace(ESC + ESC_END, END)
|
|
99
|
+
decoded = decoded.replace(ESC + ESC_ESC, ESC)
|
|
100
|
+
|
|
101
|
+
return decoded
|
|
102
|
+
|
|
103
|
+
def feed(self, data: bytes) -> Iterator[bytes]:
|
|
104
|
+
"""Feed data into the framer and yield complete packets.
|
|
105
|
+
|
|
106
|
+
This method maintains an internal buffer to handle partial packets.
|
|
107
|
+
As data arrives, it's buffered until complete SLIP-framed packets
|
|
108
|
+
can be extracted.
|
|
109
|
+
|
|
110
|
+
Args:
|
|
111
|
+
data: Raw bytes received from TCP socket
|
|
112
|
+
|
|
113
|
+
Yields:
|
|
114
|
+
Complete OSC packets (SLIP-decoded)
|
|
115
|
+
|
|
116
|
+
Raises:
|
|
117
|
+
SLIPError: If malformed SLIP sequences are detected
|
|
118
|
+
"""
|
|
119
|
+
self._buffer.extend(data)
|
|
120
|
+
|
|
121
|
+
while True:
|
|
122
|
+
# If buffer doesn't start with END, find first END and discard garbage
|
|
123
|
+
try:
|
|
124
|
+
first_end = self._buffer.index(END[0])
|
|
125
|
+
# Discard everything before the first END as garbage/misalignment
|
|
126
|
+
self._buffer = self._buffer[first_end:]
|
|
127
|
+
except ValueError:
|
|
128
|
+
# No END byte found, clear buffer and wait for more data
|
|
129
|
+
break
|
|
130
|
+
|
|
131
|
+
# Now buffer starts with END, find the closing END
|
|
132
|
+
if len(self._buffer) < 2:
|
|
133
|
+
# Need at least 2 bytes to have a complete packet
|
|
134
|
+
break
|
|
135
|
+
|
|
136
|
+
try:
|
|
137
|
+
# Search for closing END (starting from position 1)
|
|
138
|
+
closing_end = self._buffer.index(END[0], 1)
|
|
139
|
+
except ValueError:
|
|
140
|
+
# No closing END found yet, wait for more data
|
|
141
|
+
break
|
|
142
|
+
|
|
143
|
+
# Extract the complete SLIP packet (including both END bytes)
|
|
144
|
+
slip_packet = bytes(self._buffer[: closing_end + 1])
|
|
145
|
+
self._buffer = self._buffer[closing_end + 1 :]
|
|
146
|
+
|
|
147
|
+
# Skip empty packets (double END bytes - these are packet separators)
|
|
148
|
+
if slip_packet == END + END:
|
|
149
|
+
# Add an END back to the buffer to maintain alignment
|
|
150
|
+
self._buffer.insert(0, END[0])
|
|
151
|
+
continue
|
|
152
|
+
|
|
153
|
+
# Decode and yield the packet
|
|
154
|
+
try:
|
|
155
|
+
packet = self._unframe(slip_packet)
|
|
156
|
+
if packet: # Only yield non-empty packets
|
|
157
|
+
yield packet
|
|
158
|
+
except SLIPError:
|
|
159
|
+
# Skip malformed packets and continue processing
|
|
160
|
+
continue
|
|
161
|
+
|
|
162
|
+
def clear_buffer(self) -> None:
|
|
163
|
+
"""Clear the internal receive buffer.
|
|
164
|
+
|
|
165
|
+
Useful when resetting the connection or recovering from errors.
|
|
166
|
+
"""
|
|
167
|
+
self._buffer.clear()
|
|
168
|
+
|
|
169
|
+
@staticmethod
|
|
170
|
+
def _is_valid_slip(packet: bytes) -> bool:
|
|
171
|
+
"""Check if a packet is valid according to SLIP protocol.
|
|
172
|
+
|
|
173
|
+
A valid SLIP packet:
|
|
174
|
+
- Contains no unescaped END bytes except at boundaries
|
|
175
|
+
- Each ESC byte is followed by ESC_END or ESC_ESC
|
|
176
|
+
- Does not end with a trailing ESC byte
|
|
177
|
+
|
|
178
|
+
Args:
|
|
179
|
+
packet: SLIP packet to validate
|
|
180
|
+
|
|
181
|
+
Returns:
|
|
182
|
+
True if valid, False otherwise
|
|
183
|
+
"""
|
|
184
|
+
# Strip boundary END bytes
|
|
185
|
+
inner = packet.strip(END)
|
|
186
|
+
|
|
187
|
+
# Check for unescaped END bytes in the middle
|
|
188
|
+
if END[0] in inner:
|
|
189
|
+
return False
|
|
190
|
+
|
|
191
|
+
# Check for trailing ESC
|
|
192
|
+
if inner.endswith(ESC):
|
|
193
|
+
return False
|
|
194
|
+
|
|
195
|
+
# Check that all ESC bytes are properly followed
|
|
196
|
+
i = 0
|
|
197
|
+
while i < len(inner):
|
|
198
|
+
if inner[i : i + 1] == ESC:
|
|
199
|
+
if i + 1 >= len(inner):
|
|
200
|
+
return False # ESC at end
|
|
201
|
+
next_byte = inner[i + 1 : i + 2]
|
|
202
|
+
if next_byte not in (ESC_END, ESC_ESC):
|
|
203
|
+
return False # Invalid escape sequence
|
|
204
|
+
i += 2
|
|
205
|
+
else:
|
|
206
|
+
i += 1
|
|
207
|
+
|
|
208
|
+
return True
|