├── pythonosc
├── py.typed
├── __init__.py
├── test
│ ├── __init__.py
│ ├── parsing
│ │ ├── __init__.py
│ │ ├── test_ntp.py
│ │ └── test_osc_types.py
│ ├── test_osc_bundle_builder.py
│ ├── test_udp_client.py
│ ├── test_osc_packet.py
│ ├── test_tcp_client.py
│ ├── test_osc_message_builder.py
│ ├── test_osc_bundle.py
│ ├── test_osc_server.py
│ ├── test_dispatcher.py
│ ├── test_osc_message.py
│ └── test_osc_tcp_server.py
├── parsing
│ ├── __init__.py
│ ├── ntp.py
│ └── osc_types.py
├── osc_bundle_builder.py
├── osc_packet.py
├── slip.py
├── osc_bundle.py
├── udp_client.py
├── osc_message.py
├── osc_server.py
├── osc_message_builder.py
├── tcp_client.py
├── osc_tcp_server.py
└── dispatcher.py
├── MANIFEST.in
├── CONTRIBUTING.md
├── docs
├── Makefile
├── index.rst
├── make.bat
├── client.rst
├── dispatcher.rst
├── server.rst
└── conf.py
├── examples
├── simple_client.py
├── simple_echo_client.py
├── simple_echo_server.py
├── async_server.py
├── dispatcher.py
├── simple_server.py
├── simple_tcp_client.py
├── simple_tcp_server.py
├── async_tcp_server.py
├── async_simple_tcp_client.py
└── simple_2way.py
├── scripts
└── print_datagrams_main.py
├── .gitignore
├── .github
└── workflows
│ ├── publish-pypi.yml
│ └── python-test.yml
├── .readthedocs.yaml
├── LICENSE.txt
├── CHANGELOG.md
├── pyproject.toml
└── README.rst
/pythonosc/py.typed:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/pythonosc/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/pythonosc/test/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/pythonosc/parsing/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/pythonosc/test/parsing/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/MANIFEST.in:
--------------------------------------------------------------------------------
1 | include README.rst
2 | include LICENSE.txt
3 | include pythonosc/py.typed
4 |
--------------------------------------------------------------------------------
/CONTRIBUTING.md:
--------------------------------------------------------------------------------
1 | Thanks for contributing to this package!
2 |
3 | Before sending a PR, please make sure you checked the [python test workflow](.github/workflows/python-test.yml) and ran it locally, either using [act](https://nektosact.com) or by executing the workflow actions yourself.
4 |
5 | TL;DR:
6 | - Format all code with Black
7 | - Provide type annotations with mypy
8 | - Write and run tests with pytest
9 | - If you're adding a new feature, mention it in the [CHANGELOG.md](CHANGELOG.md) file under the _Unreleased_ section
10 |
11 | Please only send the PR once all of the above is done, thanks!
--------------------------------------------------------------------------------
/docs/Makefile:
--------------------------------------------------------------------------------
1 | # Minimal makefile for Sphinx documentation
2 | #
3 |
4 | # You can set these variables from the command line.
5 | SPHINXOPTS =
6 | SPHINXBUILD = sphinx-build
7 | SOURCEDIR = .
8 | BUILDDIR = _build
9 |
10 | # Put it first so that "make" without argument is like "make help".
11 | help:
12 | @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)
13 |
14 | .PHONY: help Makefile
15 |
16 | # Catch-all target: route all unknown targets to Sphinx using the new
17 | # "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS).
18 | %: Makefile
19 | @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)
--------------------------------------------------------------------------------
/pythonosc/test/parsing/test_ntp.py:
--------------------------------------------------------------------------------
1 | import unittest
2 | import time
3 |
4 | from pythonosc.parsing import ntp
5 |
6 |
7 | class TestNTP(unittest.TestCase):
8 | """TODO: Write real tests for this when I get time..."""
9 |
10 | def test_nto_to_system_time(self):
11 | unix_time = time.time()
12 | timestamp = ntp.system_time_to_ntp(unix_time)
13 | unix_time2 = ntp.ntp_to_system_time(timestamp)
14 | self.assertTrue(type(unix_time) is float)
15 | self.assertTrue(type(timestamp) is bytes)
16 | self.assertTrue(type(unix_time2) is float)
17 | self.assertAlmostEqual(unix_time, unix_time2, places=5)
18 |
19 |
20 | if __name__ == "__main__":
21 | unittest.main()
22 |
--------------------------------------------------------------------------------
/examples/simple_client.py:
--------------------------------------------------------------------------------
1 | """Small example OSC client
2 |
3 | This program sends 10 random values between 0.0 and 1.0 to the /filter address,
4 | waiting for 1 seconds between each value.
5 | """
6 |
7 | import argparse
8 | import random
9 | import time
10 |
11 | from pythonosc import udp_client
12 |
13 | if __name__ == "__main__":
14 | parser = argparse.ArgumentParser()
15 | parser.add_argument("--ip", default="127.0.0.1", help="The ip of the OSC server")
16 | parser.add_argument(
17 | "--port", type=int, default=5005, help="The port the OSC server is listening on"
18 | )
19 | args = parser.parse_args()
20 |
21 | client = udp_client.SimpleUDPClient(args.ip, args.port)
22 |
23 | for x in range(10):
24 | client.send_message("/filter", random.random())
25 | time.sleep(1)
26 |
--------------------------------------------------------------------------------
/scripts/print_datagrams_main.py:
--------------------------------------------------------------------------------
1 | """A simple program that displays datagrams received on a port."""
2 |
3 | import argparse
4 | import socket
5 |
6 |
7 | def main():
8 | parser = argparse.ArgumentParser()
9 | parser.add_argument("--ip", default="127.0.0.1", help="The ip to listen on")
10 | parser.add_argument("--port", type=int, default=5005, help="The port to listen on")
11 |
12 | args = parser.parse_args()
13 | _PrintOscMessages(args.ip, args.port)
14 |
15 |
16 | def _PrintOscMessages(ip, port):
17 | sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
18 | sock.bind((ip, port))
19 | print(f"Listening for UDP packets on {ip}:{port} ...")
20 | while True:
21 | data, _ = sock.recvfrom(1024)
22 | print(f"{data}")
23 |
24 |
25 | if __name__ == "__main__":
26 | main()
27 |
--------------------------------------------------------------------------------
/docs/index.rst:
--------------------------------------------------------------------------------
1 | .. python-osc documentation master file, created by
2 | sphinx-quickstart on Tue Jan 8 15:29:10 2019.
3 | You can adapt this file completely to your liking, but it should at least
4 | contain the root `toctree` directive.
5 |
6 | Python-osc - OSC server and client in pure python
7 | ====================================================
8 |
9 | .. toctree::
10 | :maxdepth: 2
11 | :caption: Contents:
12 |
13 | dispatcher
14 | client
15 | server
16 |
17 |
18 | Python-osc implements a server and client for Open Sound Control.
19 | It allows a python access to a versatile protocol used in many musical instruments, controller hardware and control applications.
20 |
21 |
22 |
23 |
24 |
25 |
26 | Indices and tables
27 | ==================
28 |
29 | * :ref:`genindex`
30 | * :ref:`modindex`
31 | * :ref:`search`
32 |
--------------------------------------------------------------------------------
/examples/simple_echo_client.py:
--------------------------------------------------------------------------------
1 | """Small example OSC client
2 |
3 | This program sends 10 random values between 0.0 and 1.0 to the /filter address,
4 | waiting for 1 seconds between each value.
5 | """
6 |
7 | import argparse
8 | import random
9 | import time
10 |
11 | from pythonosc import udp_client
12 |
13 | if __name__ == "__main__":
14 | parser = argparse.ArgumentParser()
15 | parser.add_argument("--ip", default="127.0.0.1", help="The ip of the OSC server")
16 | parser.add_argument(
17 | "--port", type=int, default=5005, help="The port the OSC server is listening on"
18 | )
19 | args = parser.parse_args()
20 |
21 | client = udp_client.SimpleUDPClient(args.ip, args.port)
22 |
23 | for x in range(10):
24 | client.send_message("/filter", random.random())
25 | reply = next(client.get_messages(2))
26 | print(str(reply))
27 | time.sleep(1)
28 |
--------------------------------------------------------------------------------
/docs/make.bat:
--------------------------------------------------------------------------------
1 | @ECHO OFF
2 |
3 | pushd %~dp0
4 |
5 | REM Command file for Sphinx documentation
6 |
7 | if "%SPHINXBUILD%" == "" (
8 | set SPHINXBUILD=sphinx-build
9 | )
10 | set SOURCEDIR=.
11 | set BUILDDIR=_build
12 |
13 | if "%1" == "" goto help
14 |
15 | %SPHINXBUILD% >NUL 2>NUL
16 | if errorlevel 9009 (
17 | echo.
18 | echo.The 'sphinx-build' command was not found. Make sure you have Sphinx
19 | echo.installed, then set the SPHINXBUILD environment variable to point
20 | echo.to the full path of the 'sphinx-build' executable. Alternatively you
21 | echo.may add the Sphinx directory to PATH.
22 | echo.
23 | echo.If you don't have Sphinx installed, grab it from
24 | echo.http://sphinx-doc.org/
25 | exit /b 1
26 | )
27 |
28 | %SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS%
29 | goto end
30 |
31 | :help
32 | %SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS%
33 |
34 | :end
35 | popd
36 |
--------------------------------------------------------------------------------
/examples/simple_echo_server.py:
--------------------------------------------------------------------------------
1 | """Small example OSC server
2 |
3 | This program listens to several addresses, and prints some information about
4 | received packets.
5 | """
6 |
7 | import argparse
8 | import math
9 |
10 | from pythonosc.dispatcher import Dispatcher
11 | from pythonosc import osc_server
12 |
13 |
14 | def echo_handler(client_addr, unused_addr, args):
15 | print(unused_addr, args)
16 | return (unused_addr, args)
17 |
18 |
19 | if __name__ == "__main__":
20 | parser = argparse.ArgumentParser()
21 | parser.add_argument("--ip", default="127.0.0.1", help="The ip to listen on")
22 | parser.add_argument("--port", type=int, default=5005, help="The port to listen on")
23 | args = parser.parse_args()
24 |
25 | dispatcher = Dispatcher()
26 | dispatcher.set_default_handler(echo_handler, True)
27 |
28 | server = osc_server.ThreadingOSCUDPServer((args.ip, args.port), dispatcher)
29 | print(f"Serving on {server.server_address}")
30 | server.serve_forever()
31 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Byte-compiled / optimized / DLL files
2 | __pycache__/
3 | *.py[cod]
4 |
5 | # C extensions
6 | *.so
7 |
8 | # Distribution / packaging
9 | .Python
10 | env/
11 | build/
12 | develop-eggs/
13 | dist/
14 | downloads/
15 | eggs/
16 | .eggs/
17 | lib/
18 | lib64/
19 | parts/
20 | sdist/
21 | var/
22 | *.egg-info/
23 | .installed.cfg
24 | *.egg
25 |
26 | # PyInstaller
27 | # Usually these files are written by a python script from a template
28 | # before PyInstaller builds the exe, so as to inject date/other infos into it.
29 | *.manifest
30 | *.spec
31 |
32 | # Installer logs
33 | pip-log.txt
34 | pip-delete-this-directory.txt
35 |
36 | # Unit test / coverage reports
37 | htmlcov/
38 | .tox/
39 | .coverage
40 | .coverage.*
41 | .cache
42 | nosetests.xml
43 | coverage.xml
44 | *,cover
45 |
46 | # Translations
47 | *.mo
48 | *.pot
49 |
50 | # Django stuff:
51 | *.log
52 |
53 | # Sphinx documentation
54 | docs/_build/
55 |
56 | # act
57 | bin/
58 |
59 | # PyBuilder
60 | target/
61 |
62 | # PyCharm
63 | .idea/
64 |
--------------------------------------------------------------------------------
/.github/workflows/publish-pypi.yml:
--------------------------------------------------------------------------------
1 | name: Publish to PyPI
2 | on: push
3 | jobs:
4 | build-n-publish:
5 | name: Build and publish to PyPI
6 | runs-on: ubuntu-latest
7 | permissions:
8 | # IMPORTANT: this permission is mandatory for trusted publishing
9 | id-token: write
10 | steps:
11 | - uses: actions/checkout@v4
12 | - name: Set up Python
13 | uses: actions/setup-python@v5
14 | with:
15 | python-version: "3.x"
16 | - name: Install pypa/build
17 | run: >-
18 | python3 -m
19 | pip install
20 | build
21 | --user
22 | - name: Build a binary wheel and a source tarball
23 | run: >-
24 | python3 -m
25 | build
26 | --sdist
27 | --wheel
28 | --outdir dist/
29 | .
30 | - name: Publish distribution 📦 to PyPI
31 | if: startsWith(github.ref, 'refs/tags')
32 | uses: pypa/gh-action-pypi-publish@release/v1
--------------------------------------------------------------------------------
/.readthedocs.yaml:
--------------------------------------------------------------------------------
1 | # Read the Docs configuration file for Sphinx projects
2 | # See https://docs.readthedocs.io/en/stable/config-file/v2.html for details
3 |
4 | # Required
5 | version: 2
6 |
7 | # Set the OS, Python version and other tools you might need
8 | build:
9 | os: ubuntu-22.04
10 | tools:
11 | python: "3.12"
12 | # You can also specify other tool versions:
13 | # nodejs: "20"
14 | # rust: "1.70"
15 | # golang: "1.20"
16 |
17 | # Build documentation in the "docs/" directory with Sphinx
18 | sphinx:
19 | configuration: docs/conf.py
20 | # You can configure Sphinx to use a different builder, for instance use the dirhtml builder for simpler URLs
21 | # builder: "dirhtml"
22 | # Fail on all warnings to avoid broken references
23 | # fail_on_warning: true
24 |
25 | # Optionally build your docs in additional formats such as PDF and ePub
26 | # formats:
27 | # - pdf
28 | # - epub
29 |
30 | # Optional but recommended, declare the Python requirements required
31 | # to build your documentation
32 | # See https://docs.readthedocs.io/en/stable/guides/reproducible-builds.html
33 | # python:
34 | # install:
35 | # - requirements: docs/requirements.txt
36 |
--------------------------------------------------------------------------------
/examples/async_server.py:
--------------------------------------------------------------------------------
1 | import sys
2 | from pythonosc.osc_server import AsyncIOOSCUDPServer
3 | from pythonosc.dispatcher import Dispatcher
4 | import asyncio
5 |
6 |
7 | def filter_handler(address, *args):
8 | print(f"{address}: {args}")
9 |
10 |
11 | dispatcher = Dispatcher()
12 | dispatcher.map("/filter", filter_handler)
13 |
14 | ip = "127.0.0.1"
15 | port = 1337
16 |
17 |
18 | async def loop():
19 | """Example main loop that only runs for 10 iterations before finishing"""
20 | for i in range(10):
21 | print(f"Loop {i}")
22 | await asyncio.sleep(1)
23 |
24 |
25 | async def init_main():
26 | server = AsyncIOOSCUDPServer((ip, port), dispatcher, asyncio.get_event_loop())
27 | transport, protocol = (
28 | await server.create_serve_endpoint()
29 | ) # Create datagram endpoint and start serving
30 |
31 | await loop() # Enter main loop of program
32 |
33 | transport.close() # Clean up serve endpoint
34 |
35 |
36 | if sys.version_info >= (3, 7):
37 | asyncio.run(init_main())
38 | else:
39 | # TODO(python-upgrade): drop this once 3.6 is no longer supported
40 | event_loop = asyncio.get_event_loop()
41 | event_loop.run_until_complete(init_main())
42 | event_loop.close()
43 |
--------------------------------------------------------------------------------
/examples/dispatcher.py:
--------------------------------------------------------------------------------
1 | from pythonosc.dispatcher import Dispatcher
2 | from typing import List, Any
3 | from pythonosc.osc_server import BlockingOSCUDPServer
4 | from pythonosc.udp_client import SimpleUDPClient
5 |
6 | dispatcher = Dispatcher()
7 |
8 |
9 | def set_filter(address: str, *args: List[Any]) -> None:
10 | # We expect two float arguments
11 | if not len(args) == 2 or type(args[0]) is not float or type(args[1]) is not float:
12 | return
13 |
14 | # Check that address starts with filter
15 | if not address[:-1] == "/filter": # Cut off the last character
16 | return
17 |
18 | value1 = args[0]
19 | value2 = args[1]
20 | filterno = address[-1]
21 | print(f"Setting filter {filterno} values: {value1}, {value2}")
22 |
23 |
24 | dispatcher.map("/filter*", set_filter) # Map wildcard address to set_filter function
25 |
26 | # Set up server and client for testing
27 |
28 | server = BlockingOSCUDPServer(("127.0.0.1", 1337), dispatcher)
29 | client = SimpleUDPClient("127.0.0.1", 1337)
30 |
31 | # Send message and receive exactly one message (blocking)
32 | client.send_message("/filter1", [1.0, 2.0])
33 | server.handle_request()
34 |
35 | client.send_message("/filter8", [6.0, -2.0])
36 | server.handle_request()
37 |
--------------------------------------------------------------------------------
/examples/simple_server.py:
--------------------------------------------------------------------------------
1 | """Small example OSC server
2 |
3 | This program listens to several addresses, and prints some information about
4 | received packets.
5 | """
6 |
7 | import argparse
8 | import math
9 |
10 | from pythonosc.dispatcher import Dispatcher
11 | from pythonosc import osc_server
12 |
13 |
14 | def print_volume_handler(unused_addr, args, volume):
15 | print(f"[{args[0]}] ~ {volume}")
16 |
17 |
18 | def print_compute_handler(unused_addr, args, volume):
19 | try:
20 | print(f"[{args[0]}] ~ {args[1](volume)}")
21 | except ValueError:
22 | pass
23 |
24 |
25 | if __name__ == "__main__":
26 | parser = argparse.ArgumentParser()
27 | parser.add_argument("--ip", default="127.0.0.1", help="The ip to listen on")
28 | parser.add_argument("--port", type=int, default=5005, help="The port to listen on")
29 | args = parser.parse_args()
30 |
31 | dispatcher = Dispatcher()
32 | dispatcher.map("/filter", print)
33 | dispatcher.map("/volume", print_volume_handler, "Volume")
34 | dispatcher.map("/logvolume", print_compute_handler, "Log volume", math.log)
35 |
36 | server = osc_server.ThreadingOSCUDPServer((args.ip, args.port), dispatcher)
37 | print(f"Serving on {server.server_address}")
38 | server.serve_forever()
39 |
--------------------------------------------------------------------------------
/examples/simple_tcp_client.py:
--------------------------------------------------------------------------------
1 | """Small example OSC client
2 |
3 | This program sends 10 random values between 0.0 and 1.0 to the /filter address,
4 | and listens for incoming messages for 1 second between each value.
5 | """
6 |
7 | import argparse
8 | import random
9 |
10 | from pythonosc import tcp_client
11 |
12 | if __name__ == "__main__":
13 | parser = argparse.ArgumentParser()
14 | parser.add_argument("--ip", default="127.0.0.1", help="The ip of the OSC server")
15 | parser.add_argument(
16 | "--port", type=int, default=5005, help="The port the OSC server is listening on"
17 | )
18 | parser.add_argument(
19 | "--mode",
20 | default="1.1",
21 | help="The OSC protocol version of the server (default is 1.1)",
22 | )
23 | args = parser.parse_args()
24 |
25 | with tcp_client.SimpleTCPClient(args.ip, args.port, mode=args.mode) as client:
26 | for x in range(10):
27 | n = random.random()
28 | print(f"Sending /filter {n}")
29 | client.send_message("/filter", n)
30 | resp = client.get_messages(1)
31 | for r in resp:
32 | try:
33 | print(r)
34 | except Exception as e:
35 | print(f"oops {str(e)}: {r}")
36 |
--------------------------------------------------------------------------------
/LICENSE.txt:
--------------------------------------------------------------------------------
1 | This is free and unencumbered software released into the public domain.
2 |
3 | Anyone is free to copy, modify, publish, use, compile, sell, or
4 | distribute this software, either in source code form or as a compiled
5 | binary, for any purpose, commercial or non-commercial, and by any
6 | means.
7 |
8 | In jurisdictions that recognize copyright laws, the author or authors
9 | of this software dedicate any and all copyright interest in the
10 | software to the public domain. We make this dedication for the benefit
11 | of the public at large and to the detriment of our heirs and
12 | successors. We intend this dedication to be an overt act of
13 | relinquishment in perpetuity of all present and future rights to this
14 | software under copyright law.
15 |
16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
19 | IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR
20 | OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
21 | ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
22 | OTHER DEALINGS IN THE SOFTWARE.
23 |
24 | For more information, please refer to
25 |
--------------------------------------------------------------------------------
/examples/simple_tcp_server.py:
--------------------------------------------------------------------------------
1 | """Small example OSC server
2 |
3 | This program listens to the specified address and port, and prints some information about
4 | received packets.
5 | """
6 |
7 | import argparse
8 | import math
9 |
10 | from pythonosc import osc_tcp_server
11 | from pythonosc.dispatcher import Dispatcher
12 |
13 |
14 | def print_volume_handler(unused_addr, args, volume):
15 | print(f"[{args[0]}] ~ {volume}")
16 |
17 |
18 | def print_compute_handler(unused_addr, args, volume):
19 | try:
20 | print(f"[{args[0]}] ~ {args[1](volume)}")
21 | except ValueError:
22 | pass
23 |
24 |
25 | if __name__ == "__main__":
26 | parser = argparse.ArgumentParser()
27 | parser.add_argument("--ip", default="127.0.0.1", help="The ip to listen on")
28 | parser.add_argument("--port", type=int, default=5005, help="The port to listen on")
29 | parser.add_argument(
30 | "--mode",
31 | default="1.1",
32 | help="The OSC protocol version of the server (default is 1.1)",
33 | )
34 |
35 | args = parser.parse_args()
36 |
37 | dispatcher = Dispatcher()
38 | dispatcher.map("/filter", print)
39 | dispatcher.map("/volume", print_volume_handler, "Volume")
40 | dispatcher.map("/logvolume", print_compute_handler, "Log volume", math.log)
41 |
42 | server = osc_tcp_server.ThreadingOSCTCPServer(
43 | (args.ip, args.port), dispatcher, mode=args.mode
44 | )
45 | print(f"Serving on {server.server_address}")
46 | server.serve_forever()
47 |
--------------------------------------------------------------------------------
/docs/client.rst:
--------------------------------------------------------------------------------
1 | Client
2 | ========
3 |
4 | The client allows you to connect and exchange messages with an OSC server.
5 | Client classes are available for UDP and TCP protocols.
6 | The base client class ``send`` method expects an :class:`OSCMessage` object, which is then sent out over TCP or UDP.
7 | Additionally, a simple client class exists that constructs the :class:`OSCMessage` object for you.
8 |
9 | See the examples folder for more use cases.
10 |
11 | Examples
12 | ---------
13 |
14 | .. code-block:: python
15 |
16 | from pythonosc.udp_client import SimpleUDPClient
17 |
18 | ip = "127.0.0.1"
19 | port = 1337
20 |
21 | client = SimpleUDPClient(ip, port) # Create client
22 |
23 | client.send_message("/some/address", 123) # Send float message
24 | client.send_message("/some/address", [1, 2., "hello"]) # Send message with int, float and string
25 |
26 |
27 | .. code-block:: python
28 |
29 | from pythonosc.tcp_client import SimpleTCPClient
30 |
31 | ip = "127.0.0.1"
32 | port = 1337
33 |
34 | client = SimpleTCPClient(ip, port) # Create client
35 |
36 | client.send_message("/some/address", 123) # Send float message
37 | client.send_message("/some/address", [1, 2., "hello"]) # Send message with int, float and string
38 |
39 | Client Module Documentation
40 | ---------------------------------
41 |
42 | .. automodule:: pythonosc.udp_client
43 | :special-members:
44 | :members:
45 | :exclude-members: __weakref__
46 |
47 | .. automodule:: pythonosc.tcp_client
48 | :special-members:
49 | :members:
50 | :exclude-members: __weakref__
51 |
--------------------------------------------------------------------------------
/examples/async_tcp_server.py:
--------------------------------------------------------------------------------
1 | import argparse
2 | import asyncio
3 | import sys
4 |
5 | from pythonosc.dispatcher import Dispatcher
6 | from pythonosc.osc_tcp_server import AsyncOSCTCPServer
7 |
8 |
9 | def filter_handler(address, *args):
10 | print(f"{address}: {args}")
11 |
12 |
13 | dispatcher = Dispatcher()
14 | dispatcher.map("/filter", filter_handler)
15 |
16 |
17 | async def loop():
18 | """Example main loop that only runs for 10 iterations before finishing"""
19 | for i in range(10):
20 | print(f"Loop {i}")
21 | await asyncio.sleep(10)
22 |
23 |
24 | async def init_main():
25 | parser = argparse.ArgumentParser()
26 | parser.add_argument("--ip", default="127.0.0.1", help="The ip of the OSC server")
27 | parser.add_argument(
28 | "--port", type=int, default=5005, help="The port the OSC server is listening on"
29 | )
30 | parser.add_argument(
31 | "--mode",
32 | default="1.1",
33 | help="The OSC protocol version of the server (default is 1.1)",
34 | )
35 | args = parser.parse_args()
36 |
37 | async with AsyncOSCTCPServer(
38 | args.ip, args.port, dispatcher, mode=args.mode
39 | ) as server:
40 | async with asyncio.TaskGroup() as tg:
41 | tg.create_task(server.start())
42 | tg.create_task(loop())
43 |
44 |
45 | if sys.version_info >= (3, 7):
46 | asyncio.run(init_main())
47 | else:
48 | # TODO(python-upgrade): drop this once 3.6 is no longer supported
49 | event_loop = asyncio.get_event_loop()
50 | event_loop.run_until_complete(init_main())
51 | event_loop.close()
52 |
--------------------------------------------------------------------------------
/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | # Change Log
2 | All notable changes to this project will be documented in this file.
3 |
4 | The format is based on [Keep a Changelog](http://keepachangelog.com/) and this project adheres to [Semantic Versioning](http://semver.org/).
5 |
6 | ## [Unreleased]
7 |
8 | - Added TCPDispatchClient to tcp_client
9 | - Fixed TPC dispatcher type annotations
10 |
11 | ## [1.9.3]
12 |
13 | - Reinstate mistakenly deleted package type annotations on main branch (again)
14 |
15 | ## [1.9.2]
16 |
17 | - Reinstate mistakenly deleted package type annotations on main branch
18 |
19 | ## [1.9.1]
20 |
21 | - Reinstate mistakenly deleted package type annotations
22 |
23 | ## [1.9.0]
24 |
25 | - Added TCP Client and Server support for OSC 1.0 and OSC 1.1 formats, with support for sending responses to the client
26 | - Added response support to the existing UDP Client and Server code
27 |
28 | ## [1.8.3]
29 |
30 | - Using trusted publisher setup to publish to pypi
31 |
32 | ## [1.8.2]
33 |
34 | - Changed packaging method to pypa/build
35 | - Removed pygame example to simplify dependencies
36 |
37 | ## [1.8.1]
38 |
39 | - Add options to UdpClient to force the use of IPv4 when IPv6 is available and vice versa
40 |
41 | - Add support for arguments with Nil type
42 |
43 | ### [1.8.0]
44 |
45 | - Support for sending and receiving Int64 datatype (`h`).
46 |
47 | ## [1.7.7]
48 |
49 | ### Fixed
50 |
51 | Flaky NTP test
52 |
53 | ## [1.7.6]
54 |
55 | ### Added
56 |
57 | - Support for python 3.7 and 3.8.
58 |
59 | - Releasing wheel on top of source package.
60 |
61 | ## [1.7.4]
62 |
63 | ### Added
64 |
65 | - Support for sending nil values.
66 |
67 | - IPV6 support to UDPClient.
68 |
69 | ### Fixed
70 |
71 | Timestamp parsing
72 |
--------------------------------------------------------------------------------
/pythonosc/test/test_osc_bundle_builder.py:
--------------------------------------------------------------------------------
1 | import unittest
2 |
3 | from pythonosc import osc_bundle_builder
4 | from pythonosc import osc_message_builder
5 |
6 |
7 | class TestOscBundleBuilder(unittest.TestCase):
8 | def test_empty_bundle(self):
9 | bundle = osc_bundle_builder.OscBundleBuilder(
10 | osc_bundle_builder.IMMEDIATELY
11 | ).build()
12 | self.assertEqual(0, bundle.num_contents)
13 |
14 | def test_raises_on_build(self):
15 | bundle = osc_bundle_builder.OscBundleBuilder(0.0)
16 | bundle.add_content(None)
17 | self.assertRaises(osc_bundle_builder.BuildError, bundle.build)
18 |
19 | def test_raises_on_invalid_timestamp(self):
20 | bundle = osc_bundle_builder.OscBundleBuilder("I am not a timestamp")
21 | self.assertRaises(osc_bundle_builder.BuildError, bundle.build)
22 |
23 | def test_build_complex_bundle(self):
24 | bundle = osc_bundle_builder.OscBundleBuilder(osc_bundle_builder.IMMEDIATELY)
25 | msg = osc_message_builder.OscMessageBuilder(address="/SYNC")
26 | msg.add_arg(4.0)
27 | # Add 4 messages in the bundle, each with more arguments.
28 | bundle.add_content(msg.build())
29 | msg.add_arg(2)
30 | bundle.add_content(msg.build())
31 | msg.add_arg("value")
32 | bundle.add_content(msg.build())
33 | msg.add_arg(b"\x01\x02\x03")
34 | bundle.add_content(msg.build())
35 |
36 | sub_bundle = bundle.build()
37 | # Now add the same bundle inside itself.
38 | bundle.add_content(sub_bundle)
39 |
40 | bundle = bundle.build()
41 | self.assertEqual(5, bundle.num_contents)
42 |
43 |
44 | if __name__ == "__main__":
45 | unittest.main()
46 |
--------------------------------------------------------------------------------
/examples/async_simple_tcp_client.py:
--------------------------------------------------------------------------------
1 | """Small example Asynchronous OSC TCP client
2 |
3 | This program listens for incoming messages in one task, and
4 | sends 10 random values between 0.0 and 1.0 to the /filter address,
5 | waiting for 1 seconds between each value in a second task.
6 | """
7 |
8 | import argparse
9 | import asyncio
10 | import random
11 | import sys
12 |
13 | from pythonosc import tcp_client
14 |
15 |
16 | async def get_messages(client):
17 | async for msg in client.get_messages(60):
18 | print(msg)
19 |
20 |
21 | async def send_messages(client):
22 | for x in range(10):
23 | r = random.random()
24 | print(f"Sending /filter {r}")
25 | await client.send_message("/filter", r)
26 | await asyncio.sleep(1)
27 |
28 |
29 | async def init_main():
30 | parser = argparse.ArgumentParser()
31 | parser.add_argument("--ip", default="127.0.0.1", help="The ip of the OSC server")
32 | parser.add_argument(
33 | "--port", type=int, default=5005, help="The port the OSC server is listening on"
34 | )
35 | parser.add_argument(
36 | "--mode",
37 | default="1.1",
38 | help="The OSC protocol version of the server (default is 1.1)",
39 | )
40 | args = parser.parse_args()
41 |
42 | async with tcp_client.AsyncSimpleTCPClient(
43 | args.ip, args.port, mode=args.mode
44 | ) as client:
45 | async with asyncio.TaskGroup() as tg:
46 | tg.create_task(get_messages(client))
47 | tg.create_task(send_messages(client))
48 |
49 |
50 | if sys.version_info >= (3, 7):
51 | asyncio.run(init_main())
52 | else:
53 | # TODO(python-upgrade): drop this once 3.6 is no longer supported
54 | event_loop = asyncio.get_event_loop()
55 | event_loop.run_until_complete(init_main())
56 | event_loop.close()
57 |
--------------------------------------------------------------------------------
/pyproject.toml:
--------------------------------------------------------------------------------
1 | [build-system]
2 | requires = ["setuptools"]
3 | build-backend = "setuptools.build_meta"
4 |
5 | [project]
6 | name = "python-osc"
7 | version = "1.9.3"
8 | description = "Open Sound Control server and client implementations in pure Python"
9 | readme = "README.rst"
10 | requires-python = ">=3.10"
11 | license = { file = "LICENSE.txt" }
12 | authors = [
13 | { name = "attwad", email = "tmusoft@gmail.com" },
14 | ]
15 | keywords = ["osc", "sound", "midi", "music"]
16 | classifiers = [
17 | "Development Status :: 5 - Production/Stable",
18 | "Intended Audience :: Developers",
19 | "License :: Freely Distributable",
20 | "Programming Language :: Python :: 3",
21 | "Topic :: Multimedia :: Sound/Audio",
22 | "Topic :: System :: Networking",
23 | ]
24 |
25 | [project.urls]
26 | Repository = "https://github.com/attwad/python-osc"
27 |
28 | [tool.mypy]
29 | # Would be great to turn this on, however there's too many cases it would break
30 | # right now.
31 | # disallow_any_generics = true
32 |
33 | disallow_subclassing_any = true
34 |
35 | # Allow functions _without_ type annotations, but require that annotations be
36 | # complete (possibly including the `Any` type) where they are present.
37 | disallow_incomplete_defs = true
38 | # check_untyped_defs = true
39 | disallow_untyped_decorators = true
40 |
41 | # # Would be great to turn these on eventually
42 | # no_implicit_optional = true
43 | # strict_optional = true
44 |
45 | warn_redundant_casts = true
46 | warn_unused_ignores = true
47 | show_error_codes = true
48 | # # Would be great to turn this on eventually
49 | # # warn_return_any = true
50 | # warn_unreachable = true
51 |
52 | # implicit_reexport = False
53 | # strict_equality = true
54 |
55 | scripts_are_modules = true
56 | warn_unused_configs = true
57 |
58 | enable_error_code = "ignore-without-code"
59 |
--------------------------------------------------------------------------------
/.github/workflows/python-test.yml:
--------------------------------------------------------------------------------
1 | # This workflow will install Python dependencies, run tests and lint with a variety of Python versions
2 | # For more information see: https://help.github.com/actions/language-and-framework-guides/using-python-with-github-actions
3 |
4 | name: Test
5 |
6 | permissions: read-all
7 |
8 | on: [push, pull_request]
9 |
10 | jobs:
11 | build:
12 | runs-on: ubuntu-latest
13 | strategy:
14 | fail-fast: false
15 | matrix:
16 | python-version: ['3.10', '3.11', '3.12']
17 |
18 | steps:
19 | - uses: actions/checkout@v4
20 | - name: Set up Python ${{ matrix.python-version }}
21 | uses: actions/setup-python@v5
22 | with:
23 | python-version: ${{ matrix.python-version }}
24 | - name: Install dependencies
25 | run: |
26 | python -m pip install --upgrade pip
27 | python -m pip install flake8 pytest mypy
28 | - name: Lint with flake8
29 | run: |
30 | # stop the build if there are Python syntax errors or undefined names
31 | flake8 . --count --select=E9,F63,F7,F82 --show-source --statistics
32 | - name: Check with mypy
33 | run: mypy pythonosc examples
34 | - name: Test with pytest
35 | run: pytest
36 | lint:
37 | runs-on: ubuntu-latest
38 | steps:
39 | - uses: actions/checkout@v3
40 | - uses: psf/black@stable
41 |
42 | check-types-published:
43 | runs-on: ubuntu-latest
44 | steps:
45 | - uses: actions/checkout@v3
46 | - uses: actions/setup-python@v5
47 | with:
48 | python-version: '3.12'
49 | - run: |
50 | pip install build
51 | python -m build --sdist --wheel
52 |
53 | temp=$(mktemp -d)
54 |
55 | python -m venv $temp/venv
56 | source $temp/venv/bin/activate
57 |
58 | pip install mypy ./dist/*whl
59 |
60 | cd $temp
61 |
62 | echo 'import pythonosc' > demo.py
63 | mypy demo.py
64 |
--------------------------------------------------------------------------------
/pythonosc/test/test_udp_client.py:
--------------------------------------------------------------------------------
1 | import unittest
2 | from unittest import mock
3 |
4 | from pythonosc import osc_message_builder
5 | from pythonosc import udp_client
6 |
7 |
8 | class TestUdpClient(unittest.TestCase):
9 | @mock.patch("socket.socket")
10 | def test_send(self, mock_socket_ctor):
11 | mock_socket = mock_socket_ctor.return_value
12 | client = udp_client.UDPClient("::1", 31337)
13 |
14 | msg = osc_message_builder.OscMessageBuilder("/").build()
15 | client.send(msg)
16 |
17 | self.assertTrue(mock_socket.sendto.called)
18 | mock_socket.sendto.assert_called_once_with(msg.dgram, ("::1", 31337))
19 |
20 |
21 | class TestSimpleUdpClient(unittest.TestCase):
22 | def setUp(self):
23 | self.patcher = mock.patch("pythonosc.udp_client.OscMessageBuilder")
24 | self.patcher.start()
25 | self.builder = udp_client.OscMessageBuilder.return_value
26 | self.msg = self.builder.build.return_value
27 | self.client = mock.Mock()
28 |
29 | def tearDown(self):
30 | self.patcher.stop()
31 |
32 | def test_send_message_calls_send_with_msg(self):
33 | udp_client.SimpleUDPClient.send_message(self.client, "/address", 1)
34 | self.client.send.assert_called_once_with(self.msg)
35 |
36 | def test_send_message_calls_add_arg_with_value(self):
37 | udp_client.SimpleUDPClient.send_message(self.client, "/address", 1)
38 | self.builder.add_arg.assert_called_once_with(1)
39 |
40 | def test_send_message_calls_add_arg_once_with_string(self):
41 | udp_client.SimpleUDPClient.send_message(self.client, "/address", "hello")
42 | self.builder.add_arg.assert_called_once_with("hello")
43 |
44 | def test_send_message_calls_add_arg_multiple_times_with_list(self):
45 | udp_client.SimpleUDPClient.send_message(
46 | self.client, "/address", [1, "john", True]
47 | )
48 | self.assertEqual(self.builder.add_arg.call_count, 3)
49 |
50 |
51 | if __name__ == "__main__":
52 | unittest.main()
53 |
--------------------------------------------------------------------------------
/pythonosc/parsing/ntp.py:
--------------------------------------------------------------------------------
1 | """Parsing and conversion of NTP dates contained in datagrams."""
2 |
3 | import datetime
4 | import struct
5 | import time
6 |
7 | from typing import NamedTuple
8 |
9 | # 63 zero bits followed by a one in the least signifigant bit is a special
10 | # case meaning "immediately."
11 | IMMEDIATELY = struct.pack(">Q", 1)
12 |
13 | # timetag * (1 / 2 ** 32) == l32bits + (r32bits / 1 ** 32)
14 | _NTP_TIMESTAMP_TO_SECONDS = 1.0 / 2.0**32.0
15 | _SECONDS_TO_NTP_TIMESTAMP = 2.0**32.0
16 |
17 | # From NTP lib.
18 | _SYSTEM_EPOCH = datetime.date(*time.gmtime(0)[0:3])
19 | _NTP_EPOCH = datetime.date(1900, 1, 1)
20 | # _NTP_DELTA is 2208988800
21 | _NTP_DELTA = (_SYSTEM_EPOCH - _NTP_EPOCH).days * 24 * 3600
22 |
23 |
24 | Timestamp = NamedTuple(
25 | "Timestamp",
26 | [
27 | ("seconds", int),
28 | ("fraction", int),
29 | ],
30 | )
31 |
32 |
33 | class NtpError(Exception):
34 | """Base class for ntp module errors."""
35 |
36 |
37 | def parse_timestamp(timestamp: int) -> Timestamp:
38 | """Parse NTP timestamp as Timetag."""
39 | seconds = timestamp >> 32
40 | fraction = timestamp & 0xFFFFFFFF
41 | return Timestamp(seconds, fraction)
42 |
43 |
44 | def ntp_to_system_time(timestamp: bytes) -> float:
45 | """Convert a NTP timestamp to system time in seconds."""
46 | try:
47 | ts = struct.unpack(">Q", timestamp)[0]
48 | except Exception as e:
49 | raise NtpError(e)
50 | return ts * _NTP_TIMESTAMP_TO_SECONDS - _NTP_DELTA
51 |
52 |
53 | def system_time_to_ntp(seconds: float) -> bytes:
54 | """Convert a system time in seconds to NTP timestamp."""
55 | try:
56 | seconds = seconds + _NTP_DELTA
57 | except TypeError as e:
58 | raise NtpError(e)
59 | return struct.pack(">Q", int(seconds * _SECONDS_TO_NTP_TIMESTAMP))
60 |
61 |
62 | def ntp_time_to_system_epoch(seconds: float) -> float:
63 | """Convert a NTP time in seconds to system time in seconds."""
64 | return seconds - _NTP_DELTA
65 |
66 |
67 | def system_time_to_ntp_epoch(seconds: float) -> float:
68 | """Convert a system time in seconds to NTP time in seconds."""
69 | return seconds + _NTP_DELTA
70 |
--------------------------------------------------------------------------------
/pythonosc/osc_bundle_builder.py:
--------------------------------------------------------------------------------
1 | """Build OSC bundles for client applications."""
2 |
3 | from typing import List
4 |
5 | from pythonosc import osc_bundle
6 | from pythonosc import osc_message
7 | from pythonosc.parsing import osc_types
8 |
9 | # Shortcut to specify an immediate execution of messages in the bundle.
10 | IMMEDIATELY = osc_types.IMMEDIATELY
11 |
12 |
13 | class BuildError(Exception):
14 | """Error raised when an error occurs building the bundle."""
15 |
16 |
17 | class OscBundleBuilder(object):
18 | """Builds arbitrary OscBundle instances."""
19 |
20 | def __init__(self, timestamp: int) -> None:
21 | """Build a new bundle with the associated timestamp.
22 |
23 | Args:
24 | - timestamp: system time represented as a floating point number of
25 | seconds since the epoch in UTC or IMMEDIATELY.
26 | """
27 | self._timestamp = timestamp
28 | self._contents: List[osc_bundle.OscBundle | osc_message.OscMessage] = []
29 |
30 | def add_content(
31 | self, content: osc_bundle.OscBundle | osc_message.OscMessage
32 | ) -> None:
33 | """Add a new content to this bundle.
34 |
35 | Args:
36 | - content: Either an OscBundle or an OscMessage
37 | """
38 | self._contents.append(content)
39 |
40 | def build(self) -> osc_bundle.OscBundle:
41 | """Build an OscBundle with the current state of this builder.
42 |
43 | Raises:
44 | - BuildError: if we could not build the bundle.
45 | """
46 | dgram = b"#bundle\x00"
47 | try:
48 | dgram += osc_types.write_date(self._timestamp)
49 | for content in self._contents:
50 | if isinstance(content, osc_message.OscMessage) or isinstance(
51 | content, osc_bundle.OscBundle
52 | ):
53 | size = content.size
54 | dgram += osc_types.write_int(size)
55 | dgram += content.dgram
56 | else:
57 | raise BuildError(
58 | f"Content must be either OscBundle or OscMessage, found {type(content)}"
59 | )
60 | return osc_bundle.OscBundle(dgram)
61 | except osc_types.BuildError as be:
62 | raise BuildError(f"Could not build the bundle {be}")
63 |
--------------------------------------------------------------------------------
/pythonosc/test/test_osc_packet.py:
--------------------------------------------------------------------------------
1 | import unittest
2 |
3 | from pythonosc import osc_packet
4 |
5 | _DGRAM_TWO_MESSAGES_IN_BUNDLE = (
6 | b"#bundle\x00"
7 | b"\x00\x00\x00\x00\x00\x00\x00\x01"
8 | # First message.
9 | b"\x00\x00\x00\x10"
10 | b"/SYNC\x00\x00\x00"
11 | b",f\x00\x00"
12 | b"?\x00\x00\x00"
13 | # Second message, same.
14 | b"\x00\x00\x00\x10"
15 | b"/SYNC\x00\x00\x00"
16 | b",f\x00\x00"
17 | b"?\x00\x00\x00"
18 | )
19 |
20 | _DGRAM_EMPTY_BUNDLE = b"#bundle\x00" b"\x00\x00\x00\x00\x00\x00\x00\x01"
21 |
22 | _DGRAM_NESTED_MESS = (
23 | b"#bundle\x00"
24 | b"\x10\x00\x00\x00\x00\x00\x00\x00"
25 | # First message.
26 | b"\x00\x00\x00\x10" # 16 bytes
27 | b"/1111\x00\x00\x00"
28 | b",f\x00\x00"
29 | b"?\x00\x00\x00"
30 | # Second message, same.
31 | b"\x00\x00\x00\x10" # 16 bytes
32 | b"/2222\x00\x00\x00"
33 | b",f\x00\x00"
34 | b"?\x00\x00\x00"
35 | # Now another bundle within it, oh my...
36 | b"\x00\x00\x00$" # 36 bytes.
37 | b"#bundle\x00"
38 | b"\x20\x00\x00\x00\x00\x00\x00\x00"
39 | # First message.
40 | b"\x00\x00\x00\x10"
41 | b"/3333\x00\x00\x00"
42 | b",f\x00\x00"
43 | b"?\x00\x00\x00"
44 | # And another final bundle.
45 | b"\x00\x00\x00$" # 36 bytes.
46 | b"#bundle\x00"
47 | b"\x15\x00\x00\x00\x00\x00\x00\x01" # Immediately this one.
48 | # First message.
49 | b"\x00\x00\x00\x10"
50 | b"/4444\x00\x00\x00"
51 | b",f\x00\x00"
52 | b"?\x00\x00\x00"
53 | )
54 |
55 |
56 | class TestOscPacket(unittest.TestCase):
57 | def test_two_messages_in_a_bundle(self):
58 | packet = osc_packet.OscPacket(_DGRAM_TWO_MESSAGES_IN_BUNDLE)
59 | self.assertEqual(2, len(packet.messages))
60 |
61 | def test_empty_dgram_raises_exception(self):
62 | self.assertRaises(osc_packet.ParseError, osc_packet.OscPacket, b"")
63 |
64 | def test_empty_bundle(self):
65 | packet = osc_packet.OscPacket(_DGRAM_EMPTY_BUNDLE)
66 | self.assertEqual(0, len(packet.messages))
67 |
68 | def test_nested_mess_bundle(self):
69 | packet = osc_packet.OscPacket(_DGRAM_NESTED_MESS)
70 | self.assertEqual(4, len(packet.messages))
71 | self.assertTrue(packet.messages[0][0], packet.messages[1][0])
72 | self.assertTrue(packet.messages[1][0], packet.messages[2][0])
73 | self.assertTrue(packet.messages[2][0], packet.messages[3][0])
74 |
75 |
76 | if __name__ == "__main__":
77 | unittest.main()
78 |
--------------------------------------------------------------------------------
/pythonosc/test/test_tcp_client.py:
--------------------------------------------------------------------------------
1 | import asyncio
2 | import unittest
3 | from unittest import mock
4 |
5 | from pythonosc import osc_message_builder, slip, tcp_client
6 |
7 |
8 | class TestTcpClient(unittest.TestCase):
9 | @mock.patch("socket.socket")
10 | def test_client(self, mock_socket_ctor):
11 | mock_socket = mock_socket_ctor.return_value
12 | mock_send = mock.Mock()
13 | mock_recv = mock.Mock()
14 | mock_send.return_value = None
15 | mock_recv.return_value = ""
16 |
17 | mock_socket.sendall = mock_send
18 | mock_socket.recv = mock_recv
19 | msg = osc_message_builder.OscMessageBuilder("/").build()
20 | with tcp_client.TCPClient("::1", 31337) as client:
21 | client.send(msg)
22 | mock_socket.sendall.assert_called_once_with(slip.encode(msg.dgram))
23 |
24 | @mock.patch("socket.socket")
25 | def test_simple_client(self, mock_socket_ctor):
26 | mock_socket = mock_socket_ctor.return_value
27 | mock_send = mock.Mock()
28 | mock_recv = mock.Mock()
29 | mock_send.return_value = None
30 | mock_recv.return_value = ""
31 |
32 | mock_socket.sendall = mock_send
33 | mock_socket.recv = mock_recv
34 | with tcp_client.SimpleTCPClient("::1", 31337) as client:
35 | client.send_message("/", [])
36 | mock_socket.sendall.assert_called_once()
37 |
38 |
39 | class TestAsyncTcpClient(unittest.IsolatedAsyncioTestCase):
40 | @mock.patch("asyncio.open_connection")
41 | async def test_send(self, mock_socket_ctor):
42 | mock_reader = mock.Mock()
43 | mock_writer = mock.Mock()
44 | mock_writer.drain = mock.AsyncMock()
45 | mock_writer.wait_closed = mock.AsyncMock()
46 | mock_socket_ctor.return_value = (mock_reader, mock_writer)
47 | loop = asyncio.get_running_loop()
48 | loop.set_debug(False)
49 | msg = osc_message_builder.OscMessageBuilder("/").build()
50 | async with tcp_client.AsyncTCPClient("::1", 31337) as client:
51 | await client.send(msg)
52 |
53 | self.assertTrue(mock_writer.write.called)
54 | mock_writer.write.assert_called_once_with(slip.encode(msg.dgram))
55 |
56 | @mock.patch("asyncio.open_connection")
57 | async def test_send_message_calls_send_with_msg(self, mock_socket_ctor):
58 | mock_reader = mock.Mock()
59 | mock_writer = mock.Mock()
60 | mock_writer.drain = mock.AsyncMock()
61 | mock_writer.wait_closed = mock.AsyncMock()
62 | mock_socket_ctor.return_value = (mock_reader, mock_writer)
63 | async with tcp_client.AsyncSimpleTCPClient("::1", 31337) as client:
64 | await client.send_message("/address", 1)
65 | self.assertTrue(mock_writer.write.called)
66 |
67 |
68 | if __name__ == "__main__":
69 | unittest.main()
70 |
--------------------------------------------------------------------------------
/examples/simple_2way.py:
--------------------------------------------------------------------------------
1 | """Small example OSC server anbd client combined
2 | This program listens to serveral addresses and print if there is an input.
3 | It also transmits on a different port at the same time random values to different addresses.
4 | This can be used to demonstrate concurrent send and recieve over OSC
5 | """
6 |
7 | import argparse
8 | import random
9 | import time
10 | import threading
11 |
12 | from pythonosc import udp_client
13 | from pythonosc.dispatcher import Dispatcher
14 | from pythonosc import osc_server
15 |
16 |
17 | def print_fader_handler(unused_addr, args, value):
18 | print(f"[{args[0]}] ~ {value:0.2f}")
19 |
20 |
21 | def print_xy_fader_handler(unused_addr, args, value1, value2):
22 | print(f"[{args[0]}] ~ {value2:0.2f} ~ {value1:0.2f}")
23 |
24 |
25 | if __name__ == "__main__":
26 | parser = argparse.ArgumentParser()
27 | parser.add_argument("--serverip", default="127.0.0.1", help="The ip to listen on")
28 | parser.add_argument(
29 | "--serverport",
30 | type=int,
31 | default=5005,
32 | help="The port the OSC Server is listening on",
33 | )
34 | parser.add_argument(
35 | "--clientip", default="127.0.0.1", help="The ip of the OSC server"
36 | )
37 | parser.add_argument(
38 | "--clientport",
39 | type=int,
40 | default=5006,
41 | help="The port the OSC Client is listening on",
42 | )
43 | args = parser.parse_args()
44 |
45 | # listen to addresses and print changes in values
46 | dispatcher = Dispatcher()
47 | dispatcher.map("/1/push2", print)
48 | dispatcher.map("/1/fader1", print_fader_handler, "Focus")
49 | dispatcher.map("/1/fader2", print_fader_handler, "Zoom")
50 | dispatcher.map("/1/xy1", print_xy_fader_handler, "Pan-Tilt")
51 | dispatcher.map("/ping", print)
52 |
53 |
54 | def start_server(ip, port):
55 | print("Starting Server")
56 | server = osc_server.ThreadingOSCUDPServer((ip, port), dispatcher)
57 | print(f"Serving on {server.server_address}")
58 | thread = threading.Thread(target=server.serve_forever)
59 | thread.start()
60 |
61 |
62 | def start_client(ip, port):
63 | print("Starting Client")
64 | client = udp_client.SimpleUDPClient(ip, port)
65 | # print("Sending on {}".format(client.))
66 | thread = threading.Thread(target=random_values(client))
67 | thread.start()
68 |
69 |
70 | # send random values between 0-1 to the three addresses
71 | def random_values(client):
72 | while True:
73 | for x in range(10):
74 | client.send_message("/1/fader2", random.random())
75 | client.send_message("/1/fader1", random.random())
76 | client.send_message("/1/xy1", [random.random(), random.random()])
77 | time.sleep(0.5)
78 |
79 |
80 | start_server(args.serverip, args.serverport)
81 | start_client(args.clientip, args.clientport)
82 |
--------------------------------------------------------------------------------
/pythonosc/osc_packet.py:
--------------------------------------------------------------------------------
1 | """Use OSC packets to parse incoming UDP packets into messages or bundles.
2 |
3 | It lets you access easily to OscMessage and OscBundle instances in the packet.
4 | """
5 |
6 | import time
7 |
8 | from pythonosc.parsing import osc_types
9 | from pythonosc import osc_bundle
10 | from pythonosc import osc_message
11 |
12 | from typing import List, NamedTuple
13 |
14 | # A namedtuple as returned my the _timed_msg_of_bundle function.
15 | # 1) the system time at which the message should be executed
16 | # in seconds since the epoch.
17 | # 2) the actual message.
18 | TimedMessage = NamedTuple(
19 | "TimedMessage",
20 | [
21 | ("time", float),
22 | ("message", osc_message.OscMessage),
23 | ],
24 | )
25 |
26 |
27 | def _timed_msg_of_bundle(
28 | bundle: osc_bundle.OscBundle, now: float
29 | ) -> List[TimedMessage]:
30 | """Returns messages contained in nested bundles as a list of TimedMessage."""
31 | msgs = []
32 | for content in bundle:
33 | if type(content) is osc_message.OscMessage:
34 | if bundle.timestamp == osc_types.IMMEDIATELY or bundle.timestamp < now:
35 | msgs.append(TimedMessage(now, content))
36 | else:
37 | msgs.append(TimedMessage(bundle.timestamp, content))
38 | else:
39 | msgs.extend(_timed_msg_of_bundle(content, now))
40 | return msgs
41 |
42 |
43 | class ParseError(Exception):
44 | """Base error thrown when a packet could not be parsed."""
45 |
46 |
47 | class OscPacket(object):
48 | """Unit of transmission of the OSC protocol.
49 |
50 | Any application that sends OSC Packets is an OSC Client.
51 | Any application that receives OSC Packets is an OSC Server.
52 | """
53 |
54 | def __init__(self, dgram: bytes) -> None:
55 | """Initialize an OdpPacket with the given UDP datagram.
56 |
57 | Args:
58 | - dgram: the raw UDP datagram holding the OSC packet.
59 |
60 | Raises:
61 | - ParseError if the datagram could not be parsed.
62 | """
63 | now = time.time()
64 | try:
65 | if osc_bundle.OscBundle.dgram_is_bundle(dgram):
66 | self._messages = sorted(
67 | _timed_msg_of_bundle(osc_bundle.OscBundle(dgram), now),
68 | key=lambda x: x.time,
69 | )
70 | elif osc_message.OscMessage.dgram_is_message(dgram):
71 | self._messages = [TimedMessage(now, osc_message.OscMessage(dgram))]
72 | else:
73 | # Empty packet, should not happen as per the spec but heh, UDP...
74 | raise ParseError(
75 | "OSC Packet should at least contain an OscMessage or an "
76 | "OscBundle."
77 | )
78 | except (osc_bundle.ParseError, osc_message.ParseError) as pe:
79 | raise ParseError(f"Could not parse packet {pe}")
80 |
81 | @property
82 | def messages(self) -> List[TimedMessage]:
83 | """Returns asc-time-sorted TimedMessages of the messages in this packet."""
84 | return self._messages
85 |
--------------------------------------------------------------------------------
/pythonosc/slip.py:
--------------------------------------------------------------------------------
1 | # This file is part of the SlipLib project which is released under the MIT license.
2 | # See https://github.com/rhjdjong/SlipLib for details.
3 | #
4 | # The MIT License (MIT)
5 | #
6 | # Copyright (c) 2015 Ruud de Jong
7 | #
8 | # Permission is hereby granted, free of charge, to any person obtaining a copy
9 | # of this software and associated documentation files (the "Software"), to deal
10 | # in the Software without restriction, including without limitation the rights
11 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
12 | # copies of the Software, and to permit persons to whom the Software is
13 | # furnished to do so, subject to the following conditions:
14 | #
15 | # The above copyright notice and this permission notice shall be included in all
16 | # copies or substantial portions of the Software.
17 | #
18 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
19 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
20 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
21 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
22 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
23 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
24 | # SOFTWARE.
25 |
26 | import re
27 |
28 | END = b"\xc0"
29 | ESC = b"\xdb"
30 | ESC_END = b"\xdc"
31 | ESC_ESC = b"\xdd"
32 | END_END = b"\xc0\xc0"
33 | """These constants represent the special SLIP bytes"""
34 |
35 |
36 | class ProtocolError(ValueError):
37 | """Exception to indicate that a SLIP protocol error has occurred.
38 |
39 | This exception is raised when an attempt is made to decode
40 | a packet with an invalid byte sequence.
41 | An invalid byte sequence is either an :const:`ESC` byte followed
42 | by any byte that is not an :const:`ESC_ESC` or :const:`ESC_END` byte,
43 | or a trailing :const:`ESC` byte as last byte of the packet.
44 |
45 | The :exc:`ProtocolError` carries the invalid packet
46 | as the first (and only) element in in its :attr:`args` tuple.
47 | """
48 |
49 |
50 | def encode(msg: bytes) -> bytes:
51 | """Encodes a message (a byte sequence) into a SLIP-encoded packet.
52 |
53 | Args:
54 | msg: The message that must be encoded
55 |
56 | Returns:
57 | The SLIP-encoded message
58 | """
59 | if msg:
60 | msg = bytes(msg)
61 | else:
62 | msg = b""
63 | return END + msg.replace(ESC, ESC + ESC_ESC).replace(END, ESC + ESC_END) + END
64 |
65 |
66 | def decode(packet: bytes) -> bytes:
67 | """Retrieves the message from the SLIP-encoded packet.
68 |
69 | Args:
70 | packet: The SLIP-encoded message.
71 | Note that this must be exactly one complete packet.
72 | The :func:`decode` function does not provide any buffering
73 | for incomplete packages, nor does it provide support
74 | for decoding data with multiple packets.
75 | Returns:
76 | The decoded message
77 |
78 | Raises:
79 | ProtocolError: if the packet contains an invalid byte sequence.
80 | """
81 | if not is_valid(packet):
82 | raise ProtocolError(packet)
83 | return packet.strip(END).replace(ESC + ESC_END, END).replace(ESC + ESC_ESC, ESC)
84 |
85 |
86 | def is_valid(packet: bytes) -> bool:
87 | """Indicates if the packet's contents conform to the SLIP specification.
88 |
89 | A packet is valid if:
90 |
91 | * It contains no :const:`END` bytes other than leading and/or trailing :const:`END` bytes, and
92 | * Each :const:`ESC` byte is followed by either an :const:`ESC_END` or an :const:`ESC_ESC` byte.
93 |
94 | Args:
95 | packet: The packet to inspect.
96 |
97 | Returns:
98 | :const:`True` if the packet is valid, :const:`False` otherwise
99 | """
100 | packet = packet.strip(END)
101 | return not (
102 | END in packet
103 | or packet.endswith(ESC)
104 | or re.search(ESC + b"[^" + ESC_END + ESC_ESC + b"]", packet)
105 | )
106 |
--------------------------------------------------------------------------------
/pythonosc/osc_bundle.py:
--------------------------------------------------------------------------------
1 | import logging
2 |
3 | from pythonosc import osc_message
4 | from pythonosc.parsing import osc_types
5 |
6 | from typing import Any, Iterator, List, Union
7 |
8 | _BUNDLE_PREFIX = b"#bundle\x00"
9 |
10 |
11 | class ParseError(Exception):
12 | """Base exception raised when a datagram parsing error occurs."""
13 |
14 |
15 | class OscBundle(object):
16 | """Bundles elements that should be triggered at the same time.
17 |
18 | An element can be another OscBundle or an OscMessage.
19 | """
20 |
21 | def __init__(self, dgram: bytes) -> None:
22 | """Initializes the OscBundle with the given datagram.
23 |
24 | Args:
25 | dgram: a UDP datagram representing an OscBundle.
26 |
27 | Raises:
28 | ParseError: if the datagram could not be parsed into an OscBundle.
29 | """
30 | # Interesting stuff starts after the initial b"#bundle\x00".
31 | self._dgram = dgram
32 | index = len(_BUNDLE_PREFIX)
33 | try:
34 | self._timestamp, index = osc_types.get_date(self._dgram, index)
35 | except osc_types.ParseError as pe:
36 | raise ParseError(f"Could not get the date from the datagram: {pe}")
37 | # Get the contents as a list of OscBundle and OscMessage.
38 | self._contents = self._parse_contents(index)
39 |
40 | def _parse_contents(
41 | self, index: int
42 | ) -> List[Union["OscBundle", osc_message.OscMessage]]:
43 | contents = [] # type: List[Union[OscBundle, osc_message.OscMessage]]
44 |
45 | try:
46 | # An OSC Bundle Element consists of its size and its contents.
47 | # The size is an int32 representing the number of 8-bit bytes in the
48 | # contents, and will always be a multiple of 4. The contents are either
49 | # an OSC Message or an OSC Bundle.
50 | while self._dgram[index:]:
51 | # Get the sub content size.
52 | content_size, index = osc_types.get_int(self._dgram, index)
53 | # Get the datagram for the sub content.
54 | content_dgram = self._dgram[index : index + content_size]
55 | # Increment our position index up to the next possible content.
56 | index += content_size
57 | # Parse the content into an OSC message or bundle.
58 | if OscBundle.dgram_is_bundle(content_dgram):
59 | contents.append(OscBundle(content_dgram))
60 | elif osc_message.OscMessage.dgram_is_message(content_dgram):
61 | contents.append(osc_message.OscMessage(content_dgram))
62 | else:
63 | logging.warning(
64 | f"Could not identify content type of dgram {content_dgram!r}"
65 | )
66 | except (osc_types.ParseError, osc_message.ParseError, IndexError) as e:
67 | raise ParseError(f"Could not parse a content datagram: {e}")
68 |
69 | return contents
70 |
71 | @staticmethod
72 | def dgram_is_bundle(dgram: bytes) -> bool:
73 | """Returns whether this datagram starts like an OSC bundle."""
74 | return dgram.startswith(_BUNDLE_PREFIX)
75 |
76 | @property
77 | def timestamp(self) -> float:
78 | """Returns the timestamp associated with this bundle."""
79 | return self._timestamp
80 |
81 | @property
82 | def num_contents(self) -> int:
83 | """Shortcut for len(*bundle) returning the number of elements."""
84 | return len(self._contents)
85 |
86 | @property
87 | def size(self) -> int:
88 | """Returns the length of the datagram for this bundle."""
89 | return len(self._dgram)
90 |
91 | @property
92 | def dgram(self) -> bytes:
93 | """Returns the datagram from which this bundle was built."""
94 | return self._dgram
95 |
96 | def content(self, index: int) -> Any:
97 | """Returns the bundle's content 0-indexed."""
98 | return self._contents[index]
99 |
100 | def __iter__(self) -> Iterator[Any]:
101 | """Returns an iterator over the bundle's content."""
102 | return iter(self._contents)
103 |
--------------------------------------------------------------------------------
/pythonosc/test/test_osc_message_builder.py:
--------------------------------------------------------------------------------
1 | import unittest
2 |
3 | from pythonosc import osc_message_builder
4 |
5 |
6 | class TestOscMessageBuilder(unittest.TestCase):
7 | def test_just_address(self):
8 | msg = osc_message_builder.OscMessageBuilder("/a/b/c").build()
9 | self.assertEqual("/a/b/c", msg.address)
10 | self.assertEqual([], msg.params)
11 | # Messages with just an address should still contain the ",".
12 | self.assertEqual(b"/a/b/c\x00\x00,\x00\x00\x00", msg.dgram)
13 |
14 | def test_no_address_raises(self):
15 | builder = osc_message_builder.OscMessageBuilder("")
16 | self.assertRaises(osc_message_builder.BuildError, builder.build)
17 |
18 | def test_wrong_param_raise(self):
19 | builder = osc_message_builder.OscMessageBuilder("")
20 | self.assertRaises(ValueError, builder.add_arg, "what?", 1)
21 |
22 | def test_add_arg_invalid_inferred_type(self):
23 | builder = osc_message_builder.OscMessageBuilder("")
24 | self.assertRaises(ValueError, builder.add_arg, {"name": "John"})
25 |
26 | def test_all_param_types(self):
27 | builder = osc_message_builder.OscMessageBuilder(address="/SYNC")
28 | builder.add_arg(4.0)
29 | builder.add_arg(2)
30 | builder.add_arg(1099511627776)
31 | builder.add_arg("value")
32 | builder.add_arg(True)
33 | builder.add_arg(False)
34 | builder.add_arg(b"\x01\x02\x03")
35 | builder.add_arg([1, ["abc"]])
36 | builder.add_arg(None)
37 | # The same args but with explicit types.
38 | builder.add_arg(4.0, builder.ARG_TYPE_FLOAT)
39 | builder.add_arg(2, builder.ARG_TYPE_INT)
40 | builder.add_arg(1099511627776, builder.ARG_TYPE_INT64)
41 | builder.add_arg("value", builder.ARG_TYPE_STRING)
42 | builder.add_arg(True)
43 | builder.add_arg(False)
44 | builder.add_arg(b"\x01\x02\x03", builder.ARG_TYPE_BLOB)
45 | builder.add_arg([1, ["abc"]], [builder.ARG_TYPE_INT, [builder.ARG_TYPE_STRING]])
46 | builder.add_arg(None, builder.ARG_TYPE_NIL)
47 | builder.add_arg(4278255360, builder.ARG_TYPE_RGBA)
48 | builder.add_arg((1, 145, 36, 125), builder.ARG_TYPE_MIDI)
49 | builder.add_arg(1e-9, builder.ARG_TYPE_DOUBLE)
50 | self.assertEqual(len("fihsTFb[i[s]]N") * 2 + 3, len(builder.args))
51 | self.assertEqual("/SYNC", builder.address)
52 | builder.address = "/SEEK"
53 | msg = builder.build()
54 | self.assertEqual("/SEEK", msg.address)
55 | self.assertSequenceEqual(
56 | [
57 | 4.0,
58 | 2,
59 | 1099511627776,
60 | "value",
61 | True,
62 | False,
63 | b"\x01\x02\x03",
64 | [1, ["abc"]],
65 | None,
66 | ]
67 | * 2
68 | + [4278255360, (1, 145, 36, 125), 1e-9],
69 | msg.params,
70 | )
71 |
72 | def test_long_list(self):
73 | huge_list = list(range(512))
74 | builder = osc_message_builder.OscMessageBuilder(address="/SYNC")
75 | builder.add_arg(huge_list)
76 | msg = builder.build()
77 | print(msg._dgram)
78 | self.assertSequenceEqual([huge_list], msg.params)
79 |
80 | def test_build_wrong_type_raises(self):
81 | builder = osc_message_builder.OscMessageBuilder(address="/SYNC")
82 | builder.add_arg("this is not a float", builder.ARG_TYPE_FLOAT)
83 | self.assertRaises(osc_message_builder.BuildError, builder.build)
84 |
85 | def test_build_noarg_message(self):
86 | msg = osc_message_builder.OscMessageBuilder(address="/SYNC").build()
87 | # This reference message was generated with Cycling 74's Max software
88 | # and then was intercepted with Wireshark
89 | reference = bytearray.fromhex("2f53594e430000002c000000")
90 | self.assertSequenceEqual(msg._dgram, reference)
91 |
92 | def test_bool_encoding(self):
93 | builder = osc_message_builder.OscMessageBuilder("")
94 | builder.add_arg(0)
95 | builder.add_arg(1)
96 | builder.add_arg(False)
97 | builder.add_arg(True)
98 | self.assertEqual(builder.args, [("i", 0), ("i", 1), ("F", False), ("T", True)])
99 |
100 |
101 | if __name__ == "__main__":
102 | unittest.main()
103 |
--------------------------------------------------------------------------------
/pythonosc/test/test_osc_bundle.py:
--------------------------------------------------------------------------------
1 | import unittest
2 |
3 | from pythonosc import osc_message
4 | from pythonosc import osc_bundle
5 | from pythonosc.parsing import osc_types
6 |
7 | _DGRAM_KNOB_ROTATES_BUNDLE = (
8 | b"#bundle\x00"
9 | b"\x00\x00\x00\x00\x00\x00\x00\x01"
10 | b"\x00\x00\x00\x14"
11 | b"/LFO_Rate\x00\x00\x00"
12 | b",f\x00\x00"
13 | b">\x8c\xcc\xcd"
14 | )
15 |
16 | _DGRAM_SWITCH_GOES_OFF = (
17 | b"#bundle\x00"
18 | b"\x00\x00\x00\x00\x00\x00\x00\x01"
19 | b"\x00\x00\x00\x10"
20 | b"/SYNC\x00\x00\x00"
21 | b",f\x00\x00"
22 | b"\x00\x00\x00\x00"
23 | )
24 |
25 | _DGRAM_SWITCH_GOES_ON = (
26 | b"#bundle\x00"
27 | b"\x00\x00\x00\x00\x00\x00\x00\x01"
28 | b"\x00\x00\x00\x10"
29 | b"/SYNC\x00\x00\x00"
30 | b",f\x00\x00"
31 | b"?\x00\x00\x00"
32 | )
33 |
34 | _DGRAM_TWO_MESSAGES_IN_BUNDLE = (
35 | b"#bundle\x00"
36 | b"\x00\x00\x00\x00\x00\x00\x00\x01"
37 | # First message.
38 | b"\x00\x00\x00\x10"
39 | b"/SYNC\x00\x00\x00"
40 | b",f\x00\x00"
41 | b"?\x00\x00\x00"
42 | # Second message, same.
43 | b"\x00\x00\x00\x10"
44 | b"/SYNC\x00\x00\x00"
45 | b",f\x00\x00"
46 | b"?\x00\x00\x00"
47 | )
48 |
49 | _DGRAM_EMPTY_BUNDLE = b"#bundle\x00" b"\x00\x00\x00\x00\x00\x00\x00\x01"
50 |
51 | _DGRAM_BUNDLE_IN_BUNDLE = (
52 | b"#bundle\x00"
53 | b"\x00\x00\x00\x00\x00\x00\x00\x01"
54 | b"\x00\x00\x00(" # length of sub bundle: 40 bytes.
55 | b"#bundle\x00"
56 | b"\x00\x00\x00\x00\x00\x00\x00\x01"
57 | b"\x00\x00\x00\x10"
58 | b"/SYNC\x00\x00\x00"
59 | b",f\x00\x00"
60 | b"?\x00\x00\x00"
61 | )
62 |
63 | _DGRAM_INVALID = b"#bundle\x00" b"\x00\x00\x00"
64 |
65 | _DGRAM_INVALID_INDEX = (
66 | b"#bundle\x00"
67 | b"\x00\x00\x00\x00\x00\x00\x00\x01"
68 | b"\x00\x00\x00\x20"
69 | b"/SYNC\x00\x00\x00\x00"
70 | )
71 |
72 | _DGRAM_UNKNOWN_TYPE = (
73 | b"#bundle\x00"
74 | b"\x00\x00\x00\x00\x00\x00\x00\x01"
75 | b"\x00\x00\x00\x10"
76 | b"iamnotaslash"
77 | )
78 |
79 |
80 | class TestOscBundle(unittest.TestCase):
81 | def test_switch_goes_off(self):
82 | bundle = osc_bundle.OscBundle(_DGRAM_SWITCH_GOES_OFF)
83 | self.assertEqual(1, bundle.num_contents)
84 | self.assertEqual(len(_DGRAM_SWITCH_GOES_OFF), bundle.size)
85 | self.assertEqual(osc_types.IMMEDIATELY, bundle.timestamp)
86 |
87 | def test_switch_goes_on(self):
88 | bundle = osc_bundle.OscBundle(_DGRAM_SWITCH_GOES_ON)
89 | self.assertEqual(1, bundle.num_contents)
90 | self.assertEqual(len(_DGRAM_SWITCH_GOES_ON), bundle.size)
91 | self.assertEqual(osc_types.IMMEDIATELY, bundle.timestamp)
92 |
93 | def test_datagram_length(self):
94 | bundle = osc_bundle.OscBundle(_DGRAM_KNOB_ROTATES_BUNDLE)
95 | self.assertEqual(1, bundle.num_contents)
96 | self.assertEqual(len(_DGRAM_KNOB_ROTATES_BUNDLE), bundle.size)
97 | self.assertEqual(osc_types.IMMEDIATELY, bundle.timestamp)
98 |
99 | def test_two_messages_in_bundle(self):
100 | bundle = osc_bundle.OscBundle(_DGRAM_TWO_MESSAGES_IN_BUNDLE)
101 | self.assertEqual(2, bundle.num_contents)
102 | self.assertEqual(osc_types.IMMEDIATELY, bundle.timestamp)
103 | for content in bundle:
104 | self.assertEqual(osc_message.OscMessage, type(content))
105 |
106 | def test_empty_bundle(self):
107 | bundle = osc_bundle.OscBundle(_DGRAM_EMPTY_BUNDLE)
108 | self.assertEqual(0, bundle.num_contents)
109 | self.assertEqual(osc_types.IMMEDIATELY, bundle.timestamp)
110 |
111 | def test_bundle_in_bundle_we_must_go_deeper(self):
112 | bundle = osc_bundle.OscBundle(_DGRAM_BUNDLE_IN_BUNDLE)
113 | self.assertEqual(1, bundle.num_contents)
114 | self.assertEqual(osc_types.IMMEDIATELY, bundle.timestamp)
115 | self.assertEqual(osc_bundle.OscBundle, type(bundle.content(0)))
116 |
117 | def test_dgram_is_bundle(self):
118 | self.assertTrue(osc_bundle.OscBundle.dgram_is_bundle(_DGRAM_SWITCH_GOES_ON))
119 | self.assertFalse(osc_bundle.OscBundle.dgram_is_bundle(b"junk"))
120 |
121 | def test_raises_on_invalid_datagram(self):
122 | self.assertRaises(osc_bundle.ParseError, osc_bundle.OscBundle, _DGRAM_INVALID)
123 | self.assertRaises(
124 | osc_bundle.ParseError, osc_bundle.OscBundle, _DGRAM_INVALID_INDEX
125 | )
126 |
127 | def test_unknown_type(self):
128 | osc_bundle.OscBundle(_DGRAM_UNKNOWN_TYPE)
129 |
130 |
131 | if __name__ == "__main__":
132 | unittest.main()
133 |
--------------------------------------------------------------------------------
/pythonosc/test/test_osc_server.py:
--------------------------------------------------------------------------------
1 | import unittest
2 | import unittest.mock
3 |
4 | from pythonosc import dispatcher, osc_server
5 |
6 | _SIMPLE_PARAM_INT_MSG = b"/SYNC\x00\x00\x00" b",i\x00\x00" b"\x00\x00\x00\x04"
7 |
8 | # Regression test for a datagram that should NOT be stripped, ever...
9 | _SIMPLE_PARAM_INT_9 = b"/debug\x00\x00,i\x00\x00\x00\x00\x00\t"
10 |
11 | _SIMPLE_MSG_NO_PARAMS = b"/SYNC\x00\x00\x00"
12 |
13 |
14 | class TestOscServer(unittest.TestCase):
15 | def test_is_valid_request(self):
16 | self.assertTrue(osc_server._is_valid_request((b"#bundle\x00foobar",)))
17 | self.assertTrue(osc_server._is_valid_request((b"/address/1/2/3,foobar",)))
18 | self.assertFalse(osc_server._is_valid_request((b"",)))
19 |
20 |
21 | class TestUDPHandler(unittest.TestCase):
22 | def setUp(self):
23 | super().setUp()
24 | self.dispatcher = dispatcher.Dispatcher()
25 | # We do not want to create real UDP connections during unit tests.
26 | self.server = unittest.mock.Mock(spec=osc_server.BlockingOSCUDPServer)
27 | # Need to attach property mocks to types, not objects... weird.
28 | type(self.server).dispatcher = unittest.mock.PropertyMock(
29 | return_value=self.dispatcher
30 | )
31 | self.client_address = ("127.0.0.1", 8080)
32 |
33 | def test_no_match(self):
34 | mock_meth = unittest.mock.MagicMock()
35 | mock_meth.return_value = None
36 | self.dispatcher.map("/foobar", mock_meth)
37 | osc_server._UDPHandler(
38 | [_SIMPLE_PARAM_INT_MSG, None], self.client_address, self.server
39 | )
40 | self.assertFalse(mock_meth.called)
41 |
42 | def test_match_with_args(self):
43 | mock_meth = unittest.mock.MagicMock()
44 | mock_meth.return_value = None
45 | self.dispatcher.map("/SYNC", mock_meth, 1, 2, 3)
46 | osc_server._UDPHandler(
47 | [_SIMPLE_PARAM_INT_MSG, None], self.client_address, self.server
48 | )
49 | mock_meth.assert_called_with("/SYNC", [1, 2, 3], 4)
50 |
51 | def test_match_int9(self):
52 | mock_meth = unittest.mock.MagicMock()
53 | mock_meth.return_value = None
54 | self.dispatcher.map("/debug", mock_meth)
55 | osc_server._UDPHandler(
56 | [_SIMPLE_PARAM_INT_9, None], self.client_address, self.server
57 | )
58 | self.assertTrue(mock_meth.called)
59 | mock_meth.assert_called_with("/debug", 9)
60 |
61 | def test_match_without_args(self):
62 | mock_meth = unittest.mock.MagicMock()
63 | mock_meth.return_value = None
64 | self.dispatcher.map("/SYNC", mock_meth)
65 | osc_server._UDPHandler(
66 | [_SIMPLE_MSG_NO_PARAMS, None], self.client_address, self.server
67 | )
68 | mock_meth.assert_called_with("/SYNC")
69 |
70 | def test_match_default_handler(self):
71 | mock_meth = unittest.mock.MagicMock()
72 | mock_meth.return_value = None
73 | self.dispatcher.set_default_handler(mock_meth)
74 | osc_server._UDPHandler(
75 | [_SIMPLE_MSG_NO_PARAMS, None], self.client_address, self.server
76 | )
77 | mock_meth.assert_called_with("/SYNC")
78 |
79 | def test_response_no_args(self):
80 | def respond(*args, **kwargs):
81 | return "/SYNC"
82 |
83 | mock_sock = unittest.mock.Mock()
84 | mock_sock.sendto = unittest.mock.Mock()
85 | self.dispatcher.map("/SYNC", respond)
86 | osc_server._UDPHandler(
87 | (_SIMPLE_PARAM_INT_MSG, mock_sock), self.client_address, self.server
88 | )
89 | mock_sock.sendto.assert_called_with(
90 | b"/SYNC\00\00\00,\00\00\00", ("127.0.0.1", 8080)
91 | )
92 |
93 | def test_response_with_args(self):
94 | def respond(*args, **kwargs):
95 | return (
96 | "/SYNC",
97 | 1,
98 | "2",
99 | 3.0,
100 | )
101 |
102 | self.dispatcher.map("/SYNC", respond)
103 | mock_sock = unittest.mock.Mock()
104 | mock_sock.sendto = unittest.mock.Mock()
105 | self.dispatcher.map("/SYNC", respond)
106 | osc_server._UDPHandler(
107 | (_SIMPLE_PARAM_INT_MSG, mock_sock), self.client_address, self.server
108 | )
109 | mock_sock.sendto.assert_called_with(
110 | b"/SYNC\00\00\00,isf\x00\x00\x00\x00\x00\x00\x00\x012\x00\x00\x00@@\x00\x00",
111 | ("127.0.0.1", 8080),
112 | )
113 |
114 |
115 | if __name__ == "__main__":
116 | unittest.main()
117 |
--------------------------------------------------------------------------------
/pythonosc/udp_client.py:
--------------------------------------------------------------------------------
1 | """UDP Clients for sending OSC messages to an OSC server"""
2 |
3 | import sys
4 |
5 | if sys.version_info > (3, 5):
6 | from collections.abc import Iterable
7 | else:
8 | from collections import Iterable
9 |
10 | import socket
11 | from typing import Generator, Union
12 |
13 | from pythonosc.dispatcher import Dispatcher
14 | from pythonosc.osc_bundle import OscBundle
15 | from pythonosc.osc_message import OscMessage
16 | from pythonosc.osc_message_builder import ArgValue, OscMessageBuilder
17 |
18 |
19 | class UDPClient(object):
20 | """OSC client to send :class:`OscMessage` or :class:`OscBundle` via UDP"""
21 |
22 | def __init__(
23 | self,
24 | address: str,
25 | port: int,
26 | allow_broadcast: bool = False,
27 | family: socket.AddressFamily = socket.AF_UNSPEC,
28 | ) -> None:
29 | """Initialize client
30 |
31 | As this is UDP it will not actually make any attempt to connect to the
32 | given server at ip:port until the send() method is called.
33 |
34 | Args:
35 | address: IP address of server
36 | port: Port of server
37 | allow_broadcast: Allow for broadcast transmissions
38 | family: address family parameter (passed to socket.getaddrinfo)
39 | """
40 |
41 | for addr in socket.getaddrinfo(
42 | address, port, type=socket.SOCK_DGRAM, family=family
43 | ):
44 | af, socktype, protocol, canonname, sa = addr
45 |
46 | try:
47 | self._sock = socket.socket(af, socktype)
48 | except OSError:
49 | continue
50 | break
51 |
52 | self._sock.setblocking(False)
53 | if allow_broadcast:
54 | self._sock.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1)
55 | self._address = address
56 | self._port = port
57 |
58 | def send(self, content: Union[OscMessage, OscBundle]) -> None:
59 | """Sends an :class:`OscMessage` or :class:`OscBundle` via UDP
60 |
61 | Args:
62 | content: Message or bundle to be sent
63 | """
64 | self._sock.sendto(content.dgram, (self._address, self._port))
65 |
66 | def receive(self, timeout: int = 30) -> bytes:
67 | """Wait :int:`timeout` seconds for a message an return the raw bytes
68 |
69 | Args:
70 | timeout: Number of seconds to wait for a message
71 | """
72 | self._sock.settimeout(timeout)
73 | try:
74 | return self._sock.recv(4096)
75 | except TimeoutError:
76 | return b""
77 |
78 |
79 | class SimpleUDPClient(UDPClient):
80 | """Simple OSC client that automatically builds :class:`OscMessage` from arguments"""
81 |
82 | def send_message(self, address: str, value: ArgValue) -> None:
83 | """Build :class:`OscMessage` from arguments and send to server
84 |
85 | Args:
86 | address: OSC address the message shall go to
87 | value: One or more arguments to be added to the message
88 | """
89 | builder = OscMessageBuilder(address=address)
90 | values: ArgValue
91 | if value is None:
92 | pass
93 | elif not isinstance(value, Iterable) or isinstance(value, (str, bytes)):
94 | builder.add_arg(value)
95 | else:
96 | for val in value:
97 | builder.add_arg(val)
98 | msg = builder.build()
99 | self.send(msg)
100 |
101 | def get_messages(self, timeout: int = 30) -> Generator:
102 | """Wait :int:`timeout` seconds for a message from the server and convert it to a :class:`OscMessage`
103 |
104 | Args:
105 | timeout: Time in seconds to wait for a message
106 | """
107 | msg = self.receive(timeout)
108 | while msg:
109 | yield OscMessage(msg)
110 | msg = self.receive(timeout)
111 |
112 |
113 | class DispatchClient(SimpleUDPClient):
114 | """OSC Client that includes a :class:`Dispatcher` for handling responses and other messages from the server"""
115 |
116 | dispatcher = Dispatcher()
117 |
118 | def handle_messages(self, timeout: int = 30) -> None:
119 | """Wait :int:`timeout` seconds for a message from the server and process each message with the registered
120 | handlers. Continue until a timeout occurs.
121 |
122 | Args:
123 | timeout: Time in seconds to wait for a message
124 | """
125 | msg = self.receive(timeout)
126 | while msg:
127 | self.dispatcher.call_handlers_for_packet(msg, (self._address, self._port))
128 | msg = self.receive(timeout)
129 |
--------------------------------------------------------------------------------
/docs/dispatcher.rst:
--------------------------------------------------------------------------------
1 | Dispatcher
2 | ============
3 |
4 | The dispatcher maps OSC addresses to functions and calls the functions with the messages' arguments.
5 | Function can also be mapped to wildcard addresses.
6 |
7 |
8 | Example
9 | ---------
10 |
11 | .. code-block:: python
12 |
13 | from pythonosc.dispatcher import Dispatcher
14 | from typing import List, Any
15 |
16 | dispatcher = Dispatcher()
17 |
18 |
19 | def set_filter(address: str, *args: List[Any]) -> None:
20 | # We expect two float arguments
21 | if not len(args) == 2 or type(args[0]) is not float or type(args[1]) is not float:
22 | return
23 |
24 | # Check that address starts with filter
25 | if not address[:-1] == "/filter": # Cut off the last character
26 | return
27 |
28 | value1 = args[0]
29 | value2 = args[1]
30 | filterno = address[-1]
31 | print(f"Setting filter {filterno} values: {value1}, {value2}")
32 |
33 |
34 | dispatcher.map("/filter*", set_filter) # Map wildcard address to set_filter function
35 |
36 | # Set up server and client for testing
37 | from pythonosc.osc_server import BlockingOSCUDPServer
38 | from pythonosc.udp_client import SimpleUDPClient
39 |
40 | server = BlockingOSCUDPServer(("127.0.0.1", 1337), dispatcher)
41 | client = SimpleUDPClient("127.0.0.1", 1337)
42 |
43 | # Send message and receive exactly one message (blocking)
44 | client.send_message("/filter1", [1., 2.])
45 | server.handle_request()
46 |
47 | client.send_message("/filter8", [6., -2.])
48 | server.handle_request()
49 |
50 |
51 | Mapping
52 | ---------
53 |
54 | The dispatcher associates addresses with functions by storing them in a mapping.
55 | An address can contains wildcards as defined in the OSC specifications.
56 | Call the ``Dispatcher.map`` method with an address pattern and a handler callback function:
57 |
58 | .. code-block:: python
59 |
60 | from pythonosc.dispatcher import Dispatcher
61 | disp = Dispatcher()
62 | disp.map("/some/address*", some_printing_func)
63 |
64 | This will for example print any OSC messages starting with ``/some/address``.
65 |
66 | Additionally you can provide any amount of extra fixed argument that will always be passed before the OSC message arguments:
67 |
68 | .. code-block:: python
69 |
70 | handler = disp.map("/some/other/address", some_printing_func, "This is a fixed arg", "and this is another fixed arg")
71 |
72 | The handler callback signature must look like this:
73 |
74 | .. code-block:: python
75 |
76 | def some_callback(address: str, *osc_arguments: List[Any]) -> None:
77 | def some_callback(address: str, fixed_argument: List[Any], *osc_arguments: List[Any]) -> None:
78 |
79 | Instead of a list you can of course also use a fixed amount of arguments for ``osc_arguments``
80 |
81 | The ``Dispatcher.map`` method returns a ``Handler`` object, which can be used to remove the mapping from the dispatcher.
82 |
83 |
84 | Unmapping
85 | -----------
86 |
87 | A mapping can be undone with the ``Dispatcher.unmap`` method, which takes an address and ``Handler`` object as arguments.
88 | For example, to unmap the mapping from the `Mapping`_ section:
89 |
90 | .. code-block:: python
91 |
92 | disp.unmap("some/other/address", handler)
93 |
94 | Alternatively the handler can be reconstructed from a function and optional fixed argument:
95 |
96 | .. code-block:: python
97 |
98 | disp.unmap("some/other/address", some_printing_func, *some_fixed_args)
99 |
100 | If the provided mapping doesn't exist, a ``ValueError`` is raised.
101 |
102 |
103 | Default Handler
104 | -----------------
105 |
106 | It is possible to specify a handler callback function that is called for every unmatched address:
107 |
108 | .. code-block:: python
109 |
110 | disp.set_default_handler(some_handler_function)
111 |
112 | This is extremely useful if you quickly need to find out what addresses an undocumented device is transmitting on or for building a learning function for some controls.
113 | The handler must have the same signature as map callbacks:
114 |
115 | .. code-block:: python
116 |
117 | def some_callback(address: str, *osc_arguments: List[Any]) -> None:
118 |
119 |
120 | Handler Responses
121 | -----------------
122 |
123 | Handler functions can return responses back to the client, when running on a server, or to the
124 | server when running as a client. Handler functions should return one of:
125 |
126 | * None
127 | * An OSC address in string format
128 | * A tuple containing a string OSC address and the associated arguments
129 |
130 | If the handler function response is not None it will be encoded in an OSCMessage and sent to the
131 | remote client or server.
132 |
133 | Dispatcher Module Documentation
134 | ---------------------------------
135 |
136 | .. automodule:: pythonosc.dispatcher
137 | :special-members:
138 | :members:
139 | :exclude-members: __weakref__
140 |
--------------------------------------------------------------------------------
/README.rst:
--------------------------------------------------------------------------------
1 | ==========
2 | python-osc
3 | ==========
4 |
5 | Open Sound Control server and client implementations in **pure python**.
6 |
7 | .. image:: https://github.com/attwad/python-osc/actions/workflows/python-test.yml/badge.svg
8 | :target: https://github.com/attwad/python-osc/actions/workflows/python-test.yml
9 |
10 |
11 | Current status
12 | ==============
13 |
14 | This library was developed following the
15 | `OpenSoundControl Specification 1.0 `_
16 | and is currently in a stable state.
17 |
18 | Features
19 | ========
20 |
21 | * UDP and TCP blocking/threading/forking/asyncio server implementations
22 | * UDP and TCP clients, including asyncio support
23 | * TCP support for 1.0 and 1.1 protocol formats
24 | * int, int64, float, string, double, MIDI, timestamps, blob, nil OSC arguments
25 | * simple OSC address<->callback matching system
26 | * support for sending responses from callback handlers in client and server
27 | * extensive unit test coverage
28 | * basic client and server examples
29 |
30 | Documentation
31 | =============
32 |
33 | Available at https://python-osc.readthedocs.io/.
34 |
35 | Installation
36 | ============
37 |
38 | python-osc is a pure python library that has no external dependencies,
39 | to install it just use pip (prefered):
40 |
41 | .. image:: https://img.shields.io/pypi/v/python-osc.svg
42 | :target: https://pypi.python.org/pypi/python-osc
43 |
44 | .. code-block:: bash
45 |
46 | $ pip install python-osc
47 |
48 | Examples
49 | ========
50 |
51 | Simple client
52 | -------------
53 |
54 | .. code-block:: python
55 |
56 | """Small example OSC client
57 |
58 | This program sends 10 random values between 0.0 and 1.0 to the /filter address,
59 | waiting for 1 seconds between each value.
60 | """
61 | import argparse
62 | import random
63 | import time
64 |
65 | from pythonosc import udp_client
66 |
67 |
68 | if __name__ == "__main__":
69 | parser = argparse.ArgumentParser()
70 | parser.add_argument("--ip", default="127.0.0.1",
71 | help="The ip of the OSC server")
72 | parser.add_argument("--port", type=int, default=5005,
73 | help="The port the OSC server is listening on")
74 | args = parser.parse_args()
75 |
76 | client = udp_client.SimpleUDPClient(args.ip, args.port)
77 |
78 | for x in range(10):
79 | client.send_message("/filter", random.random())
80 | time.sleep(1)
81 |
82 | Simple server
83 | -------------
84 |
85 | .. code-block:: python
86 |
87 | """Small example OSC server
88 |
89 | This program listens to several addresses, and prints some information about
90 | received packets.
91 | """
92 | import argparse
93 | import math
94 |
95 | from pythonosc.dispatcher import Dispatcher
96 | from pythonosc import osc_server
97 |
98 | def print_volume_handler(unused_addr, args, volume):
99 | print("[{0}] ~ {1}".format(args[0], volume))
100 |
101 | def print_compute_handler(unused_addr, args, volume):
102 | try:
103 | print("[{0}] ~ {1}".format(args[0], args[1](volume)))
104 | except ValueError: pass
105 |
106 | if __name__ == "__main__":
107 | parser = argparse.ArgumentParser()
108 | parser.add_argument("--ip",
109 | default="127.0.0.1", help="The ip to listen on")
110 | parser.add_argument("--port",
111 | type=int, default=5005, help="The port to listen on")
112 | args = parser.parse_args()
113 |
114 | dispatcher = Dispatcher()
115 | dispatcher.map("/filter", print)
116 | dispatcher.map("/volume", print_volume_handler, "Volume")
117 | dispatcher.map("/logvolume", print_compute_handler, "Log volume", math.log)
118 |
119 | server = osc_server.ThreadingOSCUDPServer(
120 | (args.ip, args.port), dispatcher)
121 | print("Serving on {}".format(server.server_address))
122 | server.serve_forever()
123 |
124 | Building bundles
125 | ----------------
126 |
127 | .. code-block:: python
128 |
129 | from pythonosc import osc_bundle_builder
130 | from pythonosc import osc_message_builder
131 |
132 | bundle = osc_bundle_builder.OscBundleBuilder(
133 | osc_bundle_builder.IMMEDIATELY)
134 | msg = osc_message_builder.OscMessageBuilder(address="/SYNC")
135 | msg.add_arg(4.0)
136 | # Add 4 messages in the bundle, each with more arguments.
137 | bundle.add_content(msg.build())
138 | msg.add_arg(2)
139 | bundle.add_content(msg.build())
140 | msg.add_arg("value")
141 | bundle.add_content(msg.build())
142 | msg.add_arg(b"\x01\x02\x03")
143 | bundle.add_content(msg.build())
144 |
145 | sub_bundle = bundle.build()
146 | # Now add the same bundle inside itself.
147 | bundle.add_content(sub_bundle)
148 | # The bundle has 5 elements in total now.
149 |
150 | bundle = bundle.build()
151 | # You can now send it via a client with the `.send()` method:
152 | client.send(bundle)
153 |
154 | License?
155 | ========
156 | Unlicensed, do what you want with it. (http://unlicense.org)
157 |
--------------------------------------------------------------------------------
/pythonosc/osc_message.py:
--------------------------------------------------------------------------------
1 | """Representation of an OSC message in a pythonesque way."""
2 |
3 | import logging
4 |
5 | from pythonosc.parsing import osc_types
6 | from typing import List, Iterator, Any
7 |
8 |
9 | class ParseError(Exception):
10 | """Base exception raised when a datagram parsing error occurs."""
11 |
12 |
13 | class OscMessage(object):
14 | """Representation of a parsed datagram representing an OSC message.
15 |
16 | An OSC message consists of an OSC Address Pattern followed by an OSC
17 | Type Tag String followed by zero or more OSC Arguments.
18 | """
19 |
20 | def __init__(self, dgram: bytes) -> None:
21 | self._dgram = dgram
22 | self._parameters = [] # type: List[Any]
23 | self._parse_datagram()
24 |
25 | def __str__(self):
26 | return f"{self.address} {' '.join(str(p) for p in self.params)}"
27 |
28 | def _parse_datagram(self) -> None:
29 | try:
30 | self._address_regexp, index = osc_types.get_string(self._dgram, 0)
31 | if not self._dgram[index:]:
32 | # No params is legit, just return now.
33 | return
34 |
35 | # Get the parameters types.
36 | type_tag, index = osc_types.get_string(self._dgram, index)
37 | if type_tag.startswith(","):
38 | type_tag = type_tag[1:]
39 |
40 | params = [] # type: List[Any]
41 | param_stack = [params]
42 | # Parse each parameter given its type.
43 | for param in type_tag:
44 | val = NotImplemented # type: Any
45 | if param == "i": # Integer.
46 | val, index = osc_types.get_int(self._dgram, index)
47 | elif param == "h": # Int64.
48 | val, index = osc_types.get_int64(self._dgram, index)
49 | elif param == "f": # Float.
50 | val, index = osc_types.get_float(self._dgram, index)
51 | elif param == "d": # Double.
52 | val, index = osc_types.get_double(self._dgram, index)
53 | elif param == "s": # String.
54 | val, index = osc_types.get_string(self._dgram, index)
55 | elif param == "b": # Blob.
56 | val, index = osc_types.get_blob(self._dgram, index)
57 | elif param == "r": # RGBA.
58 | val, index = osc_types.get_rgba(self._dgram, index)
59 | elif param == "m": # MIDI.
60 | val, index = osc_types.get_midi(self._dgram, index)
61 | elif param == "t": # osc time tag:
62 | val, index = osc_types.get_timetag(self._dgram, index)
63 | elif param == "T": # True.
64 | val = True
65 | elif param == "F": # False.
66 | val = False
67 | elif param == "N": # Nil.
68 | val = None
69 | elif param == "[": # Array start.
70 | array = [] # type: List[Any]
71 | param_stack[-1].append(array)
72 | param_stack.append(array)
73 | elif param == "]": # Array stop.
74 | if len(param_stack) < 2:
75 | raise ParseError(
76 | f"Unexpected closing bracket in type tag: {type_tag}"
77 | )
78 | param_stack.pop()
79 | # TODO: Support more exotic types as described in the specification.
80 | else:
81 | logging.warning(f"Unhandled parameter type: {param}")
82 | continue
83 | if param not in "[]":
84 | param_stack[-1].append(val)
85 | if len(param_stack) != 1:
86 | raise ParseError(f"Missing closing bracket in type tag: {type_tag}")
87 | self._parameters = params
88 | except osc_types.ParseError as pe:
89 | raise ParseError("Found incorrect datagram, ignoring it", pe)
90 |
91 | @property
92 | def address(self) -> str:
93 | """Returns the OSC address regular expression."""
94 | return self._address_regexp
95 |
96 | @staticmethod
97 | def dgram_is_message(dgram: bytes) -> bool:
98 | """Returns whether this datagram starts as an OSC message."""
99 | return dgram.startswith(b"/")
100 |
101 | @property
102 | def size(self) -> int:
103 | """Returns the length of the datagram for this message."""
104 | return len(self._dgram)
105 |
106 | @property
107 | def dgram(self) -> bytes:
108 | """Returns the datagram from which this message was built."""
109 | return self._dgram
110 |
111 | @property
112 | def params(self) -> List[Any]:
113 | """Convenience method for list(self) to get the list of parameters."""
114 | return list(self)
115 |
116 | def __iter__(self) -> Iterator[Any]:
117 | """Returns an iterator over the parameters of this message."""
118 | return iter(self._parameters)
119 |
--------------------------------------------------------------------------------
/docs/server.rst:
--------------------------------------------------------------------------------
1 | Server
2 | =========
3 |
4 | The server receives OSC Messages from connected clients and invoked the appropriate callback functions with the dispatcher. There are several server types available.
5 | Server implementations are available for both UDP and TCP protocols.
6 |
7 | Blocking Server
8 | -----------------
9 |
10 | The blocking server type is the simplest of them all. Once it starts to serve, it blocks the program execution forever and remains idle inbetween handling requests. This type is good enough if your application is very simple and only has to react to OSC messages coming in and nothing else.
11 |
12 | .. code-block:: python
13 |
14 | from pythonosc.dispatcher import Dispatcher
15 | from pythonosc.osc_server import BlockingOSCUDPServer
16 |
17 |
18 | def print_handler(address, *args):
19 | print(f"{address}: {args}")
20 |
21 |
22 | def default_handler(address, *args):
23 | print(f"DEFAULT {address}: {args}")
24 |
25 |
26 | dispatcher = Dispatcher()
27 | dispatcher.map("/something/*", print_handler)
28 | dispatcher.set_default_handler(default_handler)
29 |
30 | ip = "127.0.0.1"
31 | port = 1337
32 |
33 | server = BlockingOSCUDPServer((ip, port), dispatcher)
34 | server.serve_forever() # Blocks forever
35 |
36 |
37 | Threading Server
38 | ------------------
39 |
40 | Each incoming packet will be handled in it's own thread. This also blocks further program execution, but allows concurrent handling of multiple incoming messages. Otherwise usage is identical to blocking type. Use for lightweight message handlers.
41 |
42 |
43 | Forking Server
44 | -----------------
45 |
46 | The process is forked every time a packet is coming in. Also blocks program execution forever. Use for heavyweight message handlers.
47 |
48 |
49 | Async Server
50 | -------------
51 |
52 | This server type takes advantage of the asyncio functionality of python, and allows truly non-blocking parallel execution of both your main loop and the server loop. You can use it in two ways, exclusively and concurrently. In the concurrent mode other tasks (like a main loop) can run in parallel to the server, meaning that the server doesn't block further program execution. In exclusive mode the server task is the only task that is started.
53 |
54 | Concurrent Mode
55 | ^^^^^^^^^^^^^^^^^
56 |
57 | Use this mode if you have a main program loop that needs to run without being blocked by the server. The below example runs ``init_main()`` once, which creates the serve endpoint and adds it to the asyncio event loop. The transport object is returned, which is required later to clean up the endpoint and release the socket. Afterwards we start the main loop with ``await loop()``. The example loop runs 10 times and sleeps for a second on every iteration. During the sleep the program execution is handed back to the event loop which gives the serve endpoint a chance to handle incoming OSC messages. Your loop needs to at least do an ``await asyncio.sleep(0)`` every iteration, otherwise your main loop will never release program control back to the event loop.
58 |
59 | .. code-block:: python
60 |
61 | from pythonosc.osc_server import AsyncIOOSCUDPServer
62 | from pythonosc.dispatcher import Dispatcher
63 | import asyncio
64 |
65 |
66 | def filter_handler(address, *args):
67 | print(f"{address}: {args}")
68 |
69 |
70 | dispatcher = Dispatcher()
71 | dispatcher.map("/filter", filter_handler)
72 |
73 | ip = "127.0.0.1"
74 | port = 1337
75 |
76 |
77 | async def loop():
78 | """Example main loop that only runs for 10 iterations before finishing"""
79 | for i in range(10):
80 | print(f"Loop {i}")
81 | await asyncio.sleep(1)
82 |
83 |
84 | async def init_main():
85 | server = AsyncIOOSCUDPServer((ip, port), dispatcher, asyncio.get_event_loop())
86 | transport, protocol = await server.create_serve_endpoint() # Create datagram endpoint and start serving
87 |
88 | await loop() # Enter main loop of program
89 |
90 | transport.close() # Clean up serve endpoint
91 |
92 |
93 | asyncio.run(init_main())
94 |
95 |
96 | Exclusive Mode
97 | ^^^^^^^^^^^^^^^^^
98 |
99 | This mode comes without a main loop. You only have the OSC server running in the event loop initially. You could of course use an OSC message to start a main loop from within a handler.
100 |
101 | .. code-block:: python
102 |
103 | from pythonosc.osc_server import AsyncIOOSCUDPServer
104 | from pythonosc.dispatcher import Dispatcher
105 | import asyncio
106 |
107 |
108 | def filter_handler(address, *args):
109 | print(f"{address}: {args}")
110 |
111 |
112 | dispatcher = Dispatcher()
113 | dispatcher.map("/filter", filter_handler)
114 |
115 | ip = "127.0.0.1"
116 | port = 1337
117 |
118 | server = AsyncIOOSCUDPServer((ip, port), dispatcher, asyncio.get_event_loop())
119 | server.serve()
120 |
121 |
122 | Server Module Documentation
123 | ------------------------------
124 |
125 | .. automodule:: pythonosc.osc_server
126 | :special-members:
127 | :members:
128 | :exclude-members: __weakref__
129 |
130 | .. automodule:: pythonosc.osc_tcp_server
131 | :special-members:
132 | :members:
133 | :exclude-members: __weakref__
--------------------------------------------------------------------------------
/docs/conf.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | #
3 | # Configuration file for the Sphinx documentation builder.
4 | #
5 | # This file does only contain a selection of the most common options. For a
6 | # full list see the documentation:
7 | # http://www.sphinx-doc.org/en/master/config
8 |
9 | # -- Path setup --------------------------------------------------------------
10 |
11 | # If extensions (or modules to document with autodoc) are in another directory,
12 | # add these directories to sys.path here. If the directory is relative to the
13 | # documentation root, use os.path.abspath to make it absolute, like shown here.
14 | #
15 | # import os
16 | # import sys
17 | # sys.path.insert(0, os.path.abspath('.'))
18 | import os
19 | import sys
20 |
21 | sys.path.insert(0, os.path.abspath(".."))
22 |
23 | # -- Project information -----------------------------------------------------
24 |
25 | project = "python-osc"
26 | copyright = "2019, attwad"
27 | author = "attwad"
28 |
29 | # The short X.Y version
30 | version = ""
31 | # The full version, including alpha/beta/rc tags
32 | release = "1.7.1"
33 |
34 |
35 | # -- General configuration ---------------------------------------------------
36 |
37 | # If your documentation needs a minimal Sphinx version, state it here.
38 | #
39 | # needs_sphinx = '1.0'
40 |
41 | # Add any Sphinx extension module names here, as strings. They can be
42 | # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom
43 | # ones.
44 | extensions = [
45 | "sphinx.ext.autodoc",
46 | "sphinx.ext.doctest",
47 | "sphinx.ext.coverage",
48 | "sphinx.ext.viewcode",
49 | "sphinx.ext.napoleon",
50 | ]
51 |
52 | # Add any paths that contain templates here, relative to this directory.
53 | templates_path = ["_templates"]
54 |
55 | # The suffix(es) of source filenames.
56 | # You can specify multiple suffix as a list of string:
57 | #
58 | # source_suffix = ['.rst', '.md']
59 | source_suffix = ".rst"
60 |
61 | # The master toctree document.
62 | master_doc = "index"
63 |
64 | # The language for content autogenerated by Sphinx. Refer to documentation
65 | # for a list of supported languages.
66 | #
67 | # This is also used if you do content translation via gettext catalogs.
68 | # Usually you set "language" from the command line for these cases.
69 | language = None
70 |
71 | # List of patterns, relative to source directory, that match files and
72 | # directories to ignore when looking for source files.
73 | # This pattern also affects html_static_path and html_extra_path.
74 | exclude_patterns = ["_build", "Thumbs.db", ".DS_Store"]
75 |
76 | # The name of the Pygments (syntax highlighting) style to use.
77 | pygments_style = None
78 |
79 |
80 | # -- Options for HTML output -------------------------------------------------
81 |
82 | # The theme to use for HTML and HTML Help pages. See the documentation for
83 | # a list of builtin themes.
84 | #
85 | # html_theme = "sphinx_rtd_theme"
86 |
87 | # Theme options are theme-specific and customize the look and feel of a theme
88 | # further. For a list of options available for each theme, see the
89 | # documentation.
90 | #
91 | # html_theme_options = {}
92 |
93 | # Add any paths that contain custom static files (such as style sheets) here,
94 | # relative to this directory. They are copied after the builtin static files,
95 | # so a file named "default.css" will overwrite the builtin "default.css".
96 | html_static_path = ["_static"]
97 |
98 | # Custom sidebar templates, must be a dictionary that maps document names
99 | # to template names.
100 | #
101 | # The default sidebars (for documents that don't match any pattern) are
102 | # defined by theme itself. Builtin themes are using these templates by
103 | # default: ``['localtoc.html', 'relations.html', 'sourcelink.html',
104 | # 'searchbox.html']``.
105 | #
106 | # html_sidebars = {}
107 |
108 |
109 | # -- Options for HTMLHelp output ---------------------------------------------
110 |
111 | # Output file base name for HTML help builder.
112 | htmlhelp_basename = "python-oscdoc"
113 |
114 |
115 | # -- Options for LaTeX output ------------------------------------------------
116 |
117 | latex_elements = {
118 | # The paper size ('letterpaper' or 'a4paper').
119 | #
120 | # 'papersize': 'letterpaper',
121 | # The font size ('10pt', '11pt' or '12pt').
122 | #
123 | # 'pointsize': '10pt',
124 | # Additional stuff for the LaTeX preamble.
125 | #
126 | # 'preamble': '',
127 | # Latex figure (float) alignment
128 | #
129 | # 'figure_align': 'htbp',
130 | }
131 |
132 | # Grouping the document tree into LaTeX files. List of tuples
133 | # (source start file, target name, title,
134 | # author, documentclass [howto, manual, or own class]).
135 | latex_documents = [
136 | (master_doc, "python-osc.tex", "python-osc Documentation", "attwad", "manual"),
137 | ]
138 |
139 |
140 | # -- Options for manual page output ------------------------------------------
141 |
142 | # One entry per manual page. List of tuples
143 | # (source start file, name, description, authors, manual section).
144 | man_pages = [(master_doc, "python-osc", "python-osc Documentation", [author], 1)]
145 |
146 |
147 | # -- Options for Texinfo output ----------------------------------------------
148 |
149 | # Grouping the document tree into Texinfo files. List of tuples
150 | # (source start file, target name, title, author,
151 | # dir menu entry, description, category)
152 | texinfo_documents = [
153 | (
154 | master_doc,
155 | "python-osc",
156 | "python-osc Documentation",
157 | author,
158 | "python-osc",
159 | "One line description of project.",
160 | "Miscellaneous",
161 | ),
162 | ]
163 |
164 |
165 | # -- Options for Epub output -------------------------------------------------
166 |
167 | # Bibliographic Dublin Core info.
168 | epub_title = project
169 |
170 | # The unique identifier of the text. This can be a ISBN number
171 | # or the project homepage.
172 | #
173 | # epub_identifier = ''
174 |
175 | # A unique identification for the text.
176 | #
177 | # epub_uid = ''
178 |
179 | # A list of files that should not be packed into the epub file.
180 | epub_exclude_files = ["search.html"]
181 |
182 |
183 | # -- Extension configuration -------------------------------------------------
184 |
--------------------------------------------------------------------------------
/pythonosc/test/test_dispatcher.py:
--------------------------------------------------------------------------------
1 | import unittest
2 |
3 | from pythonosc.dispatcher import Dispatcher, Handler
4 |
5 |
6 | class TestDispatcher(unittest.TestCase):
7 | def setUp(self):
8 | super().setUp()
9 | self.dispatcher = Dispatcher()
10 |
11 | def sortAndAssertSequenceEqual(self, expected, result):
12 | def sort(lst):
13 | return sorted(lst, key=lambda x: x.callback)
14 |
15 | return self.assertSequenceEqual(sort(expected), sort(result))
16 |
17 | def test_empty_by_default(self):
18 | self.sortAndAssertSequenceEqual(
19 | [], self.dispatcher.handlers_for_address("/test")
20 | )
21 |
22 | def test_use_default_handler_when_set_and_no_match(self):
23 | handler = object()
24 | self.dispatcher.set_default_handler(handler)
25 |
26 | self.sortAndAssertSequenceEqual(
27 | [Handler(handler, [])], self.dispatcher.handlers_for_address("/test")
28 | )
29 |
30 | def test_simple_map_and_match(self):
31 | handler = object()
32 | self.dispatcher.map("/test", handler, 1, 2, 3)
33 | self.dispatcher.map("/test2", handler)
34 | self.sortAndAssertSequenceEqual(
35 | [Handler(handler, [1, 2, 3])], self.dispatcher.handlers_for_address("/test")
36 | )
37 | self.sortAndAssertSequenceEqual(
38 | [Handler(handler, [])], self.dispatcher.handlers_for_address("/test2")
39 | )
40 |
41 | def test_example_from_spec(self):
42 | addresses = [
43 | "/first/this/one",
44 | "/second/1",
45 | "/second/2",
46 | "/third/a",
47 | "/third/b",
48 | "/third/c",
49 | ]
50 | for index, address in enumerate(addresses):
51 | self.dispatcher.map(address, index)
52 |
53 | for index, address in enumerate(addresses):
54 | self.sortAndAssertSequenceEqual(
55 | [Handler(index, [])], self.dispatcher.handlers_for_address(address)
56 | )
57 |
58 | self.sortAndAssertSequenceEqual(
59 | [Handler(1, []), Handler(2, [])],
60 | self.dispatcher.handlers_for_address("/second/?"),
61 | )
62 |
63 | self.sortAndAssertSequenceEqual(
64 | [Handler(3, []), Handler(4, []), Handler(5, [])],
65 | self.dispatcher.handlers_for_address("/third/*"),
66 | )
67 |
68 | def test_do_not_match_over_slash(self):
69 | self.dispatcher.map("/foo/bar/1", 1)
70 | self.dispatcher.map("/foo/bar/2", 2)
71 |
72 | self.sortAndAssertSequenceEqual([], self.dispatcher.handlers_for_address("/*"))
73 |
74 | def test_match_middle_star(self):
75 | self.dispatcher.map("/foo/bar/1", 1)
76 | self.dispatcher.map("/foo/bar/2", 2)
77 |
78 | self.sortAndAssertSequenceEqual(
79 | [Handler(2, [])], self.dispatcher.handlers_for_address("/foo/*/2")
80 | )
81 |
82 | def test_match_multiple_stars(self):
83 | self.dispatcher.map("/foo/bar/1", 1)
84 | self.dispatcher.map("/foo/bar/2", 2)
85 |
86 | self.sortAndAssertSequenceEqual(
87 | [Handler(1, []), Handler(2, [])],
88 | self.dispatcher.handlers_for_address("/*/*/*"),
89 | )
90 |
91 | def test_match_address_contains_plus_as_character(self):
92 | self.dispatcher.map("/footest/bar+tender/1", 1)
93 |
94 | self.sortAndAssertSequenceEqual(
95 | [Handler(1, [])], self.dispatcher.handlers_for_address("/foo*/bar+*/*")
96 | )
97 | self.sortAndAssertSequenceEqual(
98 | [Handler(1, [])], self.dispatcher.handlers_for_address("/foo*/bar*/*")
99 | )
100 |
101 | def test_call_correct_dispatcher_on_star(self):
102 | self.dispatcher.map("/a+b", 1)
103 | self.dispatcher.map("/aaab", 2)
104 | self.sortAndAssertSequenceEqual(
105 | [Handler(2, [])], self.dispatcher.handlers_for_address("/aaab")
106 | )
107 | self.sortAndAssertSequenceEqual(
108 | [Handler(1, [])], self.dispatcher.handlers_for_address("/a+b")
109 | )
110 |
111 | def test_map_star(self):
112 | self.dispatcher.map("/starbase/*", 1)
113 | self.sortAndAssertSequenceEqual(
114 | [Handler(1, [])], self.dispatcher.handlers_for_address("/starbase/bar")
115 | )
116 |
117 | def test_map_root_star(self):
118 | self.dispatcher.map("/*", 1)
119 | self.sortAndAssertSequenceEqual(
120 | [Handler(1, [])], self.dispatcher.handlers_for_address("/anything/matches")
121 | )
122 |
123 | def test_map_double_stars(self):
124 | self.dispatcher.map("/foo/*/bar/*", 1)
125 | self.sortAndAssertSequenceEqual(
126 | [Handler(1, [])], self.dispatcher.handlers_for_address("/foo/wild/bar/wild")
127 | )
128 | self.sortAndAssertSequenceEqual(
129 | [], self.dispatcher.handlers_for_address("/foo/wild/nomatch/wild")
130 | )
131 |
132 | def test_multiple_handlers(self):
133 | self.dispatcher.map("/foo/bar", 1)
134 | self.dispatcher.map("/foo/bar", 2)
135 | self.sortAndAssertSequenceEqual(
136 | [Handler(1, []), Handler(2, [])],
137 | self.dispatcher.handlers_for_address("/foo/bar"),
138 | )
139 |
140 | def test_multiple_handlers_with_wildcard_map(self):
141 | self.dispatcher.map("/foo/bar", 1)
142 | self.dispatcher.map("/*", 2)
143 | self.sortAndAssertSequenceEqual(
144 | [Handler(1, []), Handler(2, [])],
145 | self.dispatcher.handlers_for_address("/foo/bar"),
146 | )
147 |
148 | def test_unmap(self):
149 | def dummyhandler():
150 | pass
151 |
152 | # Test with handler returned by map
153 | returnedhandler = self.dispatcher.map("/map/me", dummyhandler)
154 | self.sortAndAssertSequenceEqual(
155 | [Handler(dummyhandler, [])], self.dispatcher.handlers_for_address("/map/me")
156 | )
157 | self.dispatcher.unmap("/map/me", returnedhandler)
158 | self.sortAndAssertSequenceEqual(
159 | [], self.dispatcher.handlers_for_address("/map/me")
160 | )
161 |
162 | # Test with reconstructing handler
163 | self.dispatcher.map("/map/me/too", dummyhandler)
164 | self.sortAndAssertSequenceEqual(
165 | [Handler(dummyhandler, [])],
166 | self.dispatcher.handlers_for_address("/map/me/too"),
167 | )
168 | self.dispatcher.unmap("/map/me/too", dummyhandler)
169 | self.sortAndAssertSequenceEqual(
170 | [], self.dispatcher.handlers_for_address("/map/me/too")
171 | )
172 |
173 | def test_unmap_exception(self):
174 | def dummyhandler():
175 | pass
176 |
177 | with self.assertRaises(ValueError):
178 | self.dispatcher.unmap("/unmap/exception", dummyhandler)
179 |
180 | handlerobj = self.dispatcher.map("/unmap/somethingelse", dummyhandler())
181 | with self.assertRaises(ValueError):
182 | self.dispatcher.unmap("/unmap/exception", handlerobj)
183 |
184 |
185 | if __name__ == "__main__":
186 | unittest.main()
187 |
--------------------------------------------------------------------------------
/pythonosc/osc_server.py:
--------------------------------------------------------------------------------
1 | """OSC Servers that receive UDP packets and invoke handlers accordingly."""
2 |
3 | import asyncio
4 | import os
5 | import socketserver
6 | from socket import socket as _socket
7 | from typing import Any, Coroutine, Tuple, Union, cast
8 |
9 | from pythonosc import osc_bundle, osc_message
10 | from pythonosc.dispatcher import Dispatcher
11 | from pythonosc.osc_message_builder import build_msg
12 |
13 | _RequestType = Union[_socket, Tuple[bytes, _socket]]
14 | _AddressType = Union[Tuple[str, int], str]
15 |
16 |
17 | class _UDPHandler(socketserver.BaseRequestHandler):
18 | """Handles correct UDP messages for all types of server."""
19 |
20 | def __init__(self, request, client_address, server):
21 | self.socket = request[1]
22 | super().__init__(request, client_address, server)
23 |
24 | def handle(self) -> None:
25 | """Calls the handlers via dispatcher
26 |
27 | This method is called after a basic sanity check was done on the datagram,
28 | whether this datagram looks like an osc message or bundle.
29 | If not the server won't call it and so no new
30 | threads/processes will be spawned.
31 | """
32 | server = cast(OSCUDPServer, self.server)
33 | resp = server.dispatcher.call_handlers_for_packet(
34 | self.request[0], self.client_address
35 | )
36 | for r in resp:
37 | if not isinstance(r, tuple):
38 | r = [r]
39 | msg = build_msg(r[0], r[1:])
40 | self.socket.sendto(msg.dgram, self.client_address)
41 |
42 |
43 | def _is_valid_request(request: _RequestType) -> bool:
44 | """Returns true if the request's data looks like an osc bundle or message.
45 |
46 | Returns:
47 | True if request is OSC bundle or OSC message
48 | """
49 | assert isinstance(
50 | request, tuple
51 | ) # TODO: handle requests which are passed just as a socket?
52 | data = request[0]
53 | return osc_bundle.OscBundle.dgram_is_bundle(
54 | data
55 | ) or osc_message.OscMessage.dgram_is_message(data)
56 |
57 |
58 | class OSCUDPServer(socketserver.UDPServer):
59 | """Superclass for different flavors of OSC UDP servers"""
60 |
61 | def __init__(
62 | self,
63 | server_address: Tuple[str, int],
64 | dispatcher: Dispatcher,
65 | bind_and_activate: bool = True,
66 | ) -> None:
67 | """Initialize
68 |
69 | Args:
70 | server_address: IP and port of server
71 | dispatcher: Dispatcher this server will use
72 | (optional) bind_and_activate: default=True defines if the server has to start on call of constructor
73 | """
74 | super().__init__(server_address, _UDPHandler, bind_and_activate)
75 | self._dispatcher = dispatcher
76 |
77 | def verify_request(
78 | self, request: _RequestType, client_address: _AddressType
79 | ) -> bool:
80 | """Returns true if the data looks like a valid OSC UDP datagram
81 |
82 | Args:
83 | request: Incoming data
84 | client_address: IP and port of client this message came from
85 |
86 | Returns:
87 | True if request is OSC bundle or OSC message
88 | """
89 | return _is_valid_request(request)
90 |
91 | @property
92 | def dispatcher(self) -> Dispatcher:
93 | return self._dispatcher
94 |
95 |
96 | class BlockingOSCUDPServer(OSCUDPServer):
97 | """Blocking version of the UDP server.
98 |
99 | Each message will be handled sequentially on the same thread.
100 | Use this is you don't care about latency in your message handling or don't
101 | have a multiprocess/multithread environment.
102 | """
103 |
104 |
105 | class ThreadingOSCUDPServer(socketserver.ThreadingMixIn, OSCUDPServer):
106 | """Threading version of the OSC UDP server.
107 |
108 | Each message will be handled in its own new thread.
109 | Use this when lightweight operations are done by each message handlers.
110 | """
111 |
112 |
113 | if hasattr(os, "fork"):
114 |
115 | class ForkingOSCUDPServer(socketserver.ForkingMixIn, OSCUDPServer):
116 | """Forking version of the OSC UDP server.
117 |
118 | Each message will be handled in its own new process.
119 | Use this when heavyweight operations are done by each message handlers
120 | and forking a whole new process for each of them is worth it.
121 | """
122 |
123 |
124 | class AsyncIOOSCUDPServer:
125 | """Asynchronous OSC Server
126 |
127 | An asynchronous OSC Server using UDP. It creates a datagram endpoint that runs in an event loop.
128 | """
129 |
130 | def __init__(
131 | self,
132 | server_address: Tuple[str, int],
133 | dispatcher: Dispatcher,
134 | loop: asyncio.BaseEventLoop,
135 | ) -> None:
136 | """Initialize
137 |
138 | Args:
139 | server_address: IP and port of server
140 | dispatcher: Dispatcher this server shall use
141 | loop: Event loop to add the server task to. Use ``asyncio.get_event_loop()`` unless you know what you're
142 | doing.
143 | """
144 |
145 | self._server_address = server_address
146 | self._dispatcher = dispatcher
147 | self._loop = loop
148 |
149 | class _OSCProtocolFactory(asyncio.DatagramProtocol):
150 | """OSC protocol factory which passes datagrams to dispatcher"""
151 |
152 | def __init__(self, dispatcher: Dispatcher) -> None:
153 | self.dispatcher = dispatcher
154 |
155 | def connection_made(self, transport):
156 | self.transport = transport
157 |
158 | def datagram_received(
159 | self, data: bytes, client_address: Tuple[str, int]
160 | ) -> None:
161 | resp = self.dispatcher.call_handlers_for_packet(data, client_address)
162 | for r in resp:
163 | if not isinstance(r, tuple):
164 | r = [r]
165 | msg = build_msg(r[0], r[1:])
166 | self.transport.sendto(msg.dgram, client_address)
167 |
168 | def serve(self) -> None:
169 | """Creates a datagram endpoint and registers it with event loop.
170 |
171 | Use this only in synchronous code (i.e. not from within a coroutine). This will start the server and run it
172 | forever or until a ``stop()`` is called on the event loop.
173 | """
174 | self._loop.run_until_complete(self.create_serve_endpoint())
175 |
176 | def create_serve_endpoint(
177 | self,
178 | ) -> Coroutine[
179 | Any, Any, Tuple[asyncio.transports.BaseTransport, asyncio.DatagramProtocol]
180 | ]:
181 | """Creates a datagram endpoint and registers it with event loop as coroutine.
182 |
183 | Returns:
184 | Awaitable coroutine that returns transport and protocol objects
185 | """
186 | return self._loop.create_datagram_endpoint(
187 | lambda: self._OSCProtocolFactory(self.dispatcher),
188 | local_addr=self._server_address,
189 | )
190 |
191 | @property
192 | def dispatcher(self) -> Dispatcher:
193 | return self._dispatcher
194 |
--------------------------------------------------------------------------------
/pythonosc/osc_message_builder.py:
--------------------------------------------------------------------------------
1 | """Build OSC messages for client applications."""
2 |
3 | from typing import Any, Iterable, List, Optional, Tuple, Union
4 |
5 | from pythonosc import osc_message
6 | from pythonosc.parsing import osc_types
7 |
8 | ArgValue = Union[str, bytes, bool, int, float, osc_types.MidiPacket, list, None]
9 |
10 |
11 | class BuildError(Exception):
12 | """Error raised when an incomplete message is trying to be built."""
13 |
14 |
15 | class OscMessageBuilder(object):
16 | """Builds arbitrary OscMessage instances."""
17 |
18 | ARG_TYPE_FLOAT = "f"
19 | ARG_TYPE_DOUBLE = "d"
20 | ARG_TYPE_INT = "i"
21 | ARG_TYPE_INT64 = "h"
22 | ARG_TYPE_STRING = "s"
23 | ARG_TYPE_BLOB = "b"
24 | ARG_TYPE_RGBA = "r"
25 | ARG_TYPE_MIDI = "m"
26 | ARG_TYPE_TRUE = "T"
27 | ARG_TYPE_FALSE = "F"
28 | ARG_TYPE_NIL = "N"
29 |
30 | ARG_TYPE_ARRAY_START = "["
31 | ARG_TYPE_ARRAY_STOP = "]"
32 |
33 | _SUPPORTED_ARG_TYPES = (
34 | ARG_TYPE_FLOAT,
35 | ARG_TYPE_DOUBLE,
36 | ARG_TYPE_INT,
37 | ARG_TYPE_INT64,
38 | ARG_TYPE_BLOB,
39 | ARG_TYPE_STRING,
40 | ARG_TYPE_RGBA,
41 | ARG_TYPE_MIDI,
42 | ARG_TYPE_TRUE,
43 | ARG_TYPE_FALSE,
44 | ARG_TYPE_NIL,
45 | )
46 |
47 | def __init__(self, address: Optional[str] = None) -> None:
48 | """Initialize a new builder for a message.
49 |
50 | Args:
51 | - address: The osc address to send this message to.
52 | """
53 | self._address = address
54 | self._args = [] # type: List[Tuple[str, Union[ArgValue, None]]]
55 |
56 | @property
57 | def address(self) -> Optional[str]:
58 | """Returns the OSC address this message will be sent to."""
59 | return self._address
60 |
61 | @address.setter
62 | def address(self, value: str) -> None:
63 | """Sets the OSC address this message will be sent to."""
64 | self._address = value
65 |
66 | @property
67 | def args(self) -> List[Tuple[str, Union[ArgValue, None]]]:
68 | """Returns the (type, value) arguments list of this message."""
69 | return self._args
70 |
71 | def _valid_type(self, arg_type: str) -> bool:
72 | if arg_type in self._SUPPORTED_ARG_TYPES:
73 | return True
74 | elif isinstance(arg_type, list):
75 | for sub_type in arg_type:
76 | if not self._valid_type(sub_type):
77 | return False
78 | return True
79 | return False
80 |
81 | def add_arg(self, arg_value: ArgValue, arg_type: Optional[str] = None) -> None:
82 | """Add a typed argument to this message.
83 |
84 | Args:
85 | - arg_value: The corresponding value for the argument.
86 | - arg_type: A value in ARG_TYPE_* defined in this class,
87 | if none then the type will be guessed.
88 | Raises:
89 | - ValueError: if the type is not supported.
90 | """
91 | if arg_type and not self._valid_type(arg_type):
92 | raise ValueError(
93 | f"arg_type must be one of {self._SUPPORTED_ARG_TYPES}, or an array of valid types"
94 | )
95 | if not arg_type:
96 | arg_type = self._get_arg_type(arg_value)
97 | if isinstance(arg_type, list):
98 | self._args.append((self.ARG_TYPE_ARRAY_START, None))
99 | for v, t in zip(arg_value, arg_type): # type: ignore[arg-type]
100 | self.add_arg(v, t)
101 | self._args.append((self.ARG_TYPE_ARRAY_STOP, None))
102 | else:
103 | self._args.append((arg_type, arg_value))
104 |
105 | # The return type here is actually Union[str, List[]], however there
106 | # is no annotation for a recursive type like this.
107 | def _get_arg_type(self, arg_value: ArgValue) -> Union[str, Any]:
108 | """Guess the type of a value.
109 |
110 | Args:
111 | - arg_value: The value to guess the type of.
112 | Raises:
113 | - ValueError: if the type is not supported.
114 | """
115 | if isinstance(arg_value, str):
116 | arg_type = self.ARG_TYPE_STRING # type: Union[str, Any]
117 | elif isinstance(arg_value, bytes):
118 | arg_type = self.ARG_TYPE_BLOB
119 | elif arg_value is True:
120 | arg_type = self.ARG_TYPE_TRUE
121 | elif arg_value is False:
122 | arg_type = self.ARG_TYPE_FALSE
123 | elif isinstance(arg_value, int):
124 | if arg_value.bit_length() > 32:
125 | arg_type = self.ARG_TYPE_INT64
126 | else:
127 | arg_type = self.ARG_TYPE_INT
128 | elif isinstance(arg_value, float):
129 | arg_type = self.ARG_TYPE_FLOAT
130 | elif isinstance(arg_value, tuple) and len(arg_value) == 4:
131 | arg_type = self.ARG_TYPE_MIDI
132 | elif isinstance(arg_value, list):
133 | arg_type = [self._get_arg_type(v) for v in arg_value]
134 | elif arg_value is None:
135 | arg_type = self.ARG_TYPE_NIL
136 | else:
137 | raise ValueError("Inferred arg_value type is not supported")
138 | return arg_type
139 |
140 | def build(self) -> osc_message.OscMessage:
141 | """Builds an OscMessage from the current state of this builder.
142 |
143 | Raises:
144 | - BuildError: if the message could not be build or if the address
145 | was empty.
146 |
147 | Returns:
148 | - an osc_message.OscMessage instance.
149 | """
150 | if not self._address:
151 | raise BuildError("OSC addresses cannot be empty")
152 | dgram = b""
153 | try:
154 | # Write the address.
155 | dgram += osc_types.write_string(self._address)
156 | if not self._args:
157 | dgram += osc_types.write_string(",")
158 | return osc_message.OscMessage(dgram)
159 |
160 | # Write the parameters.
161 | arg_types = "".join([arg[0] for arg in self._args])
162 | dgram += osc_types.write_string(f",{arg_types}")
163 | for arg_type, value in self._args:
164 | if arg_type == self.ARG_TYPE_STRING:
165 | dgram += osc_types.write_string(value) # type: ignore[arg-type]
166 | elif arg_type == self.ARG_TYPE_INT:
167 | dgram += osc_types.write_int(value) # type: ignore[arg-type]
168 | elif arg_type == self.ARG_TYPE_INT64:
169 | dgram += osc_types.write_int64(value) # type: ignore[arg-type]
170 | elif arg_type == self.ARG_TYPE_FLOAT:
171 | dgram += osc_types.write_float(value) # type: ignore[arg-type]
172 | elif arg_type == self.ARG_TYPE_DOUBLE:
173 | dgram += osc_types.write_double(value) # type: ignore[arg-type]
174 | elif arg_type == self.ARG_TYPE_BLOB:
175 | dgram += osc_types.write_blob(value) # type: ignore[arg-type]
176 | elif arg_type == self.ARG_TYPE_RGBA:
177 | dgram += osc_types.write_rgba(value) # type: ignore[arg-type]
178 | elif arg_type == self.ARG_TYPE_MIDI:
179 | dgram += osc_types.write_midi(value) # type: ignore[arg-type]
180 | elif arg_type in (
181 | self.ARG_TYPE_TRUE,
182 | self.ARG_TYPE_FALSE,
183 | self.ARG_TYPE_ARRAY_START,
184 | self.ARG_TYPE_ARRAY_STOP,
185 | self.ARG_TYPE_NIL,
186 | ):
187 | continue
188 | else:
189 | raise BuildError(f"Incorrect parameter type found {arg_type}")
190 |
191 | return osc_message.OscMessage(dgram)
192 | except osc_types.BuildError as be:
193 | raise BuildError(f"Could not build the message: {be}")
194 |
195 |
196 | def build_msg(address: str, value: ArgValue = "") -> osc_message.OscMessage:
197 | builder = OscMessageBuilder(address=address)
198 | values: ArgValue
199 | if value == "":
200 | values = []
201 | elif not isinstance(value, Iterable) or isinstance(value, (str, bytes)):
202 | values = [value]
203 | else:
204 | values = value
205 | for val in values:
206 | builder.add_arg(val)
207 | return builder.build()
208 |
--------------------------------------------------------------------------------
/pythonosc/tcp_client.py:
--------------------------------------------------------------------------------
1 | """TCP Clients for sending OSC messages to an OSC server"""
2 |
3 | import asyncio
4 | import socket
5 | import struct
6 | from typing import AsyncGenerator, Generator, List, Union
7 |
8 | from pythonosc import slip
9 | from pythonosc.dispatcher import Dispatcher
10 | from pythonosc.osc_bundle import OscBundle
11 | from pythonosc.osc_message import OscMessage
12 | from pythonosc.osc_message_builder import ArgValue, build_msg
13 | from pythonosc.osc_tcp_server import MODE_1_1
14 |
15 |
16 | class TCPClient(object):
17 | """Async OSC client to send :class:`OscMessage` or :class:`OscBundle` via TCP"""
18 |
19 | def __init__(
20 | self,
21 | address: str,
22 | port: int,
23 | family: socket.AddressFamily = socket.AF_INET,
24 | mode: str = MODE_1_1,
25 | ) -> None:
26 | """Initialize client
27 |
28 | Args:
29 | address: IP address of server
30 | port: Port of server
31 | family: address family parameter (passed to socket.getaddrinfo)
32 | """
33 | self.address = address
34 | self.port = port
35 | self.family = family
36 | self.mode = mode
37 | self.socket = socket.socket(self.family, socket.SOCK_STREAM)
38 | self.socket.settimeout(30)
39 | self.socket.connect((address, port))
40 |
41 | def __enter__(self):
42 | return self
43 |
44 | def __exit__(self, exc_type, exc_val, exc_tb):
45 | self.close()
46 |
47 | def send(self, content: Union[OscMessage, OscBundle]) -> None:
48 | """Sends an :class:`OscMessage` or :class:`OscBundle` via TCP
49 |
50 | Args:
51 | content: Message or bundle to be sent
52 | """
53 | if self.mode == MODE_1_1:
54 | self.socket.sendall(slip.encode(content.dgram))
55 | else:
56 | b = struct.pack("!I", len(content.dgram))
57 | self.socket.sendall(b + content.dgram)
58 |
59 | def receive(self, timeout: int = 30) -> List[bytes]:
60 | self.socket.settimeout(timeout)
61 | if self.mode == MODE_1_1:
62 | try:
63 | buf = self.socket.recv(4096)
64 | except TimeoutError:
65 | return []
66 | if not buf:
67 | return []
68 | # If the last byte is not an END marker there could be more data coming
69 | while buf[-1] != 192:
70 | try:
71 | newbuf = self.socket.recv(4096)
72 | except TimeoutError:
73 | break
74 | if not newbuf:
75 | # Maybe should raise an exception here?
76 | break
77 | buf += newbuf
78 | return [slip.decode(p) for p in buf.split(slip.END_END)]
79 | else:
80 | buf = b""
81 | try:
82 | lengthbuf = self.socket.recv(4)
83 | except TimeoutError:
84 | return []
85 | (length,) = struct.unpack("!I", lengthbuf)
86 | while length > 0:
87 | try:
88 | newbuf = self.socket.recv(length)
89 | except TimeoutError:
90 | return []
91 | if not newbuf:
92 | return []
93 | buf += newbuf
94 | length -= len(newbuf)
95 | return [buf]
96 |
97 | def close(self):
98 | self.socket.close()
99 |
100 |
101 | class SimpleTCPClient(TCPClient):
102 | """Simple OSC client that automatically builds :class:`OscMessage` from arguments"""
103 |
104 | def __init__(self, *args, **kwargs):
105 | super().__init__(*args, **kwargs)
106 |
107 | def send_message(self, address: str, value: ArgValue = "") -> None:
108 | """Build :class:`OscMessage` from arguments and send to server
109 |
110 | Args:
111 | address: OSC address the message shall go to
112 | value: One or more arguments to be added to the message
113 | """
114 | msg = build_msg(address, value)
115 | return self.send(msg)
116 |
117 | def get_messages(self, timeout: int = 30) -> Generator:
118 | r = self.receive(timeout)
119 | while r:
120 | for m in r:
121 | yield OscMessage(m)
122 | r = self.receive(timeout)
123 |
124 |
125 | class TCPDispatchClient(SimpleTCPClient):
126 | """OSC TCP Client that includes a :class:`Dispatcher` for handling responses and other messages from the server"""
127 |
128 | dispatcher = Dispatcher()
129 |
130 | def handle_messages(self, timeout_sec: int = 30) -> None:
131 | """Wait :int:`timeout` seconds for a message from the server and process each message with the registered
132 | handlers. Continue until a timeout occurs.
133 |
134 | Args:
135 | timeout: Time in seconds to wait for a message
136 | """
137 | r = self.receive(timeout_sec)
138 | while r:
139 | for m in r:
140 | self.dispatcher.call_handlers_for_packet(m, (self.address, self.port))
141 | r = self.receive(timeout_sec)
142 |
143 |
144 | class AsyncTCPClient:
145 | """Async OSC client to send :class:`OscMessage` or :class:`OscBundle` via TCP"""
146 |
147 | def __init__(
148 | self,
149 | address: str,
150 | port: int,
151 | family: socket.AddressFamily = socket.AF_INET,
152 | mode: str = MODE_1_1,
153 | ) -> None:
154 | """Initialize client
155 |
156 | Args:
157 | address: IP address of server
158 | port: Port of server
159 | family: address family parameter (passed to socket.getaddrinfo)
160 | """
161 | self.address: str = address
162 | self.port: int = port
163 | self.mode: str = mode
164 | self.family: socket.AddressFamily = family
165 |
166 | def __await__(self):
167 | async def closure():
168 | await self.__open__()
169 | return self
170 |
171 | return closure().__await__()
172 |
173 | async def __aenter__(self):
174 | await self.__open__()
175 | return self
176 |
177 | async def __open__(self):
178 | self.reader, self.writer = await asyncio.open_connection(
179 | self.address, self.port
180 | )
181 |
182 | async def __aexit__(self, exc_type, exc_val, exc_tb):
183 | await self.close()
184 |
185 | async def send(self, content: Union[OscMessage, OscBundle]) -> None:
186 | """Sends an :class:`OscMessage` or :class:`OscBundle` via TCP
187 |
188 | Args:
189 | content: Message or bundle to be sent
190 | """
191 | if self.mode == MODE_1_1:
192 | self.writer.write(slip.encode(content.dgram))
193 | else:
194 | b = struct.pack("!I", len(content.dgram))
195 | self.writer.write(b + content.dgram)
196 | await self.writer.drain()
197 |
198 | async def receive(self, timeout: int = 30) -> List[bytes]:
199 | if self.mode == MODE_1_1:
200 | try:
201 | buf = await asyncio.wait_for(self.reader.read(4096), timeout)
202 | except TimeoutError:
203 | return []
204 | if not buf:
205 | return []
206 | # If the last byte is not an END marker there could be more data coming
207 | while buf[-1] != 192:
208 | try:
209 | newbuf = await asyncio.wait_for(self.reader.read(4096), timeout)
210 | except TimeoutError:
211 | break
212 | if not newbuf:
213 | # Maybe should raise an exception here?
214 | break
215 | buf += newbuf
216 | return [slip.decode(p) for p in buf.split(slip.END_END)]
217 | else:
218 | buf = b""
219 | try:
220 | lengthbuf = await asyncio.wait_for(self.reader.read(4), timeout)
221 | except TimeoutError:
222 | return []
223 |
224 | (length,) = struct.unpack("!I", lengthbuf)
225 | while length > 0:
226 | try:
227 | newbuf = await asyncio.wait_for(self.reader.read(length), timeout)
228 | except TimeoutError:
229 | return []
230 | if not newbuf:
231 | return []
232 | buf += newbuf
233 | length -= len(newbuf)
234 | return [buf]
235 |
236 | async def close(self):
237 | self.writer.write_eof()
238 | self.writer.close()
239 | await self.writer.wait_closed()
240 |
241 |
242 | class AsyncSimpleTCPClient(AsyncTCPClient):
243 | """Simple OSC client that automatically builds :class:`OscMessage` from arguments"""
244 |
245 | def __init__(
246 | self,
247 | address: str,
248 | port: int,
249 | family: socket.AddressFamily = socket.AF_INET,
250 | mode: str = MODE_1_1,
251 | ):
252 | super().__init__(address, port, family, mode)
253 |
254 | async def send_message(self, address: str, value: ArgValue = "") -> None:
255 | """Build :class:`OscMessage` from arguments and send to server
256 |
257 | Args:
258 | address: OSC address the message shall go to
259 | value: One or more arguments to be added to the message
260 | """
261 | msg = build_msg(address, value)
262 | return await self.send(msg)
263 |
264 | async def get_messages(self, timeout: int = 30) -> AsyncGenerator:
265 | r = await self.receive(timeout)
266 | while r:
267 | for m in r:
268 | yield OscMessage(m)
269 | r = await self.receive(timeout)
270 |
271 |
272 | class AsyncDispatchTCPClient(AsyncTCPClient):
273 | """OSC Client that includes a :class:`Dispatcher` for handling responses and other messages from the server"""
274 |
275 | dispatcher = Dispatcher()
276 |
277 | async def handle_messages(self, timeout: int = 30) -> None:
278 | """Wait :int:`timeout` seconds for a message from the server and process each message with the registered
279 | handlers. Continue until a timeout occurs.
280 |
281 | Args:
282 | timeout: Time in seconds to wait for a message
283 | """
284 | msgs = await self.receive(timeout)
285 | while msgs:
286 | for m in msgs:
287 | await self.dispatcher.async_call_handlers_for_packet(
288 | m, (self.address, self.port)
289 | )
290 | msgs = await self.receive(timeout)
291 |
--------------------------------------------------------------------------------
/pythonosc/osc_tcp_server.py:
--------------------------------------------------------------------------------
1 | """OSC Servers that receive TCP packets and invoke handlers accordingly.
2 |
3 | Use like this:
4 |
5 | dispatcher = dispatcher.Dispatcher()
6 | # This will print all parameters to stdout.
7 | dispatcher.map("/bpm", print)
8 | server = ForkingOSCTCPServer((ip, port), dispatcher)
9 | server.serve_forever()
10 |
11 | or run the server on its own thread:
12 | server = ForkingOSCTCPServer((ip, port), dispatcher)
13 | server_thread = threading.Thread(target=server.serve_forever)
14 | server_thread.start()
15 | ...
16 | server.shutdown()
17 |
18 |
19 | Those servers are using the standard socketserver from the standard library:
20 | http://docs.python.org/library/socketserver.html
21 |
22 |
23 | Alternatively, the AsyncIOOSCTCPServer server can be integrated with an
24 | asyncio event loop:
25 |
26 | loop = asyncio.get_event_loop()
27 | server = AsyncIOOSCTCPServer(server_address, dispatcher)
28 | server.serve()
29 | loop.run_forever()
30 |
31 | """
32 |
33 | # mypy: disable-error-code="attr-defined"
34 |
35 | import asyncio
36 | import logging
37 | import os
38 | import socketserver
39 | import struct
40 | from typing import List, Tuple
41 |
42 | from pythonosc import osc_message_builder, slip
43 | from pythonosc.dispatcher import Dispatcher
44 |
45 | LOG = logging.getLogger()
46 | MODE_1_0 = "1.0"
47 | MODE_1_1 = "1.1"
48 |
49 |
50 | class _TCPHandler1_0(socketserver.BaseRequestHandler):
51 | """Handles correct OSC1.0 messages.
52 |
53 | Whether this will be run on its own thread, the server's or a whole new
54 | process depends on the server you instantiated, look at their documentation.
55 |
56 | This method is called after a basic sanity check was done on the datagram,
57 | basically whether this datagram looks like an osc message or bundle,
58 | if not the server won't even bother to call it and so no new
59 | threads/processes will be spawned.
60 | """
61 |
62 | def handle(self) -> None:
63 | LOG.debug("handle OSC 1.0 protocol")
64 | while True:
65 | lengthbuf = self.recvall(4)
66 | if lengthbuf == b"":
67 | break
68 | (length,) = struct.unpack("!I", lengthbuf)
69 | data = self.recvall(length)
70 | if data == b"":
71 | break
72 |
73 | resp = self.server.dispatcher.call_handlers_for_packet(
74 | data, self.client_address
75 | )
76 | # resp = _call_handlers_for_packet(data, self.server.dispatcher)
77 | for r in resp:
78 | if not isinstance(r, tuple):
79 | r = [r]
80 | msg = osc_message_builder.build_msg(r[0], r[1:])
81 | b = struct.pack("!I", len(msg.dgram))
82 | self.request.sendall(b + msg.dgram)
83 |
84 | def recvall(self, count: int) -> bytes:
85 | buf = b""
86 | while count > 0:
87 | newbuf = self.request.recv(count)
88 | if not newbuf:
89 | return b""
90 | buf += newbuf
91 | count -= len(newbuf)
92 | return buf
93 |
94 |
95 | class _TCPHandler1_1(socketserver.BaseRequestHandler):
96 | """Handles correct OSC1.1 messages.
97 |
98 | Whether this will be run on its own thread, the server's or a whole new
99 | process depends on the server you instantiated, look at their documentation.
100 |
101 | This method is called after a basic sanity check was done on the datagram,
102 | basically whether this datagram looks like an osc message or bundle,
103 | if not the server won't even bother to call it and so no new
104 | threads/processes will be spawned.
105 | """
106 |
107 | def handle(self) -> None:
108 | LOG.debug("handle OSC 1.1 protocol")
109 | while True:
110 | packets = self.recvall()
111 | if not packets:
112 | break
113 |
114 | for p in packets:
115 | # resp = _call_handlers_for_packet(p, self.server.dispatcher)
116 | resp = self.server.dispatcher.call_handlers_for_packet(
117 | p, self.client_address
118 | )
119 | for r in resp:
120 | if not isinstance(r, tuple):
121 | r = [r]
122 | msg = osc_message_builder.build_msg(r[0], r[1:])
123 | self.request.sendall(slip.encode(msg.dgram))
124 |
125 | def recvall(self) -> List[bytes]:
126 | buf = self.request.recv(4096)
127 | if not buf:
128 | return []
129 | # If the last byte is not an END marker there could be more data coming
130 | while buf[-1] != 192:
131 | newbuf = self.request.recv(4096)
132 | if not newbuf:
133 | # Maybe should raise an exception here?
134 | break
135 | buf += newbuf
136 |
137 | packets = [slip.decode(p) for p in buf.split(slip.END_END)]
138 | return packets
139 |
140 |
141 | class OSCTCPServer(socketserver.TCPServer):
142 | """Superclass for different flavors of OSCTCPServer"""
143 |
144 | def __init__(
145 | self,
146 | server_address: Tuple[str | bytes | bytearray, int],
147 | dispatcher: Dispatcher,
148 | mode: str = MODE_1_1,
149 | ):
150 | self.request_queue_size = 300
151 | self.mode = mode
152 | if mode not in [MODE_1_0, MODE_1_1]:
153 | raise ValueError("OSC Mode must be '1.0' or '1.1'")
154 | if self.mode == MODE_1_0:
155 | super().__init__(server_address, _TCPHandler1_0)
156 | else:
157 | super().__init__(server_address, _TCPHandler1_1)
158 | self._dispatcher = dispatcher
159 |
160 | @property
161 | def dispatcher(self):
162 | """Dispatcher accessor for handlers to dispatch osc messages."""
163 | return self._dispatcher
164 |
165 |
166 | class BlockingOSCTCPServer(OSCTCPServer):
167 | """Blocking version of the TCP server.
168 |
169 | Each message will be handled sequentially on the same thread.
170 | Use this is you don't care about latency in your message handling or don't
171 | have a multiprocess/multithread environment (really?).
172 | """
173 |
174 |
175 | class ThreadingOSCTCPServer(socketserver.ThreadingMixIn, OSCTCPServer):
176 | """Threading version of the OSC TCP server.
177 |
178 | Each message will be handled in its own new thread.
179 | Use this when lightweight operations are done by each message handlers.
180 | """
181 |
182 |
183 | if hasattr(os, "fork"):
184 |
185 | class ForkingOSCTCPServer(socketserver.ForkingMixIn, OSCTCPServer):
186 | """Forking version of the OSC TCP server.
187 |
188 | Each message will be handled in its own new process.
189 | Use this when heavyweight operations are done by each message handlers
190 | and forking a whole new process for each of them is worth it.
191 | """
192 |
193 |
194 | class AsyncOSCTCPServer:
195 | """Asyncio version of the OSC TCP Server.
196 | Each TCP message is handled by _call_handlers_for_packet, the same method as in the
197 | OSCTCPServer family of blocking, threading, and forking servers
198 | """
199 |
200 | def __init__(
201 | self,
202 | server_address: str,
203 | port: int,
204 | dispatcher: Dispatcher,
205 | mode: str = MODE_1_1,
206 | ):
207 | """
208 | :param server_address: tuple of (IP address to bind to, port)
209 | :param dispatcher: a pythonosc.dispatcher.Dispatcher
210 | """
211 | self._port = port
212 | self._server_address = server_address
213 | self._dispatcher = dispatcher
214 | self._mode = mode
215 |
216 | async def __aenter__(self):
217 | return self
218 |
219 | async def __aexit__(self, exc_type, exc_val, exc_tb):
220 | await self.stop()
221 |
222 | async def start(self) -> None:
223 | """creates a socket endpoint and registers it with our event loop"""
224 | self._server = await asyncio.start_server(
225 | self.handle, self._server_address, self._port
226 | )
227 |
228 | addrs = ", ".join(str(sock.getsockname()) for sock in self._server.sockets)
229 | LOG.debug(f"Serving on {addrs}")
230 |
231 | async with self._server:
232 | await self._server.serve_forever()
233 |
234 | async def stop(self) -> None:
235 | self._server.close()
236 | await self._server.wait_closed()
237 |
238 | @property
239 | def dispatcher(self):
240 | return self._dispatcher
241 |
242 | async def handle(
243 | self, reader: asyncio.StreamReader, writer: asyncio.StreamWriter
244 | ) -> None:
245 | client_address = ("", 0)
246 | sock = writer.transport.get_extra_info("socket")
247 | if sock is not None:
248 | client_address = sock.getpeername()
249 |
250 | if self._mode == MODE_1_1:
251 | await self.handle_1_1(reader, writer, client_address)
252 | else:
253 | await self.handle1_0(reader, writer, client_address)
254 | writer.write_eof()
255 | LOG.debug("Close the connection")
256 | writer.close()
257 | await writer.wait_closed()
258 |
259 | async def handle1_0(
260 | self,
261 | reader: asyncio.StreamReader,
262 | writer: asyncio.StreamWriter,
263 | client_address: Tuple[str, int],
264 | ) -> None:
265 | LOG.debug("Incoming socket open 1.0")
266 | while True:
267 | try:
268 | buf = await reader.read(4)
269 | except Exception as e:
270 | LOG.exception("Read error", e)
271 | return
272 | if buf == b"":
273 | break
274 | (length,) = struct.unpack("!I", buf)
275 | buf = b""
276 | while length > 0:
277 | newbuf = await reader.read(length)
278 | if not newbuf:
279 | break
280 | buf += newbuf
281 | length -= len(newbuf)
282 |
283 | result = await self.dispatcher.async_call_handlers_for_packet(
284 | buf, client_address
285 | )
286 | for r in result:
287 | if not isinstance(r, tuple):
288 | r = [r]
289 | msg = osc_message_builder.build_msg(r[0], r[1:])
290 | b = struct.pack("!I", len(msg.dgram))
291 | writer.write(b + msg.dgram)
292 | await writer.drain()
293 |
294 | async def handle_1_1(
295 | self,
296 | reader: asyncio.StreamReader,
297 | writer: asyncio.StreamWriter,
298 | client_address: Tuple[str, int],
299 | ) -> None:
300 | LOG.debug("Incoming socket open 1.1")
301 | while True:
302 | try:
303 | buf = await reader.read(4096)
304 | except Exception as e:
305 | LOG.exception("Read error", e)
306 | return
307 | if buf == b"":
308 | break
309 | while len(buf) > 0 and buf[-1] != 192:
310 | newbuf = await reader.read(4096)
311 | if not newbuf:
312 | # Maybe should raise an exception here?
313 | break
314 | buf += newbuf
315 |
316 | packets = [slip.decode(p) for p in buf.split(slip.END_END)]
317 | for p in packets:
318 | result = await self.dispatcher.async_call_handlers_for_packet(
319 | p, client_address
320 | )
321 | for r in result:
322 | if not isinstance(r, tuple):
323 | r = [r]
324 | msg = osc_message_builder.build_msg(r[0], r[1:])
325 | writer.write(slip.encode(msg.dgram))
326 | await writer.drain()
327 |
--------------------------------------------------------------------------------
/pythonosc/test/test_osc_message.py:
--------------------------------------------------------------------------------
1 | import unittest
2 |
3 | from pythonosc import osc_message
4 |
5 | from datetime import datetime
6 |
7 | # Datagrams sent by Reaktor 5.8 by Native Instruments (c).
8 | _DGRAM_KNOB_ROTATES = b"/FB\x00" b",f\x00\x00" b">xca=q"
9 |
10 | _DGRAM_SWITCH_GOES_OFF = b"/SYNC\x00\x00\x00" b",f\x00\x00" b"\x00\x00\x00\x00"
11 |
12 | _DGRAM_SWITCH_GOES_ON = b"/SYNC\x00\x00\x00" b",f\x00\x00" b"?\x00\x00\x00"
13 |
14 | _DGRAM_NO_PARAMS = b"/SYNC\x00\x00\x00"
15 |
16 | _DGRAM_ALL_STANDARD_TYPES_OF_PARAMS = (
17 | b"/SYNC\x00\x00\x00"
18 | b",ifsb\x00\x00\x00"
19 | b"\x00\x00\x00\x03" # 3
20 | b"@\x00\x00\x00" # 2.0
21 | b"ABC\x00" # "ABC"
22 | b"\x00\x00\x00\x08stuff\x00\x00\x00"
23 | ) # b"stuff\x00\x00\x00"
24 |
25 | _DGRAM_ALL_NON_STANDARD_TYPES_OF_PARAMS = (
26 | b"/SYNC\x00\x00\x00"
27 | b"T" # True
28 | b"F" # False
29 | b"N" # Nil
30 | b"[]th\x00" # Empty array
31 | b"\x00\x00\x00\x00\x00\x00\x00\x00"
32 | b"\x00\x00\x00\xe8\xd4\xa5\x10\x00" # 1000000000000
33 | )
34 |
35 | _DGRAM_COMPLEX_ARRAY_PARAMS = (
36 | b"/SYNC\x00\x00\x00"
37 | b",[i][[ss]][[i][i[s]]]\x00\x00\x00"
38 | b"\x00\x00\x00\x01" # 1
39 | b"ABC\x00" # "ABC"
40 | b"DEF\x00" # "DEF"
41 | b"\x00\x00\x00\x02" # 2
42 | b"\x00\x00\x00\x03" # 3
43 | b"GHI\x00"
44 | ) # "GHI"
45 |
46 | _DGRAM_UNKNOWN_PARAM_TYPE = (
47 | b"/SYNC\x00\x00\x00"
48 | b",fx\x00" # x is an unknown param type.
49 | b"?\x00\x00\x00"
50 | )
51 |
52 | # range(512) param list.
53 | _DGRAM_LONG_LIST = b"/SYNC\x00\x00\x00,[iiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiii]\x00\x00\x00\x00\x00\x00\x00\x00\x01\x00\x00\x00\x02\x00\x00\x00\x03\x00\x00\x00\x04\x00\x00\x00\x05\x00\x00\x00\x06\x00\x00\x00\x07\x00\x00\x00\x08\x00\x00\x00\t\x00\x00\x00\n\x00\x00\x00\x0b\x00\x00\x00\x0c\x00\x00\x00\r\x00\x00\x00\x0e\x00\x00\x00\x0f\x00\x00\x00\x10\x00\x00\x00\x11\x00\x00\x00\x12\x00\x00\x00\x13\x00\x00\x00\x14\x00\x00\x00\x15\x00\x00\x00\x16\x00\x00\x00\x17\x00\x00\x00\x18\x00\x00\x00\x19\x00\x00\x00\x1a\x00\x00\x00\x1b\x00\x00\x00\x1c\x00\x00\x00\x1d\x00\x00\x00\x1e\x00\x00\x00\x1f\x00\x00\x00 \x00\x00\x00!\x00\x00\x00\"\x00\x00\x00#\x00\x00\x00$\x00\x00\x00%\x00\x00\x00&\x00\x00\x00'\x00\x00\x00(\x00\x00\x00)\x00\x00\x00*\x00\x00\x00+\x00\x00\x00,\x00\x00\x00-\x00\x00\x00.\x00\x00\x00/\x00\x00\x000\x00\x00\x001\x00\x00\x002\x00\x00\x003\x00\x00\x004\x00\x00\x005\x00\x00\x006\x00\x00\x007\x00\x00\x008\x00\x00\x009\x00\x00\x00:\x00\x00\x00;\x00\x00\x00<\x00\x00\x00=\x00\x00\x00>\x00\x00\x00?\x00\x00\x00@\x00\x00\x00A\x00\x00\x00B\x00\x00\x00C\x00\x00\x00D\x00\x00\x00E\x00\x00\x00F\x00\x00\x00G\x00\x00\x00H\x00\x00\x00I\x00\x00\x00J\x00\x00\x00K\x00\x00\x00L\x00\x00\x00M\x00\x00\x00N\x00\x00\x00O\x00\x00\x00P\x00\x00\x00Q\x00\x00\x00R\x00\x00\x00S\x00\x00\x00T\x00\x00\x00U\x00\x00\x00V\x00\x00\x00W\x00\x00\x00X\x00\x00\x00Y\x00\x00\x00Z\x00\x00\x00[\x00\x00\x00\\\x00\x00\x00]\x00\x00\x00^\x00\x00\x00_\x00\x00\x00`\x00\x00\x00a\x00\x00\x00b\x00\x00\x00c\x00\x00\x00d\x00\x00\x00e\x00\x00\x00f\x00\x00\x00g\x00\x00\x00h\x00\x00\x00i\x00\x00\x00j\x00\x00\x00k\x00\x00\x00l\x00\x00\x00m\x00\x00\x00n\x00\x00\x00o\x00\x00\x00p\x00\x00\x00q\x00\x00\x00r\x00\x00\x00s\x00\x00\x00t\x00\x00\x00u\x00\x00\x00v\x00\x00\x00w\x00\x00\x00x\x00\x00\x00y\x00\x00\x00z\x00\x00\x00{\x00\x00\x00|\x00\x00\x00}\x00\x00\x00~\x00\x00\x00\x7f\x00\x00\x00\x80\x00\x00\x00\x81\x00\x00\x00\x82\x00\x00\x00\x83\x00\x00\x00\x84\x00\x00\x00\x85\x00\x00\x00\x86\x00\x00\x00\x87\x00\x00\x00\x88\x00\x00\x00\x89\x00\x00\x00\x8a\x00\x00\x00\x8b\x00\x00\x00\x8c\x00\x00\x00\x8d\x00\x00\x00\x8e\x00\x00\x00\x8f\x00\x00\x00\x90\x00\x00\x00\x91\x00\x00\x00\x92\x00\x00\x00\x93\x00\x00\x00\x94\x00\x00\x00\x95\x00\x00\x00\x96\x00\x00\x00\x97\x00\x00\x00\x98\x00\x00\x00\x99\x00\x00\x00\x9a\x00\x00\x00\x9b\x00\x00\x00\x9c\x00\x00\x00\x9d\x00\x00\x00\x9e\x00\x00\x00\x9f\x00\x00\x00\xa0\x00\x00\x00\xa1\x00\x00\x00\xa2\x00\x00\x00\xa3\x00\x00\x00\xa4\x00\x00\x00\xa5\x00\x00\x00\xa6\x00\x00\x00\xa7\x00\x00\x00\xa8\x00\x00\x00\xa9\x00\x00\x00\xaa\x00\x00\x00\xab\x00\x00\x00\xac\x00\x00\x00\xad\x00\x00\x00\xae\x00\x00\x00\xaf\x00\x00\x00\xb0\x00\x00\x00\xb1\x00\x00\x00\xb2\x00\x00\x00\xb3\x00\x00\x00\xb4\x00\x00\x00\xb5\x00\x00\x00\xb6\x00\x00\x00\xb7\x00\x00\x00\xb8\x00\x00\x00\xb9\x00\x00\x00\xba\x00\x00\x00\xbb\x00\x00\x00\xbc\x00\x00\x00\xbd\x00\x00\x00\xbe\x00\x00\x00\xbf\x00\x00\x00\xc0\x00\x00\x00\xc1\x00\x00\x00\xc2\x00\x00\x00\xc3\x00\x00\x00\xc4\x00\x00\x00\xc5\x00\x00\x00\xc6\x00\x00\x00\xc7\x00\x00\x00\xc8\x00\x00\x00\xc9\x00\x00\x00\xca\x00\x00\x00\xcb\x00\x00\x00\xcc\x00\x00\x00\xcd\x00\x00\x00\xce\x00\x00\x00\xcf\x00\x00\x00\xd0\x00\x00\x00\xd1\x00\x00\x00\xd2\x00\x00\x00\xd3\x00\x00\x00\xd4\x00\x00\x00\xd5\x00\x00\x00\xd6\x00\x00\x00\xd7\x00\x00\x00\xd8\x00\x00\x00\xd9\x00\x00\x00\xda\x00\x00\x00\xdb\x00\x00\x00\xdc\x00\x00\x00\xdd\x00\x00\x00\xde\x00\x00\x00\xdf\x00\x00\x00\xe0\x00\x00\x00\xe1\x00\x00\x00\xe2\x00\x00\x00\xe3\x00\x00\x00\xe4\x00\x00\x00\xe5\x00\x00\x00\xe6\x00\x00\x00\xe7\x00\x00\x00\xe8\x00\x00\x00\xe9\x00\x00\x00\xea\x00\x00\x00\xeb\x00\x00\x00\xec\x00\x00\x00\xed\x00\x00\x00\xee\x00\x00\x00\xef\x00\x00\x00\xf0\x00\x00\x00\xf1\x00\x00\x00\xf2\x00\x00\x00\xf3\x00\x00\x00\xf4\x00\x00\x00\xf5\x00\x00\x00\xf6\x00\x00\x00\xf7\x00\x00\x00\xf8\x00\x00\x00\xf9\x00\x00\x00\xfa\x00\x00\x00\xfb\x00\x00\x00\xfc\x00\x00\x00\xfd\x00\x00\x00\xfe\x00\x00\x00\xff\x00\x00\x01\x00\x00\x00\x01\x01\x00\x00\x01\x02\x00\x00\x01\x03\x00\x00\x01\x04\x00\x00\x01\x05\x00\x00\x01\x06\x00\x00\x01\x07\x00\x00\x01\x08\x00\x00\x01\t\x00\x00\x01\n\x00\x00\x01\x0b\x00\x00\x01\x0c\x00\x00\x01\r\x00\x00\x01\x0e\x00\x00\x01\x0f\x00\x00\x01\x10\x00\x00\x01\x11\x00\x00\x01\x12\x00\x00\x01\x13\x00\x00\x01\x14\x00\x00\x01\x15\x00\x00\x01\x16\x00\x00\x01\x17\x00\x00\x01\x18\x00\x00\x01\x19\x00\x00\x01\x1a\x00\x00\x01\x1b\x00\x00\x01\x1c\x00\x00\x01\x1d\x00\x00\x01\x1e\x00\x00\x01\x1f\x00\x00\x01 \x00\x00\x01!\x00\x00\x01\"\x00\x00\x01#\x00\x00\x01$\x00\x00\x01%\x00\x00\x01&\x00\x00\x01'\x00\x00\x01(\x00\x00\x01)\x00\x00\x01*\x00\x00\x01+\x00\x00\x01,\x00\x00\x01-\x00\x00\x01.\x00\x00\x01/\x00\x00\x010\x00\x00\x011\x00\x00\x012\x00\x00\x013\x00\x00\x014\x00\x00\x015\x00\x00\x016\x00\x00\x017\x00\x00\x018\x00\x00\x019\x00\x00\x01:\x00\x00\x01;\x00\x00\x01<\x00\x00\x01=\x00\x00\x01>\x00\x00\x01?\x00\x00\x01@\x00\x00\x01A\x00\x00\x01B\x00\x00\x01C\x00\x00\x01D\x00\x00\x01E\x00\x00\x01F\x00\x00\x01G\x00\x00\x01H\x00\x00\x01I\x00\x00\x01J\x00\x00\x01K\x00\x00\x01L\x00\x00\x01M\x00\x00\x01N\x00\x00\x01O\x00\x00\x01P\x00\x00\x01Q\x00\x00\x01R\x00\x00\x01S\x00\x00\x01T\x00\x00\x01U\x00\x00\x01V\x00\x00\x01W\x00\x00\x01X\x00\x00\x01Y\x00\x00\x01Z\x00\x00\x01[\x00\x00\x01\\\x00\x00\x01]\x00\x00\x01^\x00\x00\x01_\x00\x00\x01`\x00\x00\x01a\x00\x00\x01b\x00\x00\x01c\x00\x00\x01d\x00\x00\x01e\x00\x00\x01f\x00\x00\x01g\x00\x00\x01h\x00\x00\x01i\x00\x00\x01j\x00\x00\x01k\x00\x00\x01l\x00\x00\x01m\x00\x00\x01n\x00\x00\x01o\x00\x00\x01p\x00\x00\x01q\x00\x00\x01r\x00\x00\x01s\x00\x00\x01t\x00\x00\x01u\x00\x00\x01v\x00\x00\x01w\x00\x00\x01x\x00\x00\x01y\x00\x00\x01z\x00\x00\x01{\x00\x00\x01|\x00\x00\x01}\x00\x00\x01~\x00\x00\x01\x7f\x00\x00\x01\x80\x00\x00\x01\x81\x00\x00\x01\x82\x00\x00\x01\x83\x00\x00\x01\x84\x00\x00\x01\x85\x00\x00\x01\x86\x00\x00\x01\x87\x00\x00\x01\x88\x00\x00\x01\x89\x00\x00\x01\x8a\x00\x00\x01\x8b\x00\x00\x01\x8c\x00\x00\x01\x8d\x00\x00\x01\x8e\x00\x00\x01\x8f\x00\x00\x01\x90\x00\x00\x01\x91\x00\x00\x01\x92\x00\x00\x01\x93\x00\x00\x01\x94\x00\x00\x01\x95\x00\x00\x01\x96\x00\x00\x01\x97\x00\x00\x01\x98\x00\x00\x01\x99\x00\x00\x01\x9a\x00\x00\x01\x9b\x00\x00\x01\x9c\x00\x00\x01\x9d\x00\x00\x01\x9e\x00\x00\x01\x9f\x00\x00\x01\xa0\x00\x00\x01\xa1\x00\x00\x01\xa2\x00\x00\x01\xa3\x00\x00\x01\xa4\x00\x00\x01\xa5\x00\x00\x01\xa6\x00\x00\x01\xa7\x00\x00\x01\xa8\x00\x00\x01\xa9\x00\x00\x01\xaa\x00\x00\x01\xab\x00\x00\x01\xac\x00\x00\x01\xad\x00\x00\x01\xae\x00\x00\x01\xaf\x00\x00\x01\xb0\x00\x00\x01\xb1\x00\x00\x01\xb2\x00\x00\x01\xb3\x00\x00\x01\xb4\x00\x00\x01\xb5\x00\x00\x01\xb6\x00\x00\x01\xb7\x00\x00\x01\xb8\x00\x00\x01\xb9\x00\x00\x01\xba\x00\x00\x01\xbb\x00\x00\x01\xbc\x00\x00\x01\xbd\x00\x00\x01\xbe\x00\x00\x01\xbf\x00\x00\x01\xc0\x00\x00\x01\xc1\x00\x00\x01\xc2\x00\x00\x01\xc3\x00\x00\x01\xc4\x00\x00\x01\xc5\x00\x00\x01\xc6\x00\x00\x01\xc7\x00\x00\x01\xc8\x00\x00\x01\xc9\x00\x00\x01\xca\x00\x00\x01\xcb\x00\x00\x01\xcc\x00\x00\x01\xcd\x00\x00\x01\xce\x00\x00\x01\xcf\x00\x00\x01\xd0\x00\x00\x01\xd1\x00\x00\x01\xd2\x00\x00\x01\xd3\x00\x00\x01\xd4\x00\x00\x01\xd5\x00\x00\x01\xd6\x00\x00\x01\xd7\x00\x00\x01\xd8\x00\x00\x01\xd9\x00\x00\x01\xda\x00\x00\x01\xdb\x00\x00\x01\xdc\x00\x00\x01\xdd\x00\x00\x01\xde\x00\x00\x01\xdf\x00\x00\x01\xe0\x00\x00\x01\xe1\x00\x00\x01\xe2\x00\x00\x01\xe3\x00\x00\x01\xe4\x00\x00\x01\xe5\x00\x00\x01\xe6\x00\x00\x01\xe7\x00\x00\x01\xe8\x00\x00\x01\xe9\x00\x00\x01\xea\x00\x00\x01\xeb\x00\x00\x01\xec\x00\x00\x01\xed\x00\x00\x01\xee\x00\x00\x01\xef\x00\x00\x01\xf0\x00\x00\x01\xf1\x00\x00\x01\xf2\x00\x00\x01\xf3\x00\x00\x01\xf4\x00\x00\x01\xf5\x00\x00\x01\xf6\x00\x00\x01\xf7\x00\x00\x01\xf8\x00\x00\x01\xf9\x00\x00\x01\xfa\x00\x00\x01\xfb\x00\x00\x01\xfc\x00\x00\x01\xfd\x00\x00\x01\xfe\x00\x00\x01\xff"
54 |
55 |
56 | class TestOscMessage(unittest.TestCase):
57 | def test_switch_goes_off(self):
58 | msg = osc_message.OscMessage(_DGRAM_SWITCH_GOES_OFF)
59 | self.assertEqual("/SYNC", msg.address)
60 | self.assertEqual(1, len(msg.params))
61 | self.assertTrue(isinstance(msg.params[0], float))
62 | self.assertAlmostEqual(0.0, msg.params[0])
63 |
64 | def test_switch_goes_on(self):
65 | msg = osc_message.OscMessage(_DGRAM_SWITCH_GOES_ON)
66 | self.assertEqual("/SYNC", msg.address)
67 | self.assertEqual(1, len(msg.params))
68 | self.assertTrue(isinstance(msg.params[0], float))
69 | self.assertAlmostEqual(0.5, msg.params[0])
70 |
71 | def test_knob_rotates(self):
72 | msg = osc_message.OscMessage(_DGRAM_KNOB_ROTATES)
73 | self.assertEqual("/FB", msg.address)
74 | self.assertEqual(1, len(msg.params))
75 | self.assertTrue(isinstance(msg.params[0], float))
76 |
77 | def test_no_params(self):
78 | msg = osc_message.OscMessage(_DGRAM_NO_PARAMS)
79 | self.assertEqual("/SYNC", msg.address)
80 | self.assertEqual(0, len(msg.params))
81 |
82 | def test_all_standard_types_off_params(self):
83 | msg = osc_message.OscMessage(_DGRAM_ALL_STANDARD_TYPES_OF_PARAMS)
84 | self.assertEqual("/SYNC", msg.address)
85 | self.assertEqual(4, len(msg.params))
86 | self.assertEqual(3, msg.params[0])
87 | self.assertAlmostEqual(2.0, msg.params[1])
88 | self.assertEqual("ABC", msg.params[2])
89 | self.assertEqual(b"stuff\x00\x00\x00", msg.params[3])
90 | self.assertEqual(4, len(list(msg)))
91 |
92 | def test_all_non_standard_params(self):
93 | msg = osc_message.OscMessage(_DGRAM_ALL_NON_STANDARD_TYPES_OF_PARAMS)
94 |
95 | self.assertEqual("/SYNC", msg.address)
96 | self.assertEqual(6, len(msg.params))
97 | self.assertEqual(True, msg.params[0])
98 | self.assertEqual(False, msg.params[1])
99 | self.assertEqual(None, msg.params[2])
100 | self.assertEqual([], msg.params[3])
101 | self.assertEqual((datetime(1900, 1, 1, 0, 0, 0), 0), msg.params[4])
102 | self.assertEqual(1000000000000, msg.params[5])
103 | self.assertEqual(6, len(list(msg)))
104 |
105 | def test_complex_array_params(self):
106 | msg = osc_message.OscMessage(_DGRAM_COMPLEX_ARRAY_PARAMS)
107 | self.assertEqual("/SYNC", msg.address)
108 | self.assertEqual(3, len(msg.params))
109 | self.assertEqual([1], msg.params[0])
110 | self.assertEqual([["ABC", "DEF"]], msg.params[1])
111 | self.assertEqual([[2], [3, ["GHI"]]], msg.params[2])
112 | self.assertEqual(3, len(list(msg)))
113 |
114 | def test_raises_on_empty_datargram(self):
115 | self.assertRaises(osc_message.ParseError, osc_message.OscMessage, b"")
116 |
117 | def test_ignores_unknown_param(self):
118 | msg = osc_message.OscMessage(_DGRAM_UNKNOWN_PARAM_TYPE)
119 | self.assertEqual("/SYNC", msg.address)
120 | self.assertEqual(1, len(msg.params))
121 | self.assertTrue(isinstance(msg.params[0], float))
122 | self.assertAlmostEqual(0.5, msg.params[0])
123 |
124 | def test_raises_on_invalid_array(self):
125 | self.assertRaises(
126 | osc_message.ParseError, osc_message.OscMessage, b"/SYNC\x00\x00\x00[]]\x00"
127 | )
128 | self.assertRaises(
129 | osc_message.ParseError, osc_message.OscMessage, b"/SYNC\x00\x00\x00[[]\x00"
130 | )
131 |
132 | def test_raises_on_incorrect_datargram(self):
133 | self.assertRaises(osc_message.ParseError, osc_message.OscMessage, b"foobar")
134 |
135 | def test_parse_long_params_list(self):
136 | msg = osc_message.OscMessage(_DGRAM_LONG_LIST)
137 | self.assertEqual("/SYNC", msg.address)
138 | self.assertEqual(1, len(msg.params))
139 | self.assertEqual(512, len(msg.params[0]))
140 |
141 |
142 | if __name__ == "__main__":
143 | unittest.main()
144 |
--------------------------------------------------------------------------------
/pythonosc/test/parsing/test_osc_types.py:
--------------------------------------------------------------------------------
1 | """Unit tests for the osc_types module."""
2 |
3 | import unittest
4 |
5 | from pythonosc.parsing import ntp
6 | from pythonosc.parsing import osc_types
7 |
8 | from datetime import datetime
9 |
10 |
11 | class TestString(unittest.TestCase):
12 | def test_get_string(self):
13 | cases = {
14 | b"A\x00\x00\x00": ("A", 4),
15 | b"AB\x00\x00": ("AB", 4),
16 | b"ABC\x00": ("ABC", 4),
17 | b"ABCD\x00\x00\x00\x00": ("ABCD", 8),
18 | b"ABCD\x00\x00\x00\x00GARBAGE": ("ABCD", 8),
19 | b"\x00\x00\x00\x00": ("", 4),
20 | }
21 |
22 | for dgram, expected in cases.items():
23 | self.assertEqual(expected, osc_types.get_string(dgram, 0))
24 |
25 | def test_get_string_raises_on_wrong_dgram(self):
26 | cases = [
27 | b"blablaba",
28 | b"",
29 | b"\x00",
30 | b"\x00\x00",
31 | True,
32 | ]
33 |
34 | for case in cases:
35 | self.assertRaises(osc_types.ParseError, osc_types.get_string, case, 0)
36 |
37 | def test_get_string_raises_when_datagram_too_short(self):
38 | self.assertRaises(osc_types.ParseError, osc_types.get_string, b"abc\x00", 1)
39 |
40 | def test_get_string_raises_on_wrong_start_index_negative(self):
41 | self.assertRaises(osc_types.ParseError, osc_types.get_string, b"abc\x00", -1)
42 |
43 |
44 | class TestInteger(unittest.TestCase):
45 | def test_get_integer(self):
46 | cases = {
47 | b"\x00\x00\x00\x00": (0, 4),
48 | b"\x00\x00\x00\x01": (1, 4),
49 | b"\x00\x00\x00\x02": (2, 4),
50 | b"\x00\x00\x00\x03": (3, 4),
51 | b"\x00\x00\x01\x00": (256, 4),
52 | b"\x00\x01\x00\x00": (65536, 4),
53 | b"\x01\x00\x00\x00": (16777216, 4),
54 | b"\x00\x00\x00\x01GARBAGE": (1, 4),
55 | }
56 |
57 | for dgram, expected in cases.items():
58 | self.assertEqual(expected, osc_types.get_int(dgram, 0))
59 |
60 | def test_get_integer_raises_on_type_error(self):
61 | cases = [b"", True]
62 |
63 | for case in cases:
64 | self.assertRaises(osc_types.ParseError, osc_types.get_int, case, 0)
65 |
66 | def test_get_integer_raises_on_wrong_start_index(self):
67 | self.assertRaises(
68 | osc_types.ParseError, osc_types.get_int, b"\x00\x00\x00\x11", 1
69 | )
70 |
71 | def test_get_integer_raises_on_wrong_start_index_negative(self):
72 | self.assertRaises(
73 | osc_types.ParseError, osc_types.get_int, b"\x00\x00\x00\x00", -1
74 | )
75 |
76 | def test_datagram_too_short(self):
77 | dgram = b"\x00" * 3
78 | self.assertRaises(osc_types.ParseError, osc_types.get_int, dgram, 2)
79 |
80 |
81 | class TestRGBA(unittest.TestCase):
82 | def test_get_rgba(self):
83 | cases = {
84 | b"\x00\x00\x00\x00": (0, 4),
85 | b"\x00\x00\x00\x01": (1, 4),
86 | b"\x00\x00\x00\x02": (2, 4),
87 | b"\x00\x00\x00\x03": (3, 4),
88 | b"\xff\x00\x00\x00": (4278190080, 4),
89 | b"\x00\xff\x00\x00": (16711680, 4),
90 | b"\x00\x00\xff\x00": (65280, 4),
91 | b"\x00\x00\x00\xff": (255, 4),
92 | b"\x00\x00\x00\x01GARBAGE": (1, 4),
93 | }
94 |
95 | for dgram, expected in cases.items():
96 | self.assertEqual(expected, osc_types.get_rgba(dgram, 0))
97 |
98 | def test_get_rgba_raises_on_type_error(self):
99 | cases = [b"", True]
100 |
101 | for case in cases:
102 | self.assertRaises(osc_types.ParseError, osc_types.get_rgba, case, 0)
103 |
104 | def test_get_rgba_raises_on_wrong_start_index(self):
105 | self.assertRaises(
106 | osc_types.ParseError, osc_types.get_rgba, b"\x00\x00\x00\x11", 1
107 | )
108 |
109 | def test_get_rgba_raises_on_wrong_start_index_negative(self):
110 | self.assertRaises(
111 | osc_types.ParseError, osc_types.get_rgba, b"\x00\x00\x00\x00", -1
112 | )
113 |
114 | def test_datagram_too_short(self):
115 | dgram = b"\x00" * 3
116 | self.assertRaises(osc_types.ParseError, osc_types.get_rgba, dgram, 2)
117 |
118 |
119 | class TestMidi(unittest.TestCase):
120 | def test_get_midi(self):
121 | cases = {
122 | b"\x00\x00\x00\x00": ((0, 0, 0, 0), 4),
123 | b"\x00\x00\x00\x01": ((0, 0, 0, 1), 4),
124 | b"\x00\x00\x00\x02": ((0, 0, 0, 2), 4),
125 | b"\x00\x00\x00\x03": ((0, 0, 0, 3), 4),
126 | b"\x00\x00\x01\x00": ((0, 0, 1, 0), 4),
127 | b"\x00\x01\x00\x00": ((0, 1, 0, 0), 4),
128 | b"\x01\x00\x00\x00": ((1, 0, 0, 0), 4),
129 | b"\x00\x00\x00\x01GARBAGE": ((0, 0, 0, 1), 4),
130 | }
131 |
132 | for dgram, expected in cases.items():
133 | self.assertEqual(expected, osc_types.get_midi(dgram, 0))
134 |
135 | def test_get_midi_raises_on_type_error(self):
136 | cases = [b"", True]
137 |
138 | for case in cases:
139 | self.assertRaises(osc_types.ParseError, osc_types.get_midi, case, 0)
140 |
141 | def test_get_midi_raises_on_wrong_start_index(self):
142 | self.assertRaises(
143 | osc_types.ParseError, osc_types.get_midi, b"\x00\x00\x00\x11", 1
144 | )
145 |
146 | def test_get_midi_raises_on_wrong_start_index_negative(self):
147 | self.assertRaises(
148 | osc_types.ParseError, osc_types.get_midi, b"\x00\x00\x00\x00", -1
149 | )
150 |
151 | def test_datagram_too_short(self):
152 | dgram = b"\x00" * 3
153 | self.assertRaises(osc_types.ParseError, osc_types.get_midi, dgram, 2)
154 |
155 |
156 | class TestDate(unittest.TestCase):
157 | def test_get_timetag(self):
158 | cases = {
159 | b"\xde\x9c\x91\xbf\x00\x01\x00\x00": (
160 | (datetime(2018, 5, 8, 21, 14, 39), 65536),
161 | 8,
162 | ), # NOTE: fraction is expresed as 32bit OSC.
163 | b"\x00\x00\x00\x00\x00\x00\x00\x00": (
164 | (datetime(1900, 1, 1, 0, 0, 0), 0),
165 | 8,
166 | ),
167 | b"\x83\xaa\x7e\x80\x0a\x00\xb0\x0c": (
168 | (datetime(1970, 1, 1, 0, 0, 0), 167817228),
169 | 8,
170 | ),
171 | }
172 |
173 | for dgram, expected in cases.items():
174 | self.assertEqual(expected, osc_types.get_timetag(dgram, 0))
175 |
176 | def test_get_timetag_raises_on_wrong_start_index_negative(self):
177 | self.assertRaises(
178 | osc_types.ParseError,
179 | osc_types.get_timetag,
180 | b"\x00\x00\x00\x00\x00\x00\x00\x00",
181 | -1,
182 | )
183 |
184 | def test_get_timetag_raises_on_type_error(self):
185 | cases = [b"", True]
186 |
187 | for case in cases:
188 | self.assertRaises(osc_types.ParseError, osc_types.get_timetag, case, 0)
189 |
190 | def test_get_timetag_raises_on_wrong_start_index(self):
191 | self.assertRaises(
192 | osc_types.ParseError,
193 | osc_types.get_date,
194 | b"\x00\x00\x00\x11\x00\x00\x00\x11",
195 | 1,
196 | )
197 |
198 | def test_ttag_datagram_too_short(self):
199 | dgram = b"\x00" * 7
200 | self.assertRaises(osc_types.ParseError, osc_types.get_timetag, dgram, 6)
201 |
202 | dgram = b"\x00" * 2
203 | self.assertRaises(osc_types.ParseError, osc_types.get_timetag, dgram, 1)
204 |
205 | dgram = b"\x00" * 5
206 | self.assertRaises(osc_types.ParseError, osc_types.get_timetag, dgram, 4)
207 |
208 | dgram = b"\x00" * 1
209 | self.assertRaises(osc_types.ParseError, osc_types.get_timetag, dgram, 0)
210 |
211 |
212 | class TestFloat(unittest.TestCase):
213 | def test_get_float(self):
214 | cases = {
215 | b"\x00\x00\x00\x00": (0.0, 4),
216 | b"?\x80\x00\x00'": (1.0, 4),
217 | b"@\x00\x00\x00": (2.0, 4),
218 | b"\x00\x00\x00\x00GARBAGE": (0.0, 4),
219 | }
220 |
221 | for dgram, expected in cases.items():
222 | self.assertAlmostEqual(expected, osc_types.get_float(dgram, 0))
223 |
224 | def test_get_float_raises_on_wrong_dgram(self):
225 | cases = [True]
226 |
227 | for case in cases:
228 | self.assertRaises(osc_types.ParseError, osc_types.get_float, case, 0)
229 |
230 | def test_get_float_raises_on_type_error(self):
231 | cases = [None]
232 |
233 | for case in cases:
234 | self.assertRaises(osc_types.ParseError, osc_types.get_float, case, 0)
235 |
236 | def test_datagram_too_short_pads(self):
237 | dgram = b"\x00" * 2
238 | self.assertEqual((0, 4), osc_types.get_float(dgram, 0))
239 |
240 |
241 | class TestDouble(unittest.TestCase):
242 | def test_get_double(self):
243 | cases = {
244 | b"\x00\x00\x00\x00\x00\x00\x00\x00": (0.0, 8),
245 | b"?\xf0\x00\x00\x00\x00\x00\x00": (1.0, 8),
246 | b"@\x00\x00\x00\x00\x00\x00\x00": (2.0, 8),
247 | b"\xbf\xf0\x00\x00\x00\x00\x00\x00": (-1.0, 8),
248 | b"\xc0\x00\x00\x00\x00\x00\x00\x00": (-2.0, 8),
249 | b"\x00\x00\x00\x00\x00\x00\x00\x00GARBAGE": (0.0, 8),
250 | }
251 |
252 | for dgram, expected in cases.items():
253 | self.assertAlmostEqual(expected, osc_types.get_double(dgram, 0))
254 |
255 | def test_get_double_raises_on_wrong_dgram(self):
256 | cases = [True]
257 |
258 | for case in cases:
259 | self.assertRaises(osc_types.ParseError, osc_types.get_double, case, 0)
260 |
261 | def test_get_double_raises_on_type_error(self):
262 | cases = [None]
263 |
264 | for case in cases:
265 | self.assertRaises(osc_types.ParseError, osc_types.get_double, case, 0)
266 |
267 | def test_datagram_too_short_pads(self):
268 | dgram = b"\x00" * 2
269 | self.assertRaises(osc_types.ParseError, osc_types.get_double, dgram, 0)
270 |
271 |
272 | class TestBlob(unittest.TestCase):
273 | def test_get_blob(self):
274 | cases = {
275 | b"\x00\x00\x00\x00": (b"", 4),
276 | b"\x00\x00\x00\x08stuff\x00\x00\x00": (b"stuff\x00\x00\x00", 12),
277 | b"\x00\x00\x00\x04\x00\x00\x00\x00": (b"\x00\x00\x00\x00", 8),
278 | b"\x00\x00\x00\x02\x00\x00\x00\x00": (b"\x00\x00", 8),
279 | b"\x00\x00\x00\x08stuff\x00\x00\x00datagramcontinues": (
280 | b"stuff\x00\x00\x00",
281 | 12,
282 | ),
283 | }
284 |
285 | for dgram, expected in cases.items():
286 | self.assertEqual(expected, osc_types.get_blob(dgram, 0))
287 |
288 | def test_get_blob_raises_on_wrong_dgram(self):
289 | cases = [b"", True, b"\x00\x00\x00\x08"]
290 |
291 | for case in cases:
292 | self.assertRaises(osc_types.ParseError, osc_types.get_blob, case, 0)
293 |
294 | def test_get_blob_raises_on_wrong_start_index(self):
295 | self.assertRaises(
296 | osc_types.ParseError, osc_types.get_blob, b"\x00\x00\x00\x11", 1
297 | )
298 |
299 | def test_get_blob_raises_too_short_buffer(self):
300 | self.assertRaises(
301 | osc_types.ParseError, osc_types.get_blob, b"\x00\x00\x00\x11\x00\x00", 1
302 | )
303 |
304 | def test_get_blog_raises_on_wrong_start_index_negative(self):
305 | self.assertRaises(
306 | osc_types.ParseError, osc_types.get_blob, b"\x00\x00\x00\x00", -1
307 | )
308 |
309 |
310 | class TestNTPTimestamp(unittest.TestCase):
311 | def test_immediately_dgram(self):
312 | dgram = ntp.IMMEDIATELY
313 | self.assertEqual(osc_types.IMMEDIATELY, osc_types.get_date(dgram, 0)[0])
314 |
315 | def test_origin_of_time(self):
316 | dgram = b"\x00" * 8
317 | self.assertGreater(0, osc_types.get_date(dgram, 0)[0])
318 |
319 | def test_datagram_too_short(self):
320 | dgram = b"\x00" * 8
321 | self.assertRaises(osc_types.ParseError, osc_types.get_date, dgram, 2)
322 |
323 | def test_write_date(self):
324 | time = 1569899476.167749 # known round(time.time(), 6)
325 | timetag = b"\xe1=BT*\xf1\x98\x00"
326 | self.assertEqual(timetag, osc_types.write_date(time))
327 |
328 |
329 | class TestBuildMethods(unittest.TestCase):
330 | def test_string(self):
331 | self.assertEqual(b"\x00\x00\x00\x00", osc_types.write_string(""))
332 | self.assertEqual(b"A\x00\x00\x00", osc_types.write_string("A"))
333 | self.assertEqual(b"AB\x00\x00", osc_types.write_string("AB"))
334 | self.assertEqual(b"ABC\x00", osc_types.write_string("ABC"))
335 | self.assertEqual(b"ABCD\x00\x00\x00\x00", osc_types.write_string("ABCD"))
336 |
337 | def test_string_raises(self):
338 | self.assertRaises(osc_types.BuildError, osc_types.write_string, 123)
339 |
340 | def test_int(self):
341 | self.assertEqual(b"\x00\x00\x00\x00", osc_types.write_int(0))
342 | self.assertEqual(b"\x00\x00\x00\x01", osc_types.write_int(1))
343 |
344 | def test_int_raises(self):
345 | self.assertRaises(osc_types.BuildError, osc_types.write_int, "no int")
346 |
347 | def test_float(self):
348 | self.assertEqual(b"\x00\x00\x00\x00", osc_types.write_float(0.0))
349 | self.assertEqual(b"?\x00\x00\x00", osc_types.write_float(0.5))
350 | self.assertEqual(b"?\x80\x00\x00", osc_types.write_float(1.0))
351 | self.assertEqual(b"?\x80\x00\x00", osc_types.write_float(1))
352 |
353 | def test_float_raises(self):
354 | self.assertRaises(osc_types.BuildError, osc_types.write_float, "no float")
355 |
356 | def test_blob(self):
357 | self.assertEqual(
358 | b"\x00\x00\x00\x02\x00\x01\x00\x00", osc_types.write_blob(b"\x00\x01")
359 | )
360 | self.assertEqual(
361 | b"\x00\x00\x00\x04\x00\x01\x02\x03",
362 | osc_types.write_blob(b"\x00\x01\x02\x03"),
363 | )
364 |
365 | def test_blob_raises(self):
366 | self.assertRaises(osc_types.BuildError, osc_types.write_blob, b"")
367 |
368 |
369 | if __name__ == "__main__":
370 | unittest.main()
371 |
--------------------------------------------------------------------------------
/pythonosc/dispatcher.py:
--------------------------------------------------------------------------------
1 | """Maps OSC addresses to handler functions"""
2 |
3 | import collections
4 | import inspect
5 | import logging
6 | import re
7 | import time
8 | from pythonosc import osc_packet
9 | from typing import (
10 | overload,
11 | List,
12 | Union,
13 | Any,
14 | AnyStr,
15 | Generator,
16 | Tuple,
17 | Callable,
18 | Optional,
19 | DefaultDict,
20 | )
21 | from pythonosc.osc_message import OscMessage
22 | from pythonosc.osc_message_builder import ArgValue
23 |
24 |
25 | class Handler(object):
26 | """Wrapper for a callback function that will be called when an OSC message is sent to the right address.
27 |
28 | Represents a handler callback function that will be called whenever an OSC message is sent to the address this
29 | handler is mapped to. It passes the address, the fixed arguments (if any) as well as all osc arguments from the
30 | message if any were passed.
31 | """
32 |
33 | def __init__(
34 | self,
35 | _callback: Callable,
36 | _args: Union[Any, List[Any]],
37 | _needs_reply_address: bool = False,
38 | ) -> None:
39 | """
40 | Args:
41 | _callback Function that is called when handler is invoked
42 | _args: Message causing invocation
43 | _needs_reply_address Whether the client's ip address shall be passed as an argument or not
44 | """
45 | self.callback = _callback
46 | self.args = _args
47 | self.needs_reply_address = _needs_reply_address
48 |
49 | # needed for test module
50 | def __eq__(self, other: Any) -> bool:
51 | return (
52 | isinstance(self, type(other))
53 | and self.callback == other.callback
54 | and self.args == other.args
55 | and self.needs_reply_address == other.needs_reply_address
56 | )
57 |
58 | def invoke(
59 | self, client_address: Tuple[str, int], message: OscMessage
60 | ) -> Union[None, AnyStr, Tuple[AnyStr, ArgValue]]:
61 | """Invokes the associated callback function
62 |
63 | Args:
64 | client_address: Address match that causes the invocation
65 | message: Message causing invocation
66 | Returns:
67 | The result of the handler function can be None, a string OSC address, or a tuple of the OSC address
68 | and arguments.
69 | """
70 | if self.needs_reply_address:
71 | if self.args:
72 | return self.callback(
73 | client_address, message.address, self.args, *message
74 | )
75 | else:
76 | return self.callback(client_address, message.address, *message)
77 | else:
78 | if self.args:
79 | return self.callback(message.address, self.args, *message)
80 | else:
81 | return self.callback(message.address, *message)
82 |
83 |
84 | class Dispatcher(object):
85 | """Maps Handlers to OSC addresses and dispatches messages to the handler on matched addresses
86 |
87 | Maps OSC addresses to handler functions and invokes the correct handler when a message comes in.
88 | """
89 |
90 | def __init__(self) -> None:
91 | self._map: DefaultDict[str, List[Handler]] = collections.defaultdict(list)
92 | self._default_handler: Optional[Handler] = None
93 |
94 | def map(
95 | self,
96 | address: str,
97 | handler: Callable,
98 | *args: Union[Any, List[Any]],
99 | needs_reply_address: bool = False,
100 | ) -> Handler:
101 | """Map an address to a handler
102 |
103 | The callback function must have one of the following signatures:
104 |
105 | ``def some_cb(address: str, *osc_args: List[Any]) -> Union[None, AnyStr, Tuple(str, ArgValue)]:``
106 | ``def some_cb(address: str, fixed_args: List[Any], *osc_args: List[Any]) -> Union[None, AnyStr,
107 | Tuple(str, ArgValue)]:``
108 |
109 | ``def some_cb(client_address: Tuple[str, int], address: str, *osc_args: List[Any]) -> Union[None, AnyStr,
110 | Tuple(str, ArgValue)]:``
111 | ``def some_cb(client_address: Tuple[str, int], address: str, fixed_args: List[Any], *osc_args: List[Any]) -> Union[None, AnyStr, Tuple(str, ArgValue)]:``
112 |
113 | The callback function can return None, or a string representing an OSC address to be returned to the client,
114 | or a tuple that includes the address and ArgValue which will be converted to an OSC message and returned to
115 | the client.
116 |
117 | Args:
118 | address: Address to be mapped
119 | handler: Callback function that will be called as the handler for the given address
120 | *args: Fixed arguements that will be passed to the callback function
121 | needs_reply_address: Whether the IP address from which the message originated from shall be passed as
122 | an argument to the handler callback
123 |
124 | Returns:
125 | The handler object that will be invoked should the given address match
126 |
127 | """
128 | # TODO: Check the spec:
129 | # http://opensoundcontrol.org/spec-1_0
130 | # regarding multiple mappings
131 | handlerobj = Handler(handler, list(args), needs_reply_address)
132 | self._map[address].append(handlerobj)
133 | return handlerobj
134 |
135 | @overload
136 | def unmap(self, address: str, handler: Handler) -> None:
137 | """Remove an already mapped handler from an address
138 |
139 | Args:
140 | address (str): Address to be unmapped
141 | handler (Handler): A Handler object as returned from map().
142 | """
143 | pass
144 |
145 | @overload
146 | def unmap(
147 | self,
148 | address: str,
149 | handler: Callable,
150 | *args: Union[Any, List[Any]],
151 | needs_reply_address: bool = False,
152 | ) -> None:
153 | """Remove an already mapped handler from an address
154 |
155 | Args:
156 | address: Address to be unmapped
157 | handler: A function that will be run when the address matches with
158 | the OscMessage passed as parameter.
159 | args: Any additional arguments that will be always passed to the
160 | handlers after the osc messages arguments if any.
161 | needs_reply_address: True if the handler function needs the
162 | originating client address passed (as the first argument).
163 | """
164 | pass
165 |
166 | def unmap(self, address, handler, *args, needs_reply_address=False):
167 | try:
168 | if isinstance(handler, Handler):
169 | self._map[address].remove(handler)
170 | else:
171 | self._map[address].remove(
172 | Handler(handler, list(args), needs_reply_address)
173 | )
174 | except ValueError as e:
175 | if str(e) == "list.remove(x): x not in list":
176 | raise ValueError(
177 | f"Address '{address}' doesn't have handler '{handler}' mapped to it"
178 | ) from e
179 |
180 | def handlers_for_address(
181 | self, address_pattern: str
182 | ) -> Generator[Handler, None, None]:
183 | """Yields handlers matching an address
184 |
185 |
186 | Args:
187 | address_pattern: Address to match
188 |
189 | Returns:
190 | Generator yielding Handlers matching address_pattern
191 | """
192 | # First convert the address_pattern into a matchable regexp.
193 | # '?' in the OSC Address Pattern matches any single character.
194 | # Let's consider numbers and _ "characters" too here, it's not said
195 | # explicitly in the specification but it sounds good.
196 | escaped_address_pattern = re.escape(address_pattern)
197 | pattern = escaped_address_pattern.replace("\\?", "\\w?")
198 | # '*' in the OSC Address Pattern matches any sequence of zero or more
199 | # characters.
200 | pattern = pattern.replace("\\*", "[\\w|\\+]*")
201 | # The rest of the syntax in the specification is like the re module so
202 | # we're fine.
203 | pattern = f"{pattern}$"
204 | patterncompiled = re.compile(pattern)
205 | matched = False
206 |
207 | for addr, handlers in self._map.items():
208 | if patterncompiled.match(addr) or (
209 | ("*" in addr)
210 | and re.match(addr.replace("*", "[^/]*?/*"), address_pattern)
211 | ):
212 | yield from handlers
213 | matched = True
214 |
215 | if not matched and self._default_handler:
216 | logging.debug("No handler matched but default handler present, added it.")
217 | yield self._default_handler
218 |
219 | def call_handlers_for_packet(
220 | self, data: bytes, client_address: Tuple[str, int]
221 | ) -> List:
222 | """Invoke handlers for all messages in OSC packet
223 |
224 | The incoming OSC Packet is decoded and the handlers for each included message is found and invoked.
225 |
226 | Args:
227 | data: Data of packet
228 | client_address: Address of client this packet originated from
229 | Returns: A list of strings or tuples to be converted to OSC messages and returned to the client
230 | """
231 | results = list()
232 | # Get OSC messages from all bundles or standalone message.
233 | try:
234 | packet = osc_packet.OscPacket(data)
235 | for timed_msg in packet.messages:
236 | now = time.time()
237 | handlers = self.handlers_for_address(timed_msg.message.address)
238 | if not handlers:
239 | continue
240 | # If the message is to be handled later, then so be it.
241 | if timed_msg.time > now:
242 | time.sleep(timed_msg.time - now)
243 | for handler in handlers:
244 | result = handler.invoke(client_address, timed_msg.message)
245 | if result is not None:
246 | results.append(result)
247 | except osc_packet.ParseError:
248 | pass
249 | return results
250 |
251 | async def async_call_handlers_for_packet(
252 | self, data: bytes, client_address: Tuple[str, int]
253 | ) -> List:
254 | """
255 | This function calls the handlers registered to the dispatcher for
256 | every message it found in the packet.
257 | The process/thread granularity is thus the OSC packet, not the handler.
258 |
259 | If parameters were registered with the dispatcher, then the handlers are
260 | called this way:
261 | handler('/address that triggered the message',
262 | registered_param_list, osc_msg_arg1, osc_msg_arg2, ...)
263 | if no parameters were registered, then it is just called like this:
264 | handler('/address that triggered the message',
265 | osc_msg_arg1, osc_msg_arg2, osc_msg_param3, ...)
266 | """
267 |
268 | # Get OSC messages from all bundles or standalone message.
269 | results = []
270 | try:
271 | packet = osc_packet.OscPacket(data)
272 | for timed_msg in packet.messages:
273 | now = time.time()
274 | handlers = self.handlers_for_address(timed_msg.message.address)
275 | if not handlers:
276 | continue
277 | # If the message is to be handled later, then so be it.
278 | if timed_msg.time > now:
279 | time.sleep(timed_msg.time - now)
280 | for handler in handlers:
281 | if inspect.iscoroutinefunction(handler.callback):
282 | if handler.needs_reply_address:
283 | result = await handler.callback(
284 | client_address,
285 | timed_msg.message.address,
286 | handler.args,
287 | *timed_msg.message,
288 | )
289 | elif handler.args:
290 | result = await handler.callback(
291 | timed_msg.message.address,
292 | handler.args,
293 | *timed_msg.message,
294 | )
295 | else:
296 | result = await handler.callback(
297 | timed_msg.message.address, *timed_msg.message
298 | )
299 | else:
300 | if handler.needs_reply_address:
301 | result = handler.callback(
302 | client_address,
303 | timed_msg.message.address,
304 | handler.args,
305 | *timed_msg.message,
306 | )
307 | elif handler.args:
308 | result = handler.callback(
309 | timed_msg.message.address,
310 | handler.args,
311 | *timed_msg.message,
312 | )
313 | else:
314 | result = handler.callback(
315 | timed_msg.message.address, *timed_msg.message
316 | )
317 | if result:
318 | results.append(result)
319 | except osc_packet.ParseError as e:
320 | pass
321 | return results
322 |
323 | def set_default_handler(
324 | self, handler: Callable, needs_reply_address: bool = False
325 | ) -> None:
326 | """Sets the default handler
327 |
328 | The default handler is invoked every time no other handler is mapped to an address.
329 |
330 | Args:
331 | handler: Callback function to handle unmapped requests
332 | needs_reply_address: Whether the callback shall be passed the client address
333 | """
334 | self._default_handler = (
335 | None if (handler is None) else Handler(handler, [], needs_reply_address)
336 | )
337 |
--------------------------------------------------------------------------------
/pythonosc/test/test_osc_tcp_server.py:
--------------------------------------------------------------------------------
1 | import struct
2 | import unittest
3 | import unittest.mock as mock
4 |
5 | from pythonosc import dispatcher, osc_tcp_server
6 | from pythonosc.slip import END
7 |
8 | _SIMPLE_PARAM_INT_MSG = b"/SYNC\x00\x00\x00" b",i\x00\x00" b"\x00\x00\x00\x04"
9 |
10 | LEN_SIMPLE_PARAM_INT_MSG = struct.pack("!I", len(_SIMPLE_PARAM_INT_MSG))
11 | _SIMPLE_PARAM_INT_MSG_1_1 = END + _SIMPLE_PARAM_INT_MSG + END
12 |
13 | # Regression test for a datagram that should NOT be stripped, ever...
14 | _SIMPLE_PARAM_INT_9 = b"/debug\x00\x00,i\x00\x00\x00\x00\x00\t"
15 | LEN_SIMPLE_PARAM_INT_9 = struct.pack("!I", len(_SIMPLE_PARAM_INT_9))
16 |
17 | _SIMPLE_PARAM_INT_9_1_1 = END + _SIMPLE_PARAM_INT_9 + END
18 |
19 | _SIMPLE_MSG_NO_PARAMS = b"/SYNC\x00\x00\x00"
20 | LEN_SIMPLE_MSG_NO_PARAMS = struct.pack("!I", len(_SIMPLE_MSG_NO_PARAMS))
21 | _SIMPLE_MSG_NO_PARAMS_1_1 = END + _SIMPLE_MSG_NO_PARAMS + END
22 |
23 |
24 | class TestTCP_1_1_Handler(unittest.TestCase):
25 | def setUp(self):
26 | super().setUp()
27 | self.dispatcher = dispatcher.Dispatcher()
28 | # We do not want to create real UDP connections during unit tests.
29 | self.server = unittest.mock.Mock(spec=osc_tcp_server.BlockingOSCTCPServer)
30 | # Need to attach property mocks to types, not objects... weird.
31 | type(self.server).dispatcher = unittest.mock.PropertyMock(
32 | return_value=self.dispatcher
33 | )
34 | self.client_address = ("127.0.0.1", 8080)
35 | self.mock_meth = unittest.mock.MagicMock()
36 | self.mock_meth.return_value = None
37 |
38 | def test_no_match(self):
39 | self.dispatcher.map("/foobar", self.mock_meth)
40 | mock_sock = mock.Mock()
41 | mock_sock.recv = mock.Mock()
42 | mock_sock.recv.side_effect = [
43 | _SIMPLE_MSG_NO_PARAMS_1_1,
44 | _SIMPLE_PARAM_INT_MSG_1_1,
45 | b"",
46 | ]
47 | osc_tcp_server._TCPHandler1_1(mock_sock, self.client_address, self.server)
48 | self.assertFalse(self.mock_meth.called)
49 |
50 | def test_match_with_args(self):
51 | self.dispatcher.map("/SYNC", self.mock_meth, 1, 2, 3)
52 | mock_sock = mock.Mock()
53 | mock_sock.recv = mock.Mock()
54 | mock_sock.recv.side_effect = [_SIMPLE_PARAM_INT_MSG_1_1, b""]
55 | osc_tcp_server._TCPHandler1_1(mock_sock, self.client_address, self.server)
56 | self.mock_meth.assert_called_with("/SYNC", [1, 2, 3], 4)
57 |
58 | def test_match_int9(self):
59 | self.dispatcher.map("/debug", self.mock_meth)
60 | mock_sock = mock.Mock()
61 | mock_sock.recv = mock.Mock()
62 | mock_sock.recv.side_effect = [_SIMPLE_PARAM_INT_9_1_1, b""]
63 | osc_tcp_server._TCPHandler1_1(mock_sock, self.client_address, self.server)
64 | self.assertTrue(self.mock_meth.called)
65 | self.mock_meth.assert_called_with("/debug", 9)
66 |
67 | def test_match_without_args(self):
68 | self.dispatcher.map("/SYNC", self.mock_meth)
69 | mock_sock = mock.Mock()
70 | mock_sock.recv = mock.Mock()
71 | mock_sock.recv.side_effect = [_SIMPLE_MSG_NO_PARAMS_1_1, b""]
72 | osc_tcp_server._TCPHandler1_1(mock_sock, self.client_address, self.server)
73 | self.mock_meth.assert_called_with("/SYNC")
74 |
75 | def test_match_default_handler(self):
76 | self.dispatcher.set_default_handler(self.mock_meth)
77 | mock_sock = mock.Mock()
78 | mock_sock.recv = mock.Mock()
79 | mock_sock.recv.side_effect = [_SIMPLE_MSG_NO_PARAMS_1_1, b""]
80 | osc_tcp_server._TCPHandler1_1(mock_sock, self.client_address, self.server)
81 | self.mock_meth.assert_called_with("/SYNC")
82 |
83 | def test_response_no_args(self):
84 | def respond(*args, **kwargs):
85 | return "/SYNC"
86 |
87 | self.dispatcher.map("/SYNC", respond)
88 | mock_sock = mock.Mock()
89 | mock_sock.recv = mock.Mock()
90 | mock_sock.recv.side_effect = [_SIMPLE_MSG_NO_PARAMS_1_1, b""]
91 | mock_sock.sendall = mock.Mock()
92 | mock_sock.sendall.return_value = None
93 | osc_tcp_server._TCPHandler1_1(mock_sock, self.client_address, self.server)
94 | mock_sock.sendall.assert_called_with(b"\xc0/SYNC\00\00\00,\00\00\00\xc0")
95 |
96 | def test_response_with_args(self):
97 | def respond(*args, **kwargs):
98 | return (
99 | "/SYNC",
100 | 1,
101 | "2",
102 | 3.0,
103 | )
104 |
105 | self.dispatcher.map("/SYNC", respond)
106 | mock_sock = mock.Mock()
107 | mock_sock.recv = mock.Mock()
108 | mock_sock.recv.side_effect = [_SIMPLE_MSG_NO_PARAMS_1_1, b""]
109 | mock_sock.sendall = mock.Mock()
110 | mock_sock.sendall.return_value = None
111 | osc_tcp_server._TCPHandler1_1(mock_sock, self.client_address, self.server)
112 | mock_sock.sendall.assert_called_with(
113 | b"\xc0/SYNC\00\00\00,isf\x00\x00\x00\x00\x00\x00\x00\x012\x00\x00\x00@@\x00\x00\xc0"
114 | )
115 |
116 |
117 | class TestTCP_1_0_Handler(unittest.TestCase):
118 | def setUp(self):
119 | super().setUp()
120 | self.dispatcher = dispatcher.Dispatcher()
121 | # We do not want to create real UDP connections during unit tests.
122 | self.server = unittest.mock.Mock(spec=osc_tcp_server.BlockingOSCTCPServer)
123 | # Need to attach property mocks to types, not objects... weird.
124 | type(self.server).dispatcher = unittest.mock.PropertyMock(
125 | return_value=self.dispatcher
126 | )
127 | self.client_address = ("127.0.0.1", 8080)
128 | self.mock_meth = unittest.mock.MagicMock()
129 | self.mock_meth.return_value = None
130 |
131 | def test_no_match(self):
132 | self.dispatcher.map("/foobar", self.mock_meth)
133 | mock_sock = mock.Mock()
134 | mock_sock.recv = mock.Mock()
135 | mock_sock.recv.side_effect = [
136 | LEN_SIMPLE_MSG_NO_PARAMS,
137 | _SIMPLE_MSG_NO_PARAMS,
138 | LEN_SIMPLE_PARAM_INT_MSG,
139 | _SIMPLE_PARAM_INT_MSG,
140 | b"",
141 | ]
142 | osc_tcp_server._TCPHandler1_0(mock_sock, self.client_address, self.server)
143 | self.assertFalse(self.mock_meth.called)
144 |
145 | def test_match_with_args(self):
146 | self.dispatcher.map("/SYNC", self.mock_meth, 1, 2, 3)
147 | mock_sock = mock.Mock()
148 | mock_sock.recv = mock.Mock()
149 | mock_sock.recv.side_effect = [
150 | LEN_SIMPLE_PARAM_INT_MSG,
151 | _SIMPLE_PARAM_INT_MSG,
152 | b"",
153 | ]
154 | osc_tcp_server._TCPHandler1_0(mock_sock, self.client_address, self.server)
155 | self.mock_meth.assert_called_with("/SYNC", [1, 2, 3], 4)
156 |
157 | def test_match_int9(self):
158 | self.dispatcher.map("/debug", self.mock_meth)
159 | mock_sock = mock.Mock()
160 | mock_sock.recv = mock.Mock()
161 | mock_sock.recv.side_effect = [LEN_SIMPLE_PARAM_INT_9, _SIMPLE_PARAM_INT_9, b""]
162 | osc_tcp_server._TCPHandler1_0(mock_sock, self.client_address, self.server)
163 | self.assertTrue(self.mock_meth.called)
164 | self.mock_meth.assert_called_with("/debug", 9)
165 |
166 | def test_match_without_args(self):
167 | self.dispatcher.map("/SYNC", self.mock_meth)
168 | mock_sock = mock.Mock()
169 | mock_sock.recv = mock.Mock()
170 | mock_sock.recv.side_effect = [
171 | LEN_SIMPLE_MSG_NO_PARAMS,
172 | _SIMPLE_MSG_NO_PARAMS,
173 | b"",
174 | ]
175 | osc_tcp_server._TCPHandler1_0(mock_sock, self.client_address, self.server)
176 | self.mock_meth.assert_called_with("/SYNC")
177 |
178 | def test_match_default_handler(self):
179 | self.dispatcher.set_default_handler(self.mock_meth)
180 | mock_sock = mock.Mock()
181 | mock_sock.recv = mock.Mock()
182 | mock_sock.recv.side_effect = [
183 | LEN_SIMPLE_MSG_NO_PARAMS,
184 | _SIMPLE_MSG_NO_PARAMS,
185 | b"",
186 | ]
187 | osc_tcp_server._TCPHandler1_0(mock_sock, self.client_address, self.server)
188 | self.mock_meth.assert_called_with("/SYNC")
189 |
190 | def test_response_no_args(self):
191 | def respond(*args, **kwargs):
192 | return "/SYNC"
193 |
194 | self.dispatcher.map("/SYNC", respond)
195 | mock_sock = mock.Mock()
196 | mock_sock.recv = mock.Mock()
197 | mock_sock.recv.side_effect = [
198 | LEN_SIMPLE_MSG_NO_PARAMS,
199 | _SIMPLE_MSG_NO_PARAMS,
200 | b"",
201 | ]
202 | mock_sock.sendall = mock.Mock()
203 | mock_sock.sendall.return_value = None
204 | osc_tcp_server._TCPHandler1_0(mock_sock, self.client_address, self.server)
205 | mock_sock.sendall.assert_called_with(
206 | b"\x00\x00\x00\x0c/SYNC\00\00\00,\00\00\00"
207 | )
208 |
209 | def test_response_with_args(self):
210 | def respond(*args, **kwargs):
211 | return (
212 | "/SYNC",
213 | 1,
214 | "2",
215 | 3.0,
216 | )
217 |
218 | self.dispatcher.map("/SYNC", respond)
219 | mock_sock = mock.Mock()
220 | mock_sock.recv = mock.Mock()
221 | mock_sock.recv.side_effect = [
222 | LEN_SIMPLE_MSG_NO_PARAMS,
223 | _SIMPLE_MSG_NO_PARAMS,
224 | b"",
225 | ]
226 | mock_sock.sendall = mock.Mock()
227 | mock_sock.sendall.return_value = None
228 | osc_tcp_server._TCPHandler1_0(mock_sock, self.client_address, self.server)
229 | mock_sock.sendall.assert_called_with(
230 | b"\x00\x00\x00\x1c/SYNC\00\00\00,isf\x00\x00\x00\x00\x00\x00\x00\x012\x00\x00\x00@@\x00\x00"
231 | )
232 |
233 |
234 | class TestAsync1_1Handler(unittest.IsolatedAsyncioTestCase):
235 | def setUp(self):
236 | super().setUp()
237 | self.dispatcher = dispatcher.Dispatcher()
238 | # We do not want to create real UDP connections during unit tests.
239 | self.server = unittest.mock.Mock(spec=osc_tcp_server.BlockingOSCTCPServer)
240 | # Need to attach property mocks to types, not objects... weird.
241 | type(self.server).dispatcher = unittest.mock.PropertyMock(
242 | return_value=self.dispatcher
243 | )
244 | self.client_address = ("127.0.0.1", 8080)
245 | self.mock_writer = mock.Mock()
246 | self.mock_writer.close = mock.Mock()
247 | self.mock_writer.write = mock.Mock()
248 | self.mock_writer.write_eof = mock.Mock()
249 | self.mock_writer.drain = mock.AsyncMock()
250 | self.mock_writer.wait_closed = mock.AsyncMock()
251 | self.mock_reader = mock.Mock()
252 | self.mock_reader.read = mock.AsyncMock()
253 | self.server = osc_tcp_server.AsyncOSCTCPServer(
254 | "127.0.0.1", 8008, self.dispatcher
255 | )
256 | self.mock_meth = unittest.mock.MagicMock()
257 | self.mock_meth.return_value = None
258 |
259 | async def test_no_match(self):
260 | self.dispatcher.map("/foobar", self.mock_meth)
261 | self.mock_reader.read.side_effect = [
262 | _SIMPLE_MSG_NO_PARAMS_1_1,
263 | _SIMPLE_PARAM_INT_MSG_1_1,
264 | b"",
265 | ]
266 | await osc_tcp_server.AsyncOSCTCPServer.handle(
267 | self.server, self.mock_reader, self.mock_writer
268 | )
269 | self.assertFalse(self.mock_meth.called)
270 |
271 | async def test_match_with_args(self):
272 | self.dispatcher.map("/SYNC", self.mock_meth, 1, 2, 3)
273 | self.mock_reader.read.side_effect = [_SIMPLE_PARAM_INT_MSG_1_1, b""]
274 | await osc_tcp_server.AsyncOSCTCPServer.handle(
275 | self.server, self.mock_reader, self.mock_writer
276 | )
277 | self.mock_meth.assert_called_with("/SYNC", [1, 2, 3], 4)
278 |
279 | async def test_match_int9(self):
280 | self.dispatcher.map("/debug", self.mock_meth)
281 | self.mock_reader.read.side_effect = [_SIMPLE_PARAM_INT_9_1_1, b""]
282 | await osc_tcp_server.AsyncOSCTCPServer.handle(
283 | self.server, self.mock_reader, self.mock_writer
284 | )
285 | self.assertTrue(self.mock_meth.called)
286 | self.mock_meth.assert_called_with("/debug", 9)
287 |
288 | async def test_match_without_args(self):
289 | self.dispatcher.map("/SYNC", self.mock_meth)
290 | self.mock_reader.read.side_effect = [_SIMPLE_MSG_NO_PARAMS_1_1, b""]
291 | await osc_tcp_server.AsyncOSCTCPServer.handle(
292 | self.server, self.mock_reader, self.mock_writer
293 | )
294 | self.mock_meth.assert_called_with("/SYNC")
295 |
296 | async def test_match_default_handler(self):
297 | self.dispatcher.set_default_handler(self.mock_meth)
298 | self.mock_reader.read.side_effect = [_SIMPLE_MSG_NO_PARAMS_1_1, b""]
299 | await osc_tcp_server.AsyncOSCTCPServer.handle(
300 | self.server, self.mock_reader, self.mock_writer
301 | )
302 | self.mock_meth.assert_called_with("/SYNC")
303 |
304 | async def test_response_no_args(self):
305 | def respond(*args, **kwargs):
306 | return "/SYNC"
307 |
308 | self.dispatcher.map("/SYNC", respond)
309 | self.mock_reader.read.side_effect = [_SIMPLE_MSG_NO_PARAMS_1_1, b""]
310 | await osc_tcp_server.AsyncOSCTCPServer.handle(
311 | self.server, self.mock_reader, self.mock_writer
312 | )
313 | self.mock_writer.write.assert_called_with(b"\xc0/SYNC\00\00\00,\00\00\00\xc0")
314 |
315 | async def test_response_with_args(self):
316 | def respond(*args, **kwargs):
317 | return (
318 | "/SYNC",
319 | 1,
320 | "2",
321 | 3.0,
322 | )
323 |
324 | self.dispatcher.map("/SYNC", respond)
325 | self.mock_reader.read.side_effect = [_SIMPLE_MSG_NO_PARAMS_1_1, b""]
326 | await osc_tcp_server.AsyncOSCTCPServer.handle(
327 | self.server, self.mock_reader, self.mock_writer
328 | )
329 | self.mock_writer.write.assert_called_with(
330 | b"\xc0/SYNC\00\00\00,isf\x00\x00\x00\x00\x00\x00\x00\x012\x00\x00\x00@@\x00\x00\xc0"
331 | )
332 |
333 | async def test_async_response_with_args(self):
334 | async def respond(*args, **kwargs):
335 | return (
336 | "/SYNC",
337 | 1,
338 | "2",
339 | 3.0,
340 | )
341 |
342 | self.dispatcher.map("/SYNC", respond)
343 | self.mock_reader.read.side_effect = [_SIMPLE_MSG_NO_PARAMS_1_1, b""]
344 | await osc_tcp_server.AsyncOSCTCPServer.handle(
345 | self.server, self.mock_reader, self.mock_writer
346 | )
347 | self.mock_writer.write.assert_called_with(
348 | b"\xc0/SYNC\00\00\00,isf\x00\x00\x00\x00\x00\x00\x00\x012\x00\x00\x00@@\x00\x00\xc0"
349 | )
350 |
351 |
352 | if __name__ == "__main__":
353 | unittest.main()
354 |
--------------------------------------------------------------------------------
/pythonosc/parsing/osc_types.py:
--------------------------------------------------------------------------------
1 | """Functions to get OSC types from datagrams and vice versa"""
2 |
3 | import struct
4 |
5 | from pythonosc.parsing import ntp
6 | from datetime import datetime, timedelta
7 |
8 | from typing import Union, Tuple, cast
9 |
10 | MidiPacket = Tuple[int, int, int, int]
11 |
12 |
13 | class ParseError(Exception):
14 | """Base exception for when a datagram parsing error occurs."""
15 |
16 |
17 | class BuildError(Exception):
18 | """Base exception for when a datagram building error occurs."""
19 |
20 |
21 | # Constant for special ntp datagram sequences that represent an immediate time.
22 | IMMEDIATELY = 0
23 |
24 | # Datagram length in bytes for types that have a fixed size.
25 | _INT_DGRAM_LEN = 4
26 | _INT64_DGRAM_LEN = 8
27 | _UINT64_DGRAM_LEN = 8
28 | _FLOAT_DGRAM_LEN = 4
29 | _DOUBLE_DGRAM_LEN = 8
30 | _TIMETAG_DGRAM_LEN = 8
31 | # Strings and blob dgram length is always a multiple of 4 bytes.
32 | _STRING_DGRAM_PAD = 4
33 | _BLOB_DGRAM_PAD = 4
34 | _EMPTY_STR_DGRAM = b"\x00\x00\x00\x00"
35 |
36 |
37 | def write_string(val: str) -> bytes:
38 | """Returns the OSC string equivalent of the given python string.
39 |
40 | Raises:
41 | - BuildError if the string could not be encoded.
42 | """
43 | try:
44 | dgram = val.encode("utf-8") # Default, but better be explicit.
45 | except (UnicodeEncodeError, AttributeError) as e:
46 | raise BuildError(f"Incorrect string, could not encode {e}")
47 | diff = _STRING_DGRAM_PAD - (len(dgram) % _STRING_DGRAM_PAD)
48 | dgram += b"\x00" * diff
49 | return dgram
50 |
51 |
52 | def get_string(dgram: bytes, start_index: int) -> Tuple[str, int]:
53 | """Get a python string from the datagram, starting at pos start_index.
54 |
55 | According to the specifications, a string is:
56 | "A sequence of non-null ASCII characters followed by a null,
57 | followed by 0-3 additional null characters to make the total number
58 | of bits a multiple of 32".
59 |
60 | Args:
61 | dgram: A datagram packet.
62 | start_index: An index where the string starts in the datagram.
63 |
64 | Returns:
65 | A tuple containing the string and the new end index.
66 |
67 | Raises:
68 | ParseError if the datagram could not be parsed.
69 | """
70 | if start_index < 0:
71 | raise ParseError("start_index < 0")
72 | offset = 0
73 | try:
74 | if (
75 | len(dgram) > start_index + _STRING_DGRAM_PAD
76 | and dgram[start_index + _STRING_DGRAM_PAD] == _EMPTY_STR_DGRAM
77 | ):
78 | return "", start_index + _STRING_DGRAM_PAD
79 | while dgram[start_index + offset] != 0:
80 | offset += 1
81 | # Align to a byte word.
82 | if (offset) % _STRING_DGRAM_PAD == 0:
83 | offset += _STRING_DGRAM_PAD
84 | else:
85 | offset += -offset % _STRING_DGRAM_PAD
86 | # Python slices do not raise an IndexError past the last index,
87 | # do it ourselves.
88 | if offset > len(dgram[start_index:]):
89 | raise ParseError("Datagram is too short")
90 | data_str = dgram[start_index : start_index + offset]
91 | return data_str.replace(b"\x00", b"").decode("utf-8"), start_index + offset
92 | except IndexError as ie:
93 | raise ParseError(f"Could not parse datagram {ie}")
94 | except TypeError as te:
95 | raise ParseError(f"Could not parse datagram {te}")
96 |
97 |
98 | def write_int(val: int) -> bytes:
99 | """Returns the datagram for the given integer parameter value
100 |
101 | Raises:
102 | - BuildError if the int could not be converted.
103 | """
104 | try:
105 | return struct.pack(">i", val)
106 | except struct.error as e:
107 | raise BuildError(f"Wrong argument value passed: {e}")
108 |
109 |
110 | def get_int(dgram: bytes, start_index: int) -> Tuple[int, int]:
111 | """Get a 32-bit big-endian two's complement integer from the datagram.
112 |
113 | Args:
114 | dgram: A datagram packet.
115 | start_index: An index where the integer starts in the datagram.
116 |
117 | Returns:
118 | A tuple containing the integer and the new end index.
119 |
120 | Raises:
121 | ParseError if the datagram could not be parsed.
122 | """
123 | try:
124 | if len(dgram[start_index:]) < _INT_DGRAM_LEN:
125 | raise ParseError("Datagram is too short")
126 | return (
127 | struct.unpack(">i", dgram[start_index : start_index + _INT_DGRAM_LEN])[0],
128 | start_index + _INT_DGRAM_LEN,
129 | )
130 | except (struct.error, TypeError) as e:
131 | raise ParseError(f"Could not parse datagram {e}")
132 |
133 |
134 | def write_int64(val: int) -> bytes:
135 | """Returns the datagram for the given 64-bit big-endian signed parameter value
136 |
137 | Raises:
138 | - BuildError if the int64 could not be converted.
139 | """
140 | try:
141 | return struct.pack(">q", val)
142 | except struct.error as e:
143 | raise BuildError(f"Wrong argument value passed: {e}")
144 |
145 |
146 | def get_int64(dgram: bytes, start_index: int) -> Tuple[int, int]:
147 | """Get a 64-bit big-endian signed integer from the datagram.
148 |
149 | Args:
150 | dgram: A datagram packet.
151 | start_index: An index where the 64-bit integer starts in the datagram.
152 |
153 | Returns:
154 | A tuple containing the 64-bit integer and the new end index.
155 |
156 | Raises:
157 | ParseError if the datagram could not be parsed.
158 | """
159 | try:
160 | if len(dgram[start_index:]) < _INT64_DGRAM_LEN:
161 | raise ParseError("Datagram is too short")
162 | return (
163 | struct.unpack(">q", dgram[start_index : start_index + _INT64_DGRAM_LEN])[0],
164 | start_index + _INT64_DGRAM_LEN,
165 | )
166 | except (struct.error, TypeError) as e:
167 | raise ParseError(f"Could not parse datagram {e}")
168 |
169 |
170 | def get_uint64(dgram: bytes, start_index: int) -> Tuple[int, int]:
171 | """Get a 64-bit big-endian unsigned integer from the datagram.
172 |
173 | Args:
174 | dgram: A datagram packet.
175 | start_index: An index where the integer starts in the datagram.
176 |
177 | Returns:
178 | A tuple containing the integer and the new end index.
179 |
180 | Raises:
181 | ParseError if the datagram could not be parsed.
182 | """
183 | try:
184 | if len(dgram[start_index:]) < _UINT64_DGRAM_LEN:
185 | raise ParseError("Datagram is too short")
186 | return (
187 | struct.unpack(">Q", dgram[start_index : start_index + _UINT64_DGRAM_LEN])[
188 | 0
189 | ],
190 | start_index + _UINT64_DGRAM_LEN,
191 | )
192 | except (struct.error, TypeError) as e:
193 | raise ParseError(f"Could not parse datagram {e}")
194 |
195 |
196 | def get_timetag(dgram: bytes, start_index: int) -> Tuple[Tuple[datetime, int], int]:
197 | """Get a 64-bit OSC time tag from the datagram.
198 |
199 | Args:
200 | dgram: A datagram packet.
201 | start_index: An index where the osc time tag starts in the datagram.
202 |
203 | Returns:
204 | A tuple containing the tuple of time of sending in utc as datetime and the
205 | fraction of the current second and the new end index.
206 |
207 | Raises:
208 | ParseError if the datagram could not be parsed.
209 | """
210 | try:
211 | if len(dgram[start_index:]) < _TIMETAG_DGRAM_LEN:
212 | raise ParseError("Datagram is too short")
213 |
214 | timetag, _ = get_uint64(dgram, start_index)
215 | seconds, fraction = ntp.parse_timestamp(timetag)
216 |
217 | hours, seconds = seconds // 3600, seconds % 3600
218 | minutes, seconds = seconds // 60, seconds % 60
219 |
220 | utc = datetime.combine(ntp._NTP_EPOCH, datetime.min.time()) + timedelta(
221 | hours=hours, minutes=minutes, seconds=seconds
222 | )
223 |
224 | return (utc, fraction), start_index + _TIMETAG_DGRAM_LEN
225 | except (struct.error, TypeError) as e:
226 | raise ParseError(f"Could not parse datagram {e}")
227 |
228 |
229 | def write_float(val: float) -> bytes:
230 | """Returns the datagram for the given float parameter value
231 |
232 | Raises:
233 | - BuildError if the float could not be converted.
234 | """
235 | try:
236 | return struct.pack(">f", val)
237 | except struct.error as e:
238 | raise BuildError(f"Wrong argument value passed: {e}")
239 |
240 |
241 | def get_float(dgram: bytes, start_index: int) -> Tuple[float, int]:
242 | """Get a 32-bit big-endian IEEE 754 floating point number from the datagram.
243 |
244 | Args:
245 | dgram: A datagram packet.
246 | start_index: An index where the float starts in the datagram.
247 |
248 | Returns:
249 | A tuple containing the float and the new end index.
250 |
251 | Raises:
252 | ParseError if the datagram could not be parsed.
253 | """
254 | try:
255 | if len(dgram[start_index:]) < _FLOAT_DGRAM_LEN:
256 | # Noticed that Reaktor doesn't send the last bunch of \x00 needed to make
257 | # the float representation complete in some cases, thus we pad here to
258 | # account for that.
259 | dgram = dgram + b"\x00" * (_FLOAT_DGRAM_LEN - len(dgram[start_index:]))
260 | return (
261 | struct.unpack(">f", dgram[start_index : start_index + _FLOAT_DGRAM_LEN])[0],
262 | start_index + _FLOAT_DGRAM_LEN,
263 | )
264 | except (struct.error, TypeError) as e:
265 | raise ParseError(f"Could not parse datagram {e}")
266 |
267 |
268 | def write_double(val: float) -> bytes:
269 | """Returns the datagram for the given double parameter value
270 |
271 | Raises:
272 | - BuildError if the double could not be converted.
273 | """
274 | try:
275 | return struct.pack(">d", val)
276 | except struct.error as e:
277 | raise BuildError(f"Wrong argument value passed: {e}")
278 |
279 |
280 | def get_double(dgram: bytes, start_index: int) -> Tuple[float, int]:
281 | """Get a 64-bit big-endian IEEE 754 floating point number from the datagram.
282 |
283 | Args:
284 | dgram: A datagram packet.
285 | start_index: An index where the double starts in the datagram.
286 |
287 | Returns:
288 | A tuple containing the double and the new end index.
289 |
290 | Raises:
291 | ParseError if the datagram could not be parsed.
292 | """
293 | try:
294 | if len(dgram[start_index:]) < _DOUBLE_DGRAM_LEN:
295 | raise ParseError("Datagram is too short")
296 | return (
297 | struct.unpack(">d", dgram[start_index : start_index + _DOUBLE_DGRAM_LEN])[
298 | 0
299 | ],
300 | start_index + _DOUBLE_DGRAM_LEN,
301 | )
302 | except (struct.error, TypeError) as e:
303 | raise ParseError(f"Could not parse datagram {e}")
304 |
305 |
306 | def get_blob(dgram: bytes, start_index: int) -> Tuple[bytes, int]:
307 | """Get a blob from the datagram.
308 |
309 | According to the specifications, a blob is made of
310 | "an int32 size count, followed by that many 8-bit bytes of arbitrary
311 | binary data, followed by 0-3 additional zero bytes to make the total
312 | number of bits a multiple of 32".
313 |
314 | Args:
315 | dgram: A datagram packet.
316 | start_index: An index where the float starts in the datagram.
317 |
318 | Returns:
319 | A tuple containing the blob and the new end index.
320 |
321 | Raises:
322 | ParseError if the datagram could not be parsed.
323 | """
324 | size, int_offset = get_int(dgram, start_index)
325 | # Make the size a multiple of 32 bits.
326 | total_size = size + (-size % _BLOB_DGRAM_PAD)
327 | end_index = int_offset + size
328 | if end_index - start_index > len(dgram[start_index:]):
329 | raise ParseError("Datagram is too short.")
330 | return dgram[int_offset : int_offset + size], int_offset + total_size
331 |
332 |
333 | def write_blob(val: bytes) -> bytes:
334 | """Returns the datagram for the given blob parameter value.
335 |
336 | Raises:
337 | - BuildError if the value was empty or if its size didn't fit an OSC int.
338 | """
339 | if not val:
340 | raise BuildError("Blob value cannot be empty")
341 | dgram = write_int(len(val))
342 | dgram += val
343 | while len(dgram) % _BLOB_DGRAM_PAD != 0:
344 | dgram += b"\x00"
345 | return dgram
346 |
347 |
348 | def get_date(dgram: bytes, start_index: int) -> Tuple[float, int]:
349 | """Get a 64-bit big-endian fixed-point time tag as a date from the datagram.
350 |
351 | According to the specifications, a date is represented as is:
352 | "the first 32 bits specify the number of seconds since midnight on
353 | January 1, 1900, and the last 32 bits specify fractional parts of a second
354 | to a precision of about 200 picoseconds".
355 |
356 | Args:
357 | dgram: A datagram packet.
358 | start_index: An index where the date starts in the datagram.
359 |
360 | Returns:
361 | A tuple containing the system date and the new end index.
362 | returns osc_immediately (0) if the corresponding OSC sequence was found.
363 |
364 | Raises:
365 | ParseError if the datagram could not be parsed.
366 | """
367 | # Check for the special case first.
368 | if dgram[start_index : start_index + _TIMETAG_DGRAM_LEN] == ntp.IMMEDIATELY:
369 | return IMMEDIATELY, start_index + _TIMETAG_DGRAM_LEN
370 | if len(dgram[start_index:]) < _TIMETAG_DGRAM_LEN:
371 | raise ParseError("Datagram is too short")
372 | timetag, start_index = get_uint64(dgram, start_index)
373 | seconds = timetag * ntp._NTP_TIMESTAMP_TO_SECONDS
374 | return ntp.ntp_time_to_system_epoch(seconds), start_index
375 |
376 |
377 | def write_date(system_time: Union[int, float]) -> bytes:
378 | if system_time == IMMEDIATELY:
379 | return ntp.IMMEDIATELY
380 |
381 | try:
382 | return ntp.system_time_to_ntp(system_time)
383 | except ntp.NtpError as ntpe:
384 | raise BuildError(ntpe)
385 |
386 |
387 | def write_rgba(val: bytes) -> bytes:
388 | """Returns the datagram for the given rgba32 parameter value
389 |
390 | Raises:
391 | - BuildError if the int could not be converted.
392 | """
393 | try:
394 | return struct.pack(">I", val)
395 | except struct.error as e:
396 | raise BuildError(f"Wrong argument value passed: {e}")
397 |
398 |
399 | def get_rgba(dgram: bytes, start_index: int) -> Tuple[bytes, int]:
400 | """Get an rgba32 integer from the datagram.
401 |
402 | Args:
403 | dgram: A datagram packet.
404 | start_index: An index where the integer starts in the datagram.
405 |
406 | Returns:
407 | A tuple containing the integer and the new end index.
408 |
409 | Raises:
410 | ParseError if the datagram could not be parsed.
411 | """
412 | try:
413 | if len(dgram[start_index:]) < _INT_DGRAM_LEN:
414 | raise ParseError("Datagram is too short")
415 | return (
416 | struct.unpack(">I", dgram[start_index : start_index + _INT_DGRAM_LEN])[0],
417 | start_index + _INT_DGRAM_LEN,
418 | )
419 | except (struct.error, TypeError) as e:
420 | raise ParseError(f"Could not parse datagram {e}")
421 |
422 |
423 | def write_midi(val: MidiPacket) -> bytes:
424 | """Returns the datagram for the given MIDI message parameter value
425 |
426 | A valid MIDI message: (port id, status byte, data1, data2).
427 |
428 | Raises:
429 | - BuildError if the MIDI message could not be converted.
430 |
431 | """
432 | if len(val) != 4:
433 | raise BuildError("MIDI message length is invalid")
434 | try:
435 | value = sum((value & 0xFF) << 8 * (3 - pos) for pos, value in enumerate(val))
436 | return struct.pack(">I", value)
437 | except struct.error as e:
438 | raise BuildError(f"Wrong argument value passed: {e}")
439 |
440 |
441 | def get_midi(dgram: bytes, start_index: int) -> Tuple[MidiPacket, int]:
442 | """Get a MIDI message (port id, status byte, data1, data2) from the datagram.
443 |
444 | Args:
445 | dgram: A datagram packet.
446 | start_index: An index where the MIDI message starts in the datagram.
447 |
448 | Returns:
449 | A tuple containing the MIDI message and the new end index.
450 |
451 | Raises:
452 | ParseError if the datagram could not be parsed.
453 | """
454 | try:
455 | if len(dgram[start_index:]) < _INT_DGRAM_LEN:
456 | raise ParseError("Datagram is too short")
457 | val = struct.unpack(">I", dgram[start_index : start_index + _INT_DGRAM_LEN])[0]
458 | midi_msg = cast(
459 | MidiPacket, tuple((val & 0xFF << 8 * i) >> 8 * i for i in range(3, -1, -1))
460 | )
461 | return (midi_msg, start_index + _INT_DGRAM_LEN)
462 | except (struct.error, TypeError) as e:
463 | raise ParseError(f"Could not parse datagram {e}")
464 |
--------------------------------------------------------------------------------