├── .gitignore
├── .vscode
└── launch.json
├── LICENSE
├── README.md
├── __init__.py
├── help
├── TestBed.blend
├── TestBed.maxpat
├── Tutorial.blend
├── Tutorial.maxpat
└── Tutorial.touchosc
├── nodes
├── AN
│ ├── __init__.py
│ ├── auto_load.py
│ ├── nodes
│ │ ├── OSCListNode.py
│ │ ├── OSCNumberNode.py
│ │ └── __init__.py
│ └── ui
│ │ ├── __init__.py
│ │ └── extend_menu.py
├── nodes.py
└── sorcar
│ └── nodes
│ ├── _base
│ └── node_base.py
│ └── osc
│ ├── ScOSCNumber.py
│ ├── ScOSCString.py
│ └── ScOSCVector.py
├── preferences.py
├── server
├── _base.py
├── callbacks.py
├── operators.py
├── oscpy
│ ├── AUTHORS.txt
│ ├── CHANGELOG
│ ├── LICENSE.txt
│ ├── README.md
│ ├── __init__.py
│ ├── cli.py
│ ├── client.py
│ ├── parser.py
│ ├── server.py
│ └── stats.py
├── pythonosc
│ ├── __init__.py
│ ├── dispatcher.py
│ ├── osc_bundle.py
│ ├── osc_bundle_builder.py
│ ├── osc_message.py
│ ├── osc_message_builder.py
│ ├── osc_packet.py
│ ├── osc_server.py
│ ├── parsing
│ │ ├── __init__.py
│ │ ├── ntp.py
│ │ └── osc_types.py
│ ├── test
│ │ ├── __init__.py
│ │ ├── parsing
│ │ │ ├── __init__.py
│ │ │ ├── test_ntp.py
│ │ │ └── test_osc_types.py
│ │ ├── test_dispatcher.py
│ │ ├── test_osc_bundle.py
│ │ ├── test_osc_bundle_builder.py
│ │ ├── test_osc_message.py
│ │ ├── test_osc_message_builder.py
│ │ ├── test_osc_packet.py
│ │ ├── test_osc_server.py
│ │ └── test_udp_client.py
│ └── udp_client.py
└── server.py
├── ui
└── panels.py
└── utils
├── keys.py
└── utils.py
/.gitignore:
--------------------------------------------------------------------------------
1 | *~
2 | .DS_Store
3 | *.blend1
4 |
5 | # Byte-compiled / optimized / DLL files
6 | __pycache__/
7 | *$py.class
8 |
9 | # C extensions
10 |
11 | # Distribution / packaging
12 | .Python
13 | build/
14 | develop-eggs/
15 | dist/
16 | downloads/
17 | eggs/
18 | .eggs/
19 | lib/
20 | lib64/
21 | parts/
22 | sdist/
23 | var/
24 | wheels/
25 | *.egg-info/
26 | .installed.cfg
27 | *.egg
28 | MANIFEST
29 |
30 | # PyInstaller
31 | # Usually these files are written by a python script from a template
32 | # before PyInstaller builds the exe, so as to inject date/other infos into it.
33 | *.manifest
34 | *.spec
35 |
36 | # Installer logs
37 | pip-log.txt
38 | pip-delete-this-directory.txt
39 |
40 | # Unit test / coverage reports
41 | htmlcov/
42 | .tox/
43 | .coverage
44 | .coverage.*
45 | .cache
46 | nosetests.xml
47 | coverage.xml
48 | *.cover
49 | .hypothesis/
50 |
51 | # Translations
52 | *.mo
53 | *.pot
54 |
55 | # Django stuff:
56 | *.log
57 | .static_storage/
58 | .media/
59 | local_settings.py
60 |
61 | # Flask stuff:
62 | instance/
63 | .webassets-cache
64 |
65 | # Scrapy stuff:
66 | .scrapy
67 |
68 | # Sphinx documentation
69 | docs/_build/
70 |
71 | # PyBuilder
72 | target/
73 |
74 | # Jupyter Notebook
75 | .ipynb_checkpoints
76 |
77 | # pyenv
78 | .python-version
79 |
80 | # celery beat schedule file
81 | celerybeat-schedule
82 |
83 | # SageMath parsed files
84 | *.sage.py
85 |
86 | # Environments
87 | .env
88 | .venv
89 | env/
90 | venv/
91 | ENV/
92 | env.bak/
93 | venv.bak/
94 |
95 | # Spyder project settings
96 | .spyderproject
97 | .spyproject
98 |
99 | # Rope project settings
100 | .ropeproject
101 |
102 | # mkdocs documentation
103 | /site
104 |
105 | # mypy
106 | .mypy_cache/
107 | .vscode/
108 | .idea
109 |
--------------------------------------------------------------------------------
/.vscode/launch.json:
--------------------------------------------------------------------------------
1 | {
2 | // Use IntelliSense to learn about possible attributes.
3 | // Hover to view descriptions of existing attributes.
4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
5 | "version": "0.2.0",
6 | "configurations": [
7 | {
8 | "name": "Python: Current File (Integrated Terminal)",
9 | "type": "python",
10 | "request": "launch",
11 | "program": "${file}",
12 | "console": "integratedTerminal"
13 | },
14 | {
15 | "name": "Python: Attach",
16 | "type": "python",
17 | "request": "attach",
18 | "port": 5678,
19 | "host": "localhost"
20 | },
21 | {
22 | "name": "Python: Module",
23 | "type": "python",
24 | "request": "launch",
25 | "module": "enter-your-module-name-here",
26 | "console": "integratedTerminal"
27 | },
28 | {
29 | "name": "Python: Django",
30 | "type": "python",
31 | "request": "launch",
32 | "program": "${workspaceFolder}/manage.py",
33 | "console": "integratedTerminal",
34 | "args": [
35 | "runserver",
36 | "--noreload",
37 | "--nothreading"
38 | ],
39 | "django": true
40 | },
41 | {
42 | "name": "Python: Flask",
43 | "type": "python",
44 | "request": "launch",
45 | "module": "flask",
46 | "env": {
47 | "FLASK_APP": "app.py"
48 | },
49 | "args": [
50 | "run",
51 | "--no-debugger",
52 | "--no-reload"
53 | ],
54 | "jinja": true
55 | },
56 | {
57 | "name": "Python: Current File (External Terminal)",
58 | "type": "python",
59 | "request": "launch",
60 | "program": "${file}",
61 | "console": "externalTerminal"
62 | }
63 | ]
64 | }
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # NodeOSC 2.4.0
2 | OSC support for nodes and general usage.
3 |
4 | This add-on does not require any other add-on to work.
5 |
6 | Currently it has node support for
7 | * [Animation Nodes](https://animation-nodes.com/)
8 | * [Sorcar](https://blender-addons.org/sorcar-addon/)
9 |
10 | ## Download
11 |
12 | latest release from [here](https://github.com/maybites/blender.NodeOSC/releases/latest)
13 |
14 | ## Usage
15 |
16 | please visit the [wiki](https://github.com/maybites/blender.NodeOSC/wiki) for more info.
17 |
18 | ### Video Tutorial
19 |
20 |
22 |
23 | ## Credits
24 |
25 | written by maybites (2021)
26 |
27 | heavily inspired by and code used from http://www.jpfep.net/pages/addosc/ and http://www.jpfep.net/pages/addroutes/.
28 |
29 | NodeOSC relies on
30 |
31 | * the pure [python module](https://pypi.org/project/oscpy/) [oscPy](https://github.com/kivy/oscpy) (by Kivy).
32 | * the pure [python module](https://pypi.org/project/python-osc/) [python-osc](https://github.com/attwad/python-osc) (by Attwad).
33 |
34 |
35 | ## ChangeLog
36 |
37 | ### V2.4.0
38 | added script() calling functionality
39 | added more variables for 'format' and 'filter'
40 |
41 | ### V2.3.2
42 | fixed deprecated API-error
43 |
44 | ### V2.3.1
45 | fixed output from animation nodes
46 |
47 | ### V2.3.0
48 | fixed server crash on windows
49 | added incomming address filter
50 |
51 | ### V2.2.0
52 | added argument filtering
53 | updated testbeds with examples for filter
54 |
55 | ### V2.1.0
56 | better error reporting
57 | updated testbeds with examples for statement
58 |
59 | ### V2.0.2
60 | Added ability to execute datapaths as statements
61 |
62 | ### V2.0.0
63 | Added dynamic evaluation format functionality combined with loops. Inspired by functionality introduced in http://www.jpfep.net/pages/addroutes/. Code cleanup and improved user interface.
64 |
65 | ### V1.0.9
66 | Added the neat operator I found in http://www.jpfep.net/pages/addroutes/ to create new osc handlers from the context menu while hovering over a user element.
67 |
68 | ### V1.0.8
69 | Allows to execute function calls with datapath. For example: bpy.ops.screen.animation_play(). values passed on with osc message are ignored.
70 |
71 | ### V1.0.6
72 | Fixed (hopefully) the reference of the dynamic link library for liblo.
73 |
74 | ### V1.0.5
75 | It plays now nice if liblo library is not installed.
76 |
77 | ### V1.0.4
78 | Moved the transformation of AnimationNodes datatype DoubleList into the node.
79 |
80 | ### V1.0.3
81 | Added AnimationNodes datatype DoubleList to be able to send via OscNumber node.
82 |
--------------------------------------------------------------------------------
/__init__.py:
--------------------------------------------------------------------------------
1 | # This Addon for Blender implements realtime OSC controls in the viewport
2 | #
3 | # ***** BEGIN GPL LICENSE BLOCK *****
4 | #
5 | # Copyright (C) 2018 maybites
6 | #
7 | # Copyright (C) 2017 AG6GR
8 | #
9 | # Copyright (C) 2015 JPfeP
10 | #
11 | # This program is free software: you can redistribute it and/or modify
12 | # it under the terms of the GNU General Public License as published by
13 | # the Free Software Foundation, either version 3 of the License, or
14 | # (at your option) any later version.
15 | #
16 | # This program is distributed in the hope that it will be useful,
17 | # but WITHOUT ANY WARRANTY; without even the implied warranty of
18 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
19 | # GNU General Public License for more details.
20 | #
21 | # You should have received a copy of the GNU General Public License
22 | # along with this program. If not, see .
23 | #
24 | # ***** END GPL LICENCE BLOCK *****
25 |
26 | # TODO:
27 | #
28 | # pbm not set to None du modal timer when opening a new blend file
29 |
30 |
31 | bl_info = {
32 | "name": "NodeOSC",
33 | "author": "maybites",
34 | "version": (2, 3, 2),
35 | "blender": (2, 80, 0),
36 | "location": "View3D > Tools > NodeOSC",
37 | "description": "Realtime control of Blender using OSC data protocol",
38 | "wiki_url": "https://github.com/maybites/blender.NodeOSC/wiki",
39 | "tracker_url": "https://github.com/maybites/blender.NodeOSC/issues",
40 | "support": "COMMUNITY",
41 | "category": "System"}
42 |
43 | import bpy
44 |
45 | from bpy.app.handlers import persistent
46 |
47 | #Restore saved settings
48 | @persistent
49 | def nodeosc_handler(scene):
50 | if bpy.context.scene.nodeosc_envars.autorun == True:
51 | if bpy.context.scene.nodeosc_envars.isServerRunning == False:
52 | preferences = bpy.context.preferences
53 | addon_prefs = preferences.addons[__package__].preferences
54 | if addon_prefs.usePyLiblo == False:
55 | bpy.ops.nodeosc.oscpy_operator()
56 | else:
57 | bpy.ops.nodeosc.pythonosc_operator()
58 |
59 |
60 | from . import preferences
61 | from .server import server, operators
62 | from .ui import panels
63 | from .nodes import nodes
64 | from .utils import keys
65 |
66 | def register():
67 | preferences.register()
68 | keys.register()
69 | operators.register()
70 | panels.register()
71 | server.register()
72 | nodes.register()
73 | bpy.app.handlers.load_post.append(nodeosc_handler)
74 |
75 | def unregister():
76 | nodes.unregister()
77 | server.unregister()
78 | panels.unregister()
79 | operators.unregister()
80 | keys.unregister()
81 | preferences.unregister()
82 |
83 | if __name__ == "__main__":
84 | register()
85 |
--------------------------------------------------------------------------------
/help/TestBed.blend:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/maybites/NodeOSC/54287fb5addbd6f69065aef63d7d83eb8ae06671/help/TestBed.blend
--------------------------------------------------------------------------------
/help/Tutorial.blend:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/maybites/NodeOSC/54287fb5addbd6f69065aef63d7d83eb8ae06671/help/Tutorial.blend
--------------------------------------------------------------------------------
/help/Tutorial.maxpat:
--------------------------------------------------------------------------------
1 | {
2 | "patcher" : {
3 | "fileversion" : 1,
4 | "appversion" : {
5 | "major" : 8,
6 | "minor" : 1,
7 | "revision" : 2,
8 | "architecture" : "x64",
9 | "modernui" : 1
10 | }
11 | ,
12 | "classnamespace" : "box",
13 | "rect" : [ 1076.0, 79.0, 330.0, 787.0 ],
14 | "bglocked" : 0,
15 | "openinpresentation" : 0,
16 | "default_fontsize" : 12.0,
17 | "default_fontface" : 0,
18 | "default_fontname" : "Arial",
19 | "gridonopen" : 1,
20 | "gridsize" : [ 15.0, 15.0 ],
21 | "gridsnaponopen" : 1,
22 | "objectsnaponopen" : 1,
23 | "statusbarvisible" : 2,
24 | "toolbarvisible" : 1,
25 | "lefttoolbarpinned" : 0,
26 | "toptoolbarpinned" : 0,
27 | "righttoolbarpinned" : 0,
28 | "bottomtoolbarpinned" : 0,
29 | "toolbars_unpinned_last_save" : 0,
30 | "tallnewobj" : 0,
31 | "boxanimatetime" : 200,
32 | "enablehscroll" : 1,
33 | "enablevscroll" : 1,
34 | "devicewidth" : 0.0,
35 | "description" : "",
36 | "digest" : "",
37 | "tags" : "",
38 | "style" : "",
39 | "subpatcher_template" : "",
40 | "boxes" : [ {
41 | "box" : {
42 | "id" : "obj-1",
43 | "maxclass" : "newobj",
44 | "numinlets" : 1,
45 | "numoutlets" : 0,
46 | "patching_rect" : [ 42.0, 500.0, 32.0, 22.0 ],
47 | "text" : "print"
48 | }
49 |
50 | }
51 | , {
52 | "box" : {
53 | "id" : "obj-12",
54 | "maxclass" : "message",
55 | "numinlets" : 2,
56 | "numoutlets" : 1,
57 | "outlettype" : [ "" ],
58 | "patching_rect" : [ 211.0, 233.0, 49.0, 22.0 ],
59 | "text" : "POS_X"
60 | }
61 |
62 | }
63 | , {
64 | "box" : {
65 | "id" : "obj-11",
66 | "maxclass" : "message",
67 | "numinlets" : 2,
68 | "numoutlets" : 1,
69 | "outlettype" : [ "" ],
70 | "patching_rect" : [ 157.0, 226.0, 49.0, 22.0 ],
71 | "text" : "POS_Y"
72 | }
73 |
74 | }
75 | , {
76 | "box" : {
77 | "id" : "obj-9",
78 | "maxclass" : "message",
79 | "numinlets" : 2,
80 | "numoutlets" : 1,
81 | "outlettype" : [ "" ],
82 | "patching_rect" : [ 139.0, 170.0, 171.0, 22.0 ],
83 | "text" : "/cube/location -7.4 0. 0."
84 | }
85 |
86 | }
87 | , {
88 | "box" : {
89 | "id" : "obj-2",
90 | "maxclass" : "message",
91 | "numinlets" : 2,
92 | "numoutlets" : 1,
93 | "outlettype" : [ "" ],
94 | "patching_rect" : [ 155.0, 283.0, 100.0, 22.0 ],
95 | "text" : "/cube/tracking $1"
96 | }
97 |
98 | }
99 | , {
100 | "box" : {
101 | "id" : "obj-68",
102 | "maxclass" : "toggle",
103 | "numinlets" : 1,
104 | "numoutlets" : 1,
105 | "outlettype" : [ "int" ],
106 | "parameter_enable" : 0,
107 | "patching_rect" : [ 23.0, 20.0, 24.0, 24.0 ]
108 | }
109 |
110 | }
111 | , {
112 | "box" : {
113 | "id" : "obj-64",
114 | "maxclass" : "message",
115 | "numinlets" : 2,
116 | "numoutlets" : 1,
117 | "outlettype" : [ "" ],
118 | "patching_rect" : [ 23.0, 119.0, 73.0, 22.0 ],
119 | "text" : "/viewport $1"
120 | }
121 |
122 | }
123 | , {
124 | "box" : {
125 | "id" : "obj-62",
126 | "maxclass" : "message",
127 | "numinlets" : 2,
128 | "numoutlets" : 1,
129 | "outlettype" : [ "" ],
130 | "patching_rect" : [ 23.0, 525.0, 241.0, 22.0 ],
131 | "text" : "/viewport 0"
132 | }
133 |
134 | }
135 | , {
136 | "box" : {
137 | "format" : 6,
138 | "id" : "obj-56",
139 | "maxclass" : "flonum",
140 | "numinlets" : 1,
141 | "numoutlets" : 2,
142 | "outlettype" : [ "", "bang" ],
143 | "parameter_enable" : 0,
144 | "patching_rect" : [ 241.0, 32.0, 50.0, 22.0 ]
145 | }
146 |
147 | }
148 | , {
149 | "box" : {
150 | "format" : 6,
151 | "id" : "obj-55",
152 | "maxclass" : "flonum",
153 | "numinlets" : 1,
154 | "numoutlets" : 2,
155 | "outlettype" : [ "", "bang" ],
156 | "parameter_enable" : 0,
157 | "patching_rect" : [ 184.0, 32.0, 50.0, 22.0 ]
158 | }
159 |
160 | }
161 | , {
162 | "box" : {
163 | "format" : 6,
164 | "id" : "obj-54",
165 | "maxclass" : "flonum",
166 | "numinlets" : 1,
167 | "numoutlets" : 2,
168 | "outlettype" : [ "", "bang" ],
169 | "parameter_enable" : 0,
170 | "patching_rect" : [ 127.0, 32.0, 50.0, 22.0 ]
171 | }
172 |
173 | }
174 | , {
175 | "box" : {
176 | "id" : "obj-52",
177 | "maxclass" : "message",
178 | "numinlets" : 2,
179 | "numoutlets" : 1,
180 | "outlettype" : [ "" ],
181 | "patching_rect" : [ 127.0, 119.0, 133.0, 22.0 ],
182 | "text" : "/cube/location $1 $2 $3"
183 | }
184 |
185 | }
186 | , {
187 | "box" : {
188 | "id" : "obj-50",
189 | "maxclass" : "newobj",
190 | "numinlets" : 3,
191 | "numoutlets" : 1,
192 | "outlettype" : [ "" ],
193 | "patching_rect" : [ 127.0, 75.0, 133.0, 22.0 ],
194 | "text" : "pak f f f"
195 | }
196 |
197 | }
198 | , {
199 | "box" : {
200 | "id" : "obj-49",
201 | "maxclass" : "newobj",
202 | "numinlets" : 1,
203 | "numoutlets" : 0,
204 | "patching_rect" : [ 23.0, 371.0, 138.0, 22.0 ],
205 | "text" : "udpsend 127.0.0.1 9001"
206 | }
207 |
208 | }
209 | , {
210 | "box" : {
211 | "id" : "obj-48",
212 | "maxclass" : "newobj",
213 | "numinlets" : 1,
214 | "numoutlets" : 1,
215 | "outlettype" : [ "" ],
216 | "patching_rect" : [ 23.0, 443.0, 97.0, 22.0 ],
217 | "text" : "udpreceive 9002"
218 | }
219 |
220 | }
221 | ],
222 | "lines" : [ {
223 | "patchline" : {
224 | "destination" : [ "obj-2", 0 ],
225 | "source" : [ "obj-11", 0 ]
226 | }
227 |
228 | }
229 | , {
230 | "patchline" : {
231 | "destination" : [ "obj-2", 0 ],
232 | "source" : [ "obj-12", 0 ]
233 | }
234 |
235 | }
236 | , {
237 | "patchline" : {
238 | "destination" : [ "obj-49", 0 ],
239 | "source" : [ "obj-2", 0 ]
240 | }
241 |
242 | }
243 | , {
244 | "patchline" : {
245 | "destination" : [ "obj-1", 0 ],
246 | "order" : 1,
247 | "source" : [ "obj-48", 0 ]
248 | }
249 |
250 | }
251 | , {
252 | "patchline" : {
253 | "destination" : [ "obj-62", 1 ],
254 | "order" : 0,
255 | "source" : [ "obj-48", 0 ]
256 | }
257 |
258 | }
259 | , {
260 | "patchline" : {
261 | "destination" : [ "obj-52", 0 ],
262 | "source" : [ "obj-50", 0 ]
263 | }
264 |
265 | }
266 | , {
267 | "patchline" : {
268 | "destination" : [ "obj-49", 0 ],
269 | "order" : 1,
270 | "source" : [ "obj-52", 0 ]
271 | }
272 |
273 | }
274 | , {
275 | "patchline" : {
276 | "destination" : [ "obj-9", 1 ],
277 | "order" : 0,
278 | "source" : [ "obj-52", 0 ]
279 | }
280 |
281 | }
282 | , {
283 | "patchline" : {
284 | "destination" : [ "obj-50", 0 ],
285 | "source" : [ "obj-54", 0 ]
286 | }
287 |
288 | }
289 | , {
290 | "patchline" : {
291 | "destination" : [ "obj-50", 1 ],
292 | "source" : [ "obj-55", 0 ]
293 | }
294 |
295 | }
296 | , {
297 | "patchline" : {
298 | "destination" : [ "obj-50", 2 ],
299 | "source" : [ "obj-56", 0 ]
300 | }
301 |
302 | }
303 | , {
304 | "patchline" : {
305 | "destination" : [ "obj-49", 0 ],
306 | "source" : [ "obj-64", 0 ]
307 | }
308 |
309 | }
310 | , {
311 | "patchline" : {
312 | "destination" : [ "obj-64", 0 ],
313 | "source" : [ "obj-68", 0 ]
314 | }
315 |
316 | }
317 | ],
318 | "dependency_cache" : [ ],
319 | "autosave" : 0
320 | }
321 |
322 | }
323 |
--------------------------------------------------------------------------------
/help/Tutorial.touchosc:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/maybites/NodeOSC/54287fb5addbd6f69065aef63d7d83eb8ae06671/help/Tutorial.touchosc
--------------------------------------------------------------------------------
/nodes/AN/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/maybites/NodeOSC/54287fb5addbd6f69065aef63d7d83eb8ae06671/nodes/AN/__init__.py
--------------------------------------------------------------------------------
/nodes/AN/auto_load.py:
--------------------------------------------------------------------------------
1 | import os
2 | import bpy
3 | import sys
4 | import typing
5 | import inspect
6 | import pkgutil
7 | import importlib
8 | from pathlib import Path
9 |
10 | __all__ = (
11 | "init",
12 | "register",
13 | "unregister",
14 | )
15 |
16 | modules = None
17 | ordered_classes = None
18 |
19 | def init():
20 | global modules
21 | global ordered_classes
22 | #print(Path(__file__).parent.parent.parent)
23 | modules = get_all_submodules(Path(__file__).parent.parent.parent, Path(__file__).parent)
24 | ordered_classes = get_ordered_classes_to_register(modules)
25 |
26 | def register():
27 | try:
28 | for cls in ordered_classes:
29 | bpy.utils.register_class(cls)
30 | except:
31 | print("Unable to find any animation node classes")
32 |
33 | for module in modules:
34 | if module.__name__ == __name__:
35 | continue
36 | if hasattr(module, "register"):
37 | module.register()
38 |
39 | def unregister():
40 | for cls in reversed(ordered_classes):
41 | bpy.utils.unregister_class(cls)
42 |
43 | for module in modules:
44 | if module.__name__ == __name__:
45 | continue
46 | if hasattr(module, "unregister"):
47 | module.unregister()
48 |
49 |
50 | # Import modules
51 | #################################################
52 |
53 | def get_all_submodules(package_dir, directory):
54 | return list(iter_submodules(directory, package_dir.name))
55 |
56 | def iter_submodules(path, package_name):
57 | #print("dictionary: ", path)
58 | #print("package_name: ", package_name)
59 | for name in sorted(iter_submodule_names(path)):
60 | #print("name: ", name, " package_name ", package_name)
61 | yield importlib.import_module("." + name, package_name)
62 |
63 | def iter_submodule_names(path, root="nodes.AN."):
64 | for _, module_name, is_package in pkgutil.iter_modules([str(path)]):
65 | if is_package:
66 | sub_path = path / module_name
67 | sub_root = root + module_name + "."
68 | yield from iter_submodule_names(sub_path, sub_root)
69 | else:
70 | #print("module_name: ", root + module_name)
71 | yield root + module_name
72 |
73 |
74 | # Find classes to register
75 | #################################################
76 |
77 | def get_ordered_classes_to_register(modules):
78 | return toposort(get_register_deps_dict(modules))
79 |
80 | def get_register_deps_dict(modules):
81 | deps_dict = {}
82 | classes_to_register = set(iter_classes_to_register(modules))
83 | for cls in classes_to_register:
84 | deps_dict[cls] = set(iter_own_register_deps(cls, classes_to_register))
85 | return deps_dict
86 |
87 | def iter_own_register_deps(cls, own_classes):
88 | yield from (dep for dep in iter_register_deps(cls) if dep in own_classes)
89 |
90 | def iter_register_deps(cls):
91 | for value in typing.get_type_hints(cls, {}, {}).values():
92 | dependency = get_dependency_from_annotation(value)
93 | if dependency is not None:
94 | yield dependency
95 |
96 | def get_dependency_from_annotation(value):
97 | if isinstance(value, tuple) and len(value) == 2:
98 | if value[0] in (bpy.props.PointerProperty, bpy.props.CollectionProperty):
99 | return value[1]["type"]
100 | return None
101 |
102 | def iter_classes_to_register(modules):
103 | base_types = get_register_base_types()
104 | for cls in get_classes_in_modules(modules):
105 | if any(base in base_types for base in cls.__bases__):
106 | yield cls
107 |
108 | def get_classes_in_modules(modules):
109 | classes = set()
110 | for module in modules:
111 | for cls in iter_classes_in_module(module):
112 | classes.add(cls)
113 | return classes
114 |
115 | def iter_classes_in_module(module):
116 | for value in module.__dict__.values():
117 | if inspect.isclass(value):
118 | yield value
119 |
120 | def get_register_base_types():
121 | return set(getattr(bpy.types, name) for name in [
122 | "Panel", "Operator", "PropertyGroup",
123 | "AddonPreferences", "Header", "Menu",
124 | "Node", "NodeSocket", "NodeTree",
125 | "UIList"
126 | ])
127 |
128 |
129 | # Find order to register to solve dependencies
130 | #################################################
131 |
132 | def toposort(deps_dict):
133 | sorted_list = []
134 | sorted_values = set()
135 | while len(deps_dict) > 0:
136 | unsorted = []
137 | for value, deps in deps_dict.items():
138 | if len(deps) == 0:
139 | sorted_list.append(value)
140 | sorted_values.add(value)
141 | else:
142 | unsorted.append(value)
143 | deps_dict = {value : deps_dict[value] - sorted_values for value in unsorted}
144 | return sorted_list
145 |
--------------------------------------------------------------------------------
/nodes/AN/nodes/OSCListNode.py:
--------------------------------------------------------------------------------
1 | import bpy
2 | import ast
3 | from bpy.props import *
4 | from collections import defaultdict
5 | from animation_nodes.sockets.info import toIdName
6 | from animation_nodes.base_types import AnimationNode
7 | from animation_nodes.data_structures import DoubleList
8 |
9 | from ....utils.utils import *
10 |
11 | dataByIdentifier = defaultdict(None)
12 |
13 | class OSCListNode(bpy.types.Node, AnimationNode):
14 | bl_idname = "an_OSCListNode"
15 | bl_label = "OSCList"
16 |
17 | osc_address: bpy.props.StringProperty(name="Osc address",
18 | default="/an/list",
19 | update = AnimationNode.refresh)
20 | osc_type: bpy.props.StringProperty(
21 | name="Type",
22 | default="fff")
23 | osc_index: bpy.props.StringProperty(
24 | name="Argument indices. Indicate in which order the arguments will be handled inside blender. Have to be in the format \'() or (0 [, 1, 2])\' with 0...n integers, separated by a comma, and inside two parantheses \'()\'. There should be no more indices than arriving arguments, otherwise the message will be ignored",
25 | default="()",
26 | update = AnimationNode.refresh)
27 | osc_direction: bpy.props.EnumProperty(
28 | name = "RX/TX",
29 | default = "INPUT",
30 | items = dataNodeDirectionItems,
31 | update = AnimationNode.refresh)
32 | data_path: bpy.props.StringProperty(
33 | name="data path",
34 | default="")
35 | props: bpy.props.StringProperty(
36 | name="props",
37 | default="")
38 | node_data_type: bpy.props.EnumProperty(
39 | name="NodeDataType",
40 | default="LIST",
41 | items = nodeDataTypeItems)
42 | node_type: bpy.props.IntProperty(
43 | name="NodeType",
44 | default=1)
45 |
46 | createString: BoolProperty(name = "Make String", default = False,
47 | description = "Transform list to string",
48 | update = AnimationNode.refresh)
49 |
50 | default_list: bpy.props.StringProperty(
51 | name="defaultList",
52 | default='[0, 0]',
53 | description = "make sure you follow this structure [ val1, val2, etc..]",
54 | update = AnimationNode.refresh)
55 |
56 | def create(self):
57 | self.data_path = 'bpy.data.node_groups[\'' + self.nodeTree.name + '\'].nodes[\'' + self.name +'\']'
58 |
59 | self.setValue(ast.literal_eval(self.default_list))
60 |
61 | if self.osc_direction == "OUTPUT":
62 | self.props = "value"
63 | self.newInput("Generic", "Value", "value")
64 | if self.osc_direction == "INPUT":
65 | self.props = "setValue"
66 | self.newOutput("Generic", "Value", "value")
67 |
68 | #def delete(self):
69 |
70 | def draw(self, layout):
71 | envars = bpy.context.scene.nodeosc_envars
72 | layout.enabled = not envars.isServerRunning
73 | layout.prop(self, "default_list", text = "")
74 | layout.prop(self, "createString", text = "", icon = "FILE_TEXT")
75 | layout.prop(self, "osc_address", text = "")
76 | layout.prop(self, "osc_index", text = "")
77 | layout.prop(self, "osc_direction", text = "")
78 |
79 | def getExecutionCode(self, required):
80 | if self.osc_direction == "OUTPUT":
81 | return "self.setValue(value)"
82 | if self.osc_direction == "INPUT":
83 | return "value = self.getValue()"
84 |
85 | def setValue(self, value):
86 | if self.createString:
87 | if len(value) == 1:
88 | dataByIdentifier[self.identifier] = str(value[0])
89 | else:
90 | dataByIdentifier[self.identifier] = str(value)
91 | else:
92 | dataByIdentifier[self.identifier] = value
93 |
94 |
95 | def getValue(self):
96 | value = dataByIdentifier.get(self.identifier)
97 | if isinstance(value, DoubleList):
98 | value = tuple(value)
99 | if value is not None and self.createString:
100 | if len(value) == 1:
101 | value = str(value[0])
102 | else:
103 | value = str(value)
104 | return value
105 |
106 |
107 | @property
108 | def value(self):
109 | return self.getValue()
110 |
111 | @value.setter
112 | def value(self, value):
113 | self.setValue(value)
114 |
--------------------------------------------------------------------------------
/nodes/AN/nodes/OSCNumberNode.py:
--------------------------------------------------------------------------------
1 | import bpy
2 | import ast
3 | from bpy.props import *
4 | from collections import defaultdict
5 | from animation_nodes.sockets.info import toIdName
6 | from animation_nodes.base_types import AnimationNode
7 | from animation_nodes.data_structures import DoubleList
8 |
9 | from ....utils.utils import *
10 |
11 | dataByIdentifier = defaultdict(None)
12 |
13 | class OSCNumberNode(bpy.types.Node, AnimationNode):
14 | bl_idname = "an_OSCNumberNode"
15 | bl_label = "OSCNumber"
16 | bl_width_default = 160
17 |
18 | osc_address: bpy.props.StringProperty(name="Osc address",
19 | default="/an/number",
20 | update = AnimationNode.refresh)
21 | osc_type: bpy.props.StringProperty(
22 | name="Type",
23 | default="fff")
24 | osc_index: bpy.props.StringProperty(
25 | name="Argument indices. Indicate in which order the arguments will be handled inside blender. Have to be in the format \'() or (0 [, 1, 2])\' with 0...n integers, separated by a comma, and inside two parantheses \'()\'. There should be no more indices than arriving arguments, otherwise the message will be ignored",
26 | default="()",
27 | update = AnimationNode.refresh)
28 | osc_direction: bpy.props.EnumProperty(
29 | name = "RX/TX",
30 | default = "INPUT",
31 | items = dataNodeDirectionItems,
32 | update = AnimationNode.refresh)
33 | data_path: bpy.props.StringProperty(
34 | name="data path",
35 | default="")
36 | props: bpy.props.StringProperty(
37 | name="props",
38 | default="")
39 | node_data_type: bpy.props.EnumProperty(
40 | name="NodeDataType",
41 | default="SINGLE",
42 | items = nodeDataTypeItems)
43 | node_type: bpy.props.IntProperty(
44 | name="NodeType",
45 | default=1)
46 |
47 | createList: BoolProperty(name = "Create List", default = False,
48 | description = "Create a list of numbers",
49 | update = AnimationNode.refresh)
50 |
51 | default_single: bpy.props.FloatProperty(
52 | name="defaultNumber",
53 | default=0,
54 | update = AnimationNode.refresh)
55 |
56 | default_list: bpy.props.StringProperty(
57 | name="defaultList",
58 | description = "make sure you follow this structure [ val1, val2, etc..]",
59 | default='[0, 0]',
60 | update = AnimationNode.refresh)
61 |
62 | def create(self):
63 | self.data_path = 'bpy.data.node_groups[\'' + self.nodeTree.name + '\'].nodes[\'' + self.name +'\']'
64 | if self.createList:
65 | self.node_data_type = "LIST"
66 | self.setValue(ast.literal_eval(self.default_list))
67 | else:
68 | self.node_data_type = "SINGLE"
69 | self.setValue(self.default_single)
70 |
71 | if self.osc_direction == "OUTPUT":
72 | self.props = "value"
73 | if self.createList:
74 | self.newInput("Float List", "Numbers", "numbers")
75 | else:
76 | self.newInput("Float", "Number", "number")
77 |
78 | if self.osc_direction == "INPUT":
79 | self.props = "setValue"
80 | if self.createList:
81 | self.newOutput("Float List", "Numbers", "numbers")
82 | else:
83 | self.newOutput("Float", "Number", "number")
84 |
85 | def draw(self, layout):
86 | envars = bpy.context.scene.nodeosc_envars
87 | layout.enabled = not envars.isServerRunning
88 | if self.createList:
89 | layout.prop(self, "default_list", text = "")
90 | else:
91 | layout.prop(self, "default_single", text = "")
92 | layout.prop(self, "createList", text = "", icon = "LINENUMBERS_ON")
93 | layout.prop(self, "osc_address", text = "")
94 | layout.prop(self, "osc_index", text = "")
95 | layout.prop(self, "osc_direction", text = "")
96 |
97 | def getExecutionCode(self, required):
98 | if self.osc_direction == "OUTPUT":
99 | if self.createList:
100 | yield "self.setValue(numbers)"
101 | else:
102 | yield "self.setValue(number)"
103 | if self.osc_direction == "INPUT":
104 | if self.createList:
105 | yield "numbers = self.getValue()"
106 | else:
107 | yield "number = self.getValue()"
108 |
109 | def setValue(self, value):
110 | dataByIdentifier[self.identifier] = value
111 |
112 | def getValue(self):
113 | value = dataByIdentifier.get(self.identifier)
114 | if isinstance(value, DoubleList):
115 | return tuple(value)
116 | else:
117 | return value
118 |
119 | @property
120 | def value(self):
121 | return tuple(self.getValue())
122 |
123 | @value.setter
124 | def value(self, value):
125 | self.setValue(value)
126 |
--------------------------------------------------------------------------------
/nodes/AN/nodes/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/maybites/NodeOSC/54287fb5addbd6f69065aef63d7d83eb8ae06671/nodes/AN/nodes/__init__.py
--------------------------------------------------------------------------------
/nodes/AN/ui/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/maybites/NodeOSC/54287fb5addbd6f69065aef63d7d83eb8ae06671/nodes/AN/ui/__init__.py
--------------------------------------------------------------------------------
/nodes/AN/ui/extend_menu.py:
--------------------------------------------------------------------------------
1 | import bpy
2 | from bpy.props import *
3 | from animation_nodes.utils.operators import makeOperator
4 | from animation_nodes.sockets.info import getBaseDataTypes
5 | from animation_nodes.tree_info import getSubprogramNetworks
6 | from animation_nodes.utils.nodes import getAnimationNodeTrees
7 |
8 | mainBaseDataTypes = ("Object", "Integer", "Float", "Vector", "Text")
9 | numericalDataTypes = ("Matrix", "Vector", "Float", "Color", "Euler", "Quaternion")
10 |
11 | def drawMenu(self, context):
12 | if context.space_data.tree_type != "an_AnimationNodeTree": return
13 |
14 | layout = self.layout
15 | layout.operator_context = "INVOKE_DEFAULT"
16 | layout.separator()
17 | layout.menu("AN_MT_OSC_menu", text = "OSC", icon = "LINENUMBERS_ON")
18 |
19 | class OSCMenu(bpy.types.Menu):
20 | bl_idname = "AN_MT_OSC_menu"
21 | bl_label = "OSC Menu"
22 |
23 | def draw(self, context):
24 | layout = self.layout
25 | insertNode(layout, "an_OSCListNode", "List", {"assignedType" : repr("List")})
26 | insertNode(layout, "an_OSCNumberNode", "Number", {"assignedType" : repr("Number")})
27 | layout.separator()
28 |
29 | def insertNode(layout, type, text, settings = {}, icon = "NONE"):
30 | operator = layout.operator("node.add_node", text = text, icon = icon)
31 | operator.type = type
32 | operator.use_transform = True
33 | for name, value in settings.items():
34 | item = operator.settings.add()
35 | item.name = name
36 | item.value = value
37 | return operator
38 |
39 | def register():
40 | bpy.types.NODE_MT_add.append(drawMenu)
41 |
42 | def unregister():
43 | bpy.types.NODE_MT_add.remove(drawMenu)
44 |
--------------------------------------------------------------------------------
/nodes/nodes.py:
--------------------------------------------------------------------------------
1 | import bpy
2 | import os
3 | import addon_utils
4 | import importlib
5 | import nodeitems_utils
6 | import platform
7 |
8 | from nodeitems_utils import NodeItem
9 | from pathlib import Path
10 |
11 | # try loading the node modules
12 | load_an_success = False
13 | load_sc_success = False
14 |
15 | try:
16 | from animation_nodes.events import propertyChanged
17 | load_an_success = True
18 | except ModuleNotFoundError:
19 | load_an_success = False
20 |
21 |
22 | try:
23 | if platform.system() == "Windows":
24 | from sorcar.helper import print_log
25 | from sorcar.tree.ScNodeCategory import ScNodeCategory
26 | load_sc_success = True
27 | else:
28 | from Sorcar.helper import print_log
29 | from Sorcar.tree.ScNodeCategory import ScNodeCategory
30 | load_sc_success = True
31 |
32 | except ModuleNotFoundError:
33 | load_sc_success = False
34 |
35 | # fill up the collections for further processing:
36 | # OSC_nodes with all the OSC nodes we can find
37 | # OSC_outputs with all the handles that need to be sent to the output
38 | def nodes_createCollections():
39 | bpy.context.scene.NodeOSC_nodes.clear()
40 | for node_group in bpy.data.node_groups:
41 | if node_group.bl_idname == 'an_AnimationNodeTree':
42 | for node in node_group.nodes:
43 | if node.bl_idname.find("an_OSC") != -1:
44 | node.refresh()
45 | item = bpy.context.scene.NodeOSC_nodes.add()
46 | item.data_path = node.data_path
47 | item.props = node.props
48 | item.osc_address = node.osc_address
49 | item.osc_type = node.osc_type
50 | item.osc_index = node.osc_index
51 | item.osc_direction = node.osc_direction
52 | item.node_data_type = node.node_data_type
53 | item.node_type = node.node_type
54 | if node_group.bl_idname == 'ScNodeTree':
55 | for node in node_group.nodes:
56 | if node.bl_idname.find("ScOSC") != -1:
57 | node.post_execute()
58 | item = bpy.context.scene.NodeOSC_nodes.add()
59 | item.data_path = node.data_path
60 | item.props = node.props
61 | item.osc_address = node.osc_address
62 | item.osc_type = node.osc_type
63 | item.osc_index = node.osc_index
64 | item.osc_direction = node.osc_direction
65 | item.node_data_type = node.node_data_type
66 | item.node_type = node.node_type
67 |
68 | bpy.context.scene.NodeOSC_outputs.clear()
69 | for itemN in bpy.context.scene.NodeOSC_nodes:
70 | if itemN.enabled and itemN.osc_direction != "INPUT":
71 | item = bpy.context.scene.NodeOSC_outputs.add()
72 | item.data_path = itemN.data_path
73 | item.props = itemN.props
74 | item.osc_address = itemN.osc_address
75 | item.osc_type = itemN.osc_type
76 | item.osc_index = itemN.osc_index
77 | item.osc_direction = itemN.osc_direction
78 | item.node_data_type = itemN.node_data_type
79 | item.node_type = itemN.node_type
80 | for itemN in bpy.context.scene.NodeOSC_keys:
81 | if itemN.enabled and itemN.osc_direction != "INPUT":
82 | item = bpy.context.scene.NodeOSC_outputs.add()
83 | item.data_path = itemN.data_path
84 | item.props = itemN.props
85 | item.osc_address = itemN.osc_address
86 | item.osc_type = itemN.osc_type
87 | item.osc_index = itemN.osc_index
88 | item.osc_direction = itemN.osc_direction
89 | item.node_data_type = itemN.node_data_type
90 | item.node_type = itemN.node_type
91 |
92 |
93 |
94 | # checks if there is any active and supported node system
95 | def hasNodes():
96 | if hasAnimationNodes() or hasSorcarNodes():
97 | return True
98 | return False
99 |
100 | # checks if there is any active animation node system
101 | def hasAnimationNodes():
102 | if bpy.context.scene.nodeosc_AN_isLoaded:
103 | for node_group in bpy.data.node_groups:
104 | if node_group.bl_idname == 'an_AnimationNodeTree':
105 | return True
106 | return False
107 |
108 | # checks if there is any active sorcar node system
109 | def hasSorcarNodes():
110 | if bpy.context.scene.nodeosc_AN_isLoaded:
111 | for node_group in bpy.data.node_groups:
112 | if node_group.bl_idname == 'ScNodeTree':
113 | return True
114 | return False
115 |
116 | # executes only animation node systems
117 | def executeAnimationNodeTrees():
118 | if load_an_success:
119 | if bpy.context.scene.nodeosc_AN_needsUpdate:
120 | propertyChanged()
121 | bpy.context.scene.nodeosc_AN_needsUpdate = False
122 |
123 | # executes only sorcar node systems
124 | # this method needs to be called from a server Modal
125 | def executeSorcarNodeTrees(context):
126 | if load_sc_success:
127 | if bpy.context.scene.nodeosc_SORCAR_needsUpdate:
128 | for node_group in bpy.data.node_groups:
129 | if node_group.bl_idname == 'ScNodeTree':
130 | node_group.execute_node()
131 | bpy.context.scene.nodeosc_SORCAR_needsUpdate = False
132 |
133 | def import_sorcar_nodes(path):
134 | out = {}
135 | for cat in [i for i in os.listdir(str(path) + "/nodes/sorcar/nodes") if not i.startswith("_") and not i.startswith(".")]:
136 | out[cat] = []
137 | for i in bpy.path.module_names(str(path) + "/nodes/sorcar/nodes/" + cat):
138 | out[cat].append(getattr(importlib.import_module(".nodes.sorcar.nodes." + cat + "." + i[0], path.name), i[0]))
139 | print_log("IMPORT NODE", bpy.path.display_name(cat), msg=i[0])
140 | return out
141 |
142 | if load_an_success:
143 | from .AN import auto_load
144 | auto_load.init()
145 |
146 | if load_sc_success:
147 | if platform.system() == "Windows":
148 | from sorcar import all_classes
149 | else:
150 | from Sorcar import all_classes
151 | classes_nodes = []
152 |
153 | def register():
154 | global load_an_success
155 | global load_sc_success
156 |
157 | bpy.types.Scene.nodeosc_AN_needsUpdate = bpy.props.BoolProperty(default=False)
158 | bpy.types.Scene.nodeosc_SORCAR_needsUpdate = bpy.props.BoolProperty(default=False)
159 |
160 | if load_an_success:
161 | # importing and registering animation nodes...
162 | auto_load.register()
163 | bpy.types.Scene.nodeosc_AN_isLoaded = bpy.props.BoolProperty(default=True, description='AN addon detected')
164 | else:
165 | bpy.types.Scene.nodeosc_AN_isLoaded = bpy.props.BoolProperty(default=False, description='AN addon detected')
166 |
167 | if load_sc_success:
168 | # importing and registering sorcar nodes...
169 | packagePath = Path(__file__).parent.parent
170 |
171 | global classes_nodes
172 |
173 | classes_nodes = import_sorcar_nodes(packagePath)
174 |
175 | total_nodes = 0
176 | node_categories = []
177 | for cat in classes_nodes:
178 | total_nodes += len(classes_nodes[cat])
179 | node_categories.append(ScNodeCategory(identifier="sc_"+cat, name=bpy.path.display_name(cat), items=[NodeItem(i.bl_idname) for i in classes_nodes[cat]]))
180 | for c in classes_nodes[cat]:
181 | bpy.utils.register_class(c)
182 |
183 | nodeitems_utils.register_node_categories("osc_node_categories", node_categories)
184 | bpy.types.Scene.nodeosc_SORCAR_isLoaded = bpy.props.BoolProperty(default=True, description='SORCAR addon detected')
185 | else:
186 | bpy.types.Scene.nodeosc_SORCAR_isLoaded = bpy.props.BoolProperty(default=False, description='SORCAR addon detected')
187 |
188 |
189 | def unregister():
190 | del bpy.types.Scene.nodeosc_AN_isLoaded
191 | del bpy.types.Scene.nodeosc_AN_needsUpdate
192 |
193 | del bpy.types.Scene.nodeosc_SORCAR_isLoaded
194 | del bpy.types.Scene.nodeosc_SORCAR_needsUpdate
195 |
196 | if load_an_success:
197 | auto_load.unregister()
198 |
199 | if load_sc_success:
200 | global classes_nodes
201 |
202 | for cat in classes_nodes:
203 | for c in classes_nodes[cat]:
204 | bpy.utils.unregister_class(c)
205 | nodeitems_utils.unregister_node_categories("osc_node_categories")
206 |
--------------------------------------------------------------------------------
/nodes/sorcar/nodes/_base/node_base.py:
--------------------------------------------------------------------------------
1 | import bpy
2 |
3 | import platform
4 |
5 | if platform.system() == "Windows":
6 | from sorcar.nodes._base.node_base import ScNode
7 | else:
8 | from Sorcar.nodes._base.node_base import ScNode
9 |
10 | from .....utils.utils import *
11 |
12 | def sorcarTreeUpdate(self, context):
13 | bpy.context.scene.nodeosc_SORCAR_needsUpdate = True
14 |
15 | class ScOSCNode(ScNode):
16 |
17 | osc_address: bpy.props.StringProperty(name="Osc address",
18 | default="/sorcar/number")
19 | osc_type: bpy.props.StringProperty(
20 | name="Type",
21 | default="fff")
22 | osc_index: bpy.props.StringProperty(
23 | name="Argument indices. Indicate in which order the arguments will be handled inside blender. Have to be in the format \'() or (0 [, 1, 2])\' with 0...n integers, separated by a comma, and inside two parantheses \'()\'. There should be no more indices than arriving arguments, otherwise the message will be ignored",
24 | default="()")
25 | osc_direction: bpy.props.EnumProperty(
26 | name = "RX/TX",
27 | default = "INPUT",
28 | items = dataNodeDirectionItems)
29 | data_path: bpy.props.StringProperty(
30 | name="data path",
31 | default="")
32 | id: bpy.props.StringProperty(
33 | name="id",
34 | default="setValue")
35 | node_data_type: bpy.props.EnumProperty(
36 | name="NodeDataType",
37 | default="SINGLE",
38 | items = nodeDataTypeItems)
39 | node_type: bpy.props.IntProperty(
40 | name="NodeType",
41 | default=2)
42 |
43 | def init(self, context):
44 | super().init(context)
45 | self.data_path = 'bpy.data.node_groups[\'' + self.id_data.name + '\'].nodes[\'' + self.name +'\']'
46 |
47 | def draw_buttons(self, context, layout):
48 | super().draw_buttons(context, layout)
49 | envars = bpy.context.scene.nodeosc_envars
50 | layout.enabled = not envars.isServerRunning
51 |
52 | def error_condition(self):
53 | return (
54 | super().error_condition()
55 | )
56 |
57 | def update_value(self, context):
58 | if context.space_data is not None:
59 | super().update_value(context)
60 | else:
61 | sorcarTreeUpdate(self, context)
62 | return None
63 |
64 | def post_execute(self):
65 | return super().post_execute()
66 |
67 | def setValue(self, value):
68 | self.post_execute()
69 |
70 | def getValue(self):
71 | pass
72 |
73 | @property
74 | def value(self):
75 | return self.getValue()
76 |
77 | @value.setter
78 | def value(self, value):
79 | self.setValue(value)
80 |
--------------------------------------------------------------------------------
/nodes/sorcar/nodes/osc/ScOSCNumber.py:
--------------------------------------------------------------------------------
1 | import bpy
2 | import numpy
3 |
4 | from bpy.props import EnumProperty, FloatProperty, IntProperty, BoolProperty, StringProperty
5 | from bpy.types import Node
6 | from .._base.node_base import *
7 | from numpy import array, uint32
8 |
9 | from .....utils.utils import *
10 |
11 | class ScOSCNumber(Node, ScOSCNode):
12 | bl_idname = "ScOSCNumber"
13 | bl_label = "OSCNumber"
14 |
15 | prop_type: EnumProperty(name="Type", items=[("FLOAT", "Float", ""), ("INT", "Integer", ""), ("ANGLE", "Angle", "")], default="FLOAT", update=ScOSCNode.update_value)
16 | prop_float: FloatProperty(name="Float", update=ScOSCNode.update_value)
17 | prop_int: IntProperty(name="Integer", update=ScOSCNode.update_value)
18 | prop_angle: FloatProperty(name="Angle", unit="ROTATION", update=ScOSCNode.update_value)
19 |
20 | def init(self, context):
21 | super().init(context)
22 | self.outputs.new("ScNodeSocketNumber", "Value")
23 |
24 | def draw_buttons(self, context, layout):
25 | super().draw_buttons(context, layout)
26 | #if (not self.inputs["Random"].default_value):
27 | layout.prop(self, "prop_type", expand=True)
28 | if (self.prop_type == "FLOAT"):
29 | layout.prop(self, "prop_float")
30 | elif (self.prop_type == "INT"):
31 | layout.prop(self, "prop_int")
32 | elif (self.prop_type == "ANGLE"):
33 | layout.prop(self, "prop_angle")
34 | layout.prop(self, "osc_address", text="")
35 | layout.prop(self, "osc_index", text="")
36 | #layout.prop(self, "osc_direction", text="")
37 |
38 | def error_condition(self):
39 | return (
40 | super().error_condition()
41 | )
42 |
43 | def post_execute(self):
44 | out = {}
45 | if (self.prop_type == "FLOAT"):
46 | out["Value"] = self.prop_float
47 | elif (self.prop_type == "INT"):
48 | out["Value"] = self.prop_int
49 | elif (self.prop_type == "ANGLE"):
50 | out["Value"] = self.prop_angle
51 | return out
52 |
53 | def setValue(self, value):
54 | if (self.prop_type == "FLOAT"):
55 | self.prop_float = value
56 | elif (self.prop_type == "INT"):
57 | self.prop_int = value
58 | elif (self.prop_type == "ANGLE"):
59 | self.prop_angle = value
60 | self.post_execute()
61 |
62 | def getValue(self):
63 | if (self.prop_type == "FLOAT"):
64 | return self.prop_float
65 | elif (self.prop_type == "INT"):
66 | return self.prop_int
67 | elif (self.prop_type == "ANGLE"):
68 | return self.prop_angle
69 |
--------------------------------------------------------------------------------
/nodes/sorcar/nodes/osc/ScOSCString.py:
--------------------------------------------------------------------------------
1 | import bpy
2 |
3 | from bpy.props import StringProperty
4 | from bpy.types import Node
5 | from .._base.node_base import *
6 |
7 | from .....utils.utils import *
8 |
9 | class ScOSCString(Node, ScOSCNode):
10 | bl_idname = "ScOSCString"
11 | bl_label = "OSCString"
12 |
13 | prop_string: StringProperty(name="String", update=ScOSCNode.update_value)
14 |
15 | def init(self, context):
16 | super().init(context)
17 | self.osc_address = "/sorcar/string"
18 | self.node_data_type = "LIST"
19 | self.outputs.new("ScNodeSocketString", "Value")
20 |
21 | def draw_buttons(self, context, layout):
22 | super().draw_buttons(context, layout)
23 | layout.prop(self, "prop_string", text="")
24 | layout.prop(self, "osc_address", text="")
25 | layout.prop(self, "osc_index", text="")
26 |
27 |
28 | def post_execute(self):
29 | out = {}
30 | out["Value"] = self.prop_string
31 | return out
32 |
33 | def setValue(self, value):
34 | self.prop_string = value
35 | self.post_execute()
36 |
37 | def getValue(self):
38 | return self.prop_string
39 |
--------------------------------------------------------------------------------
/nodes/sorcar/nodes/osc/ScOSCVector.py:
--------------------------------------------------------------------------------
1 | import bpy
2 | import mathutils
3 |
4 | from bpy.props import EnumProperty, FloatProperty
5 | from bpy.types import Node
6 | from mathutils import Vector
7 | from .._base.node_base import *
8 |
9 | from .....utils.utils import *
10 |
11 | class ScOSCVector(Node, ScOSCNode):
12 | bl_idname = "ScOSCVector"
13 | bl_label = "OSCVector"
14 |
15 | in_uniform: EnumProperty(items=[("NONE", "None", "-"), ("XY", "XY", "-"), ("YZ", "YZ", "-"), ("XZ", "XZ", "-"), ("XYZ", "XYZ", "-")], default="NONE", update=ScOSCNode.update_value)
16 | in_x: FloatProperty(update=ScOSCNode.update_value)
17 | in_y: FloatProperty(update=ScOSCNode.update_value)
18 | in_z: FloatProperty(update=ScOSCNode.update_value)
19 |
20 | def init(self, context):
21 | super().init(context)
22 | self.inputs.new("ScNodeSocketString", "Uniform").init("in_uniform")
23 | self.inputs.new("ScNodeSocketNumber", "X").init("in_x", True)
24 | self.inputs.new("ScNodeSocketNumber", "Y").init("in_y", True)
25 | self.inputs.new("ScNodeSocketNumber", "Z").init("in_z", True)
26 |
27 | self.osc_address = "/sorcar/vector"
28 | self.node_data_type = "LIST"
29 | self.outputs.new("ScNodeSocketVector", "Value")
30 |
31 | def error_condition(self):
32 | return (
33 | not self.inputs["Uniform"].default_value in ["NONE", "XY", "YZ", "XZ", "XYZ"]
34 | )
35 |
36 | def post_execute(self):
37 | out = {}
38 | if (self.inputs["Uniform"].default_value == "NONE"):
39 | out["Value"] = Vector((self.inputs["X"].default_value, self.inputs["Y"].default_value, self.inputs["Z"].default_value))
40 | elif (self.inputs["Uniform"].default_value == "XY"):
41 | out["Value"] = Vector((self.inputs["X"].default_value, self.inputs["X"].default_value, self.inputs["Z"].default_value))
42 | elif (self.inputs["Uniform"].default_value == "YZ"):
43 | out["Value"] = Vector((self.inputs["X"].default_value, self.inputs["Y"].default_value, self.inputs["Y"].default_value))
44 | elif (self.inputs["Uniform"].default_value == "XZ"):
45 | out["Value"] = Vector((self.inputs["X"].default_value, self.inputs["Y"].default_value, self.inputs["X"].default_value))
46 | elif (self.inputs["Uniform"].default_value == "XYZ"):
47 | out["Value"] = Vector((self.inputs["X"].default_value, self.inputs["X"].default_value, self.inputs["X"].default_value))
48 | return out
49 |
50 | def draw_buttons(self, context, layout):
51 | super().draw_buttons(context, layout)
52 | layout.prop(self, "osc_address", text="")
53 | layout.prop(self, "osc_index", text="")
54 |
55 | def error_condition(self):
56 | return (
57 | super().error_condition()
58 | )
59 |
60 | def setValue(self, value):
61 | if len(value) > 0:
62 | self.in_x = value[0]
63 | if len(value) > 1:
64 | self.in_y = value[1]
65 | if len(value) > 2:
66 | self.in_z = value[2]
67 | self.post_execute()
68 |
69 | def getValue(self):
70 | return post_execute()["Value"]
71 |
--------------------------------------------------------------------------------
/preferences.py:
--------------------------------------------------------------------------------
1 | import bpy
2 | import platform
3 |
4 | from bpy.types import Operator, AddonPreferences
5 | from bpy.props import StringProperty, IntProperty, BoolProperty
6 |
7 | nodeUpdateItems = {
8 | ("EACH", "on each message", "Node Tree is executed on each message (ideal for low frequency messages)", "NONE", 0),
9 | ("MESSAGE", "on specific message", "Node Tree is executed on a specific message (ideal for high frequency messages)", "NONE", 1) }
10 |
11 | class ErrorEntry(bpy.types.PropertyGroup):
12 | type: bpy.props.StringProperty(name="type", default="ERROR")
13 | name: bpy.props.StringProperty(name="name", default="error")
14 | value: bpy.props.StringProperty(name="value", default="empty")
15 |
16 | class NodeOSCEnvVarSettings(bpy.types.PropertyGroup):
17 | udp_in: bpy.props.StringProperty(default="127.0.0.1", description='The IP of this machine (on which blender is running)')
18 | udp_out: bpy.props.StringProperty(default="127.0.0.1", description='The IP of the machine to send messages to (can be the same if you want to send it to another application that runs on this machine)')
19 | port_in: bpy.props.IntProperty(default=9001, min=0, max=65535, description='The input network port (0-65535)')
20 | port_out: bpy.props.IntProperty(default=9002, min=0, max= 65535, description='The output network port (0-65535)')
21 | input_rate: bpy.props.IntProperty(default=0 ,description="The refresh rate of checking for input messages (millisecond)", min=0)
22 | output_rate: bpy.props.IntProperty(default=40 ,description="The refresh rate of sending output messages (millisecond)", min=1)
23 | repeat_address_filter_IN: bpy.props.BoolProperty(default=False ,description="Filter repeating incomming addresses")
24 | repeat_argument_filter_OUT: bpy.props.BoolProperty(default=False ,description="Avoid sending messages with repeating arguments. This applies the filter to all handlers")
25 | isUIExpanded: bpy.props.BoolProperty(default=True, description='Shows the detailed settings inside the UI panel')
26 | isServerRunning: bpy.props.BoolProperty(default=False, description='Show if the engine is running or not')
27 | message_monitor: bpy.props.BoolProperty(description="Display the current value of your keys, the last message received and some infos in console")
28 | enable_incomming_message_printout: bpy.props.BoolProperty(description="Printout all incomming messages to the info log")
29 | debug_monitor: bpy.props.BoolProperty(name="Format Debug Monitor", description="Printout the evaluated data-paths when using format functionality")
30 | autorun: bpy.props.BoolProperty(description="Start the OSC engine automatically after loading a project. IMPORTANT: This only works if the project is saved while the server is NOT running!")
31 | lastaddr: bpy.props.StringProperty(description="Display the last OSC address received")
32 | lastpayload: bpy.props.StringProperty(description="Display the last OSC message content")
33 | node_update: bpy.props.EnumProperty(name = "node update", default = "EACH", items = nodeUpdateItems)
34 | node_frameMessage: bpy.props.StringProperty(default="/frame/end",description="OSC message that triggers a node tree execution")
35 | error: bpy.props.CollectionProperty(type = ErrorEntry)
36 | executionTimeInput: bpy.props.FloatProperty(name = "Input Execution Time")
37 | executionTimeOutput: bpy.props.FloatProperty(name = "Input Execution Time")
38 |
39 | class NodeOSCPreferences(AddonPreferences):
40 | # this must match the addon name, use '__package__'
41 | # when defining this in a submodule of a python package.
42 | bl_idname = __package__
43 |
44 | usePyLiblo: BoolProperty(
45 | name="Use Python OSC library. This is an alternative library that also accepts asterisk '*' inside the address",
46 | default=False,
47 | )
48 |
49 | def draw(self, context):
50 | prefs = context.preferences
51 | view = prefs.view
52 |
53 | layout = self.layout
54 | layout.prop(self, "usePyLiblo")
55 |
56 | layout.label(text="Helpfull to get full data paths is to enable python tool tips:")
57 | layout.prop(view, "show_tooltips_python")
58 | layout.label(text="Use Ctrl-Alt-Shift-C to copy the full datapath to the clipboard")
59 |
60 | def register():
61 | bpy.utils.register_class(ErrorEntry)
62 | bpy.utils.register_class(NodeOSCEnvVarSettings)
63 | bpy.utils.register_class(NodeOSCPreferences)
64 | bpy.types.Scene.nodeosc_envars = bpy.props.PointerProperty(type=NodeOSCEnvVarSettings)
65 |
66 | def unregister():
67 | del bpy.types.Scene.nodeosc_envars
68 | bpy.utils.unregister_class(NodeOSCPreferences)
69 | bpy.utils.unregister_class(NodeOSCEnvVarSettings)
70 | bpy.utils.unregister_class(ErrorEntry)
71 |
--------------------------------------------------------------------------------
/server/oscpy/AUTHORS.txt:
--------------------------------------------------------------------------------
1 | Gabriel Pettier
2 | Andre Miras
3 | Armin Sebastian
4 | Ray Chang
5 | Tamas Levai
6 |
--------------------------------------------------------------------------------
/server/oscpy/CHANGELOG:
--------------------------------------------------------------------------------
1 | v0.5.0
2 | ======
3 |
4 | Allow accessing sender's ip/port from outside of answer()
5 | Fix get_sender() error on IOS/Android.
6 | Fix encoding issue in OSCThreadServer.unbind().
7 |
8 | v0.4.0
9 | ======
10 |
11 | Unicode support, servers and clients can declare an encoding for strings.
12 | Fix timeout bug after 15mn without any activity.
13 | Allow answering to a specific port.
14 | Allow callbacks to get the addresses they were called with.
15 | Add default_handler option for a server, to get all messages that didn't match any known address.
16 | Allow using default socket implicitly in OSCThreadServer.stop()
17 | Add statistics collections (messages/bytes sent/received)
18 | Add default routes to probe the server about existing routes and usage statistics.
19 | Improve reliability of stopping server.
20 | Add support for Midi messages
21 | Add support for True/False/Nil/Infinitum messages
22 | Add support for Symbol messages (treated as strings)
23 | Test/Coverage of more python versions and OSs in CI
24 | Improve documentation (README, CHANGELOG, and CONTRIBUTING)
25 |
26 | v0.3.0
27 | ======
28 |
29 | increase test coverage
30 | remove notice about WIP status
31 | add test/fix for refusing to send unicode strings
32 | use bytearray for blobs, since object catch wrong types
33 | fix client code example in readme
34 | allow binding methods with the @address_method decorator
35 | add test for @address decorator
36 | clarify that @address decorator won't work for methods
37 | add test and warnings about AF_UNIX not working on windows
38 | fix negative letter matching
39 | check exception is raised when no default socket
40 | add test and implementation for advanced_matching
41 |
42 |
43 | v0.2.0
44 | ======
45 |
46 | ignore build and dist dirs
47 | fix inet/unix comparison in performance test
48 | cleanup & documentation & performance test
49 | first minor version
50 |
51 |
52 | v0.1.3, v0.1.2, v0.1.1
53 | ======================
54 | fix setup.py classifiers
55 |
56 |
57 | v0.1.0 Initial release
58 | ======================
59 |
60 | OSCThreadServer implementation and basic tests
61 | OSCClient implementation
62 |
--------------------------------------------------------------------------------
/server/oscpy/LICENSE.txt:
--------------------------------------------------------------------------------
1 | Copyright (c) 2018 Gabriel Pettier & al
2 |
3 | Permission is hereby granted, free of charge, to any person obtaining a copy
4 | of this software and associated documentation files (the "Software"), to deal
5 | in the Software without restriction, including without limitation the rights
6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
7 | copies of the Software, and to permit persons to whom the Software is
8 | furnished to do so, subject to the following conditions:
9 |
10 | The above copyright notice and this permission notice shall be included in all
11 | copies or substantial portions of the Software.
12 |
13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
19 | SOFTWARE.
20 |
--------------------------------------------------------------------------------
/server/oscpy/README.md:
--------------------------------------------------------------------------------
1 | ### OSCPy
2 |
3 | [](https://coveralls.io/github/kivy/oscpy?branch=master)
4 | CI is done by Github Checks, see the current commit for build status.
5 |
6 |
7 | A modern implementation of OSC for python2/3.
8 |
9 | #### What is OSC.
10 |
11 | OpenSoundControl is an UDP based network protocol, that is designed for fast
12 | dispatching of time-sensitive messages, as the name suggests, it was designed
13 | as a replacement for MIDI, but applies well to other situations. The protocol is
14 | simple to use, OSC addresses look like http URLs, and accept various basic
15 | types, such as string, float, int, etc. You can think of it basically as an
16 | http POST, with less overhead.
17 |
18 | You can learn more about OSC on [OpenSoundControl.org](http://opensoundcontrol.org/introduction-osc)
19 |
20 | #### Goals
21 |
22 | - python2.7/3.6+ compatibility (can be relaxed more on the python3 side
23 | if needed, but nothing before 2.7 will be supported)
24 | - fast
25 | - easy to use
26 | - robust (returns meaningful errors in case of malformed messages,
27 | always do the right thing on correct messages, and by default intercept+log
28 | the exceptions raised by callbacks)
29 | - separation of concerns (message parsing vs communication)
30 | - sync and async compatibility (threads, asyncio, trio…)
31 | - clean and easy to read code
32 |
33 | #### Features
34 |
35 | - serialize and parse OSC data types/Messages/Bundles
36 | - a thread based udp server to open sockets (INET or UNIX) and bind callbacks on osc addresses on them
37 | - a simple client
38 |
39 | #### Install
40 | ```sh
41 | pip install oscpy
42 | ```
43 |
44 | #### Usage
45 |
46 | Server (thread)
47 |
48 | ```python
49 | from oscpy.server import OSCThreadServer
50 | from time import sleep
51 |
52 | def callback(*values):
53 | print("got values: {}".format(values))
54 |
55 | osc = OSCThreadServer() # See sources for all the arguments
56 |
57 | # You can also use an \*nix socket path here
58 | sock = osc.listen(address='0.0.0.0', port=8000, default=True)
59 | osc.bind(b'/address', callback)
60 | sleep(1000)
61 | osc.stop() # Stop the default socket
62 |
63 | osc.stop_all() # Stop all sockets
64 |
65 | # Here the server is still alive, one might call osc.listen() again
66 |
67 | osc.terminate_server() # Request the handler thread to stop looping
68 |
69 | osc.join_server() # Wait for the handler thread to finish pending tasks and exit
70 | ```
71 |
72 | or you can use the decorator API.
73 |
74 | Server (thread)
75 |
76 | ```python
77 | from oscpy.server import OSCThreadServer
78 | from time import sleep
79 |
80 | osc = OSCThreadServer()
81 | sock = osc.listen(address='0.0.0.0', port=8000, default=True)
82 |
83 | @osc.address(b'/address')
84 | def callback(*values):
85 | print("got values: {}".format(values))
86 |
87 | sleep(1000)
88 | osc.stop()
89 | ```
90 |
91 | Servers are also client, in the sense they can send messages and answer to
92 | messages from other servers
93 |
94 | ```python
95 | from oscpy.server import OSCThreadServer
96 | from time import sleep
97 |
98 | osc_1 = OSCThreadServer()
99 | osc_1.listen(default=True)
100 |
101 | @osc_1.address(b'/ping')
102 | def ping(*values):
103 | print("ping called")
104 | if True in values:
105 | cont.append(True)
106 | else:
107 | osc_1.answer(b'/pong')
108 |
109 | osc_2 = OSCThreadServer()
110 | osc_2.listen(default=True)
111 |
112 | @osc_2.address(b'/pong')
113 | def pong(*values):
114 | print("pong called")
115 | osc_2.answer(b'/ping', [True])
116 |
117 | osc_2.send_message(b'/ping', [], *osc_1.getaddress())
118 |
119 | timeout = time() + 1
120 | while not cont:
121 | if time() > timeout:
122 | raise OSError('timeout while waiting for success message.')
123 | ```
124 |
125 |
126 | Server (async) (TODO!)
127 |
128 | ```python
129 | from oscpy.server import OSCThreadServer
130 |
131 | with OSCAsyncServer(port=8000) as OSC:
132 | for address, values in OSC.listen():
133 | if address == b'/example':
134 | print("got {} on /example".format(values))
135 | else:
136 | print("unknown address {}".format(address))
137 | ```
138 |
139 | Client
140 |
141 | ```python
142 | from oscpy.client import OSCClient
143 |
144 | address = "127.0.0.1"
145 | port = 8000
146 |
147 | osc = OSCClient(address, port)
148 | for i in range(10):
149 | osc.send_message(b'/ping', [i])
150 | ```
151 |
152 | #### Unicode
153 |
154 | By default, the server and client take bytes (encoded strings), not unicode
155 | strings, for osc addresses as well as osc strings. However, you can pass an
156 | `encoding` parameter to have your strings automatically encoded and decoded by
157 | them, so your callbacks will get unicode strings (unicode in python2, str in
158 | python3).
159 |
160 | ```python
161 | osc = OSCThreadServer(encoding='utf8')
162 | osc.listen(default=True)
163 |
164 | values = []
165 |
166 | @osc.address(u'/encoded')
167 | def encoded(*val):
168 | for v in val:
169 | assert not isinstance(v, bytes)
170 | values.append(val)
171 |
172 | send_message(
173 | u'/encoded',
174 | [u'hello world', u'ééééé ààààà'],
175 | *osc.getaddress(), encoding='utf8')
176 | ```
177 |
178 | (`u` literals added here for clarity).
179 |
180 | #### CLI
181 |
182 | OSCPy provides an "oscli" util, to help with debugging:
183 | - `oscli dump` to listen for messages and dump them
184 | - `oscli send` to send messages or bundles to a server
185 |
186 | See `oscli -h` for more information.
187 |
188 | #### GOTCHAS
189 |
190 | - `None` values are not allowed in serialization
191 | - Unix-type sockets must not already exist when you listen() on them
192 |
193 | #### TODO
194 |
195 | - real support for timetag (currently only supports optionally
196 | dropping late bundles, not delaying those with timetags in the future)
197 | - support for additional argument types
198 | - an asyncio-oriented server implementation
199 | - examples & documentation
200 |
201 | #### Contributing
202 |
203 | Check out our [contribution guide](CONTRIBUTING.md) and feel free to improve OSCPy.
204 |
205 | #### License
206 |
207 | OSCPy is released under the terms of the MIT License.
208 | Please see the [LICENSE.txt](LICENSE.txt) file.
209 |
--------------------------------------------------------------------------------
/server/oscpy/__init__.py:
--------------------------------------------------------------------------------
1 | """See README.md for package information."""
2 |
3 | __version__ = '0.6.0-dev6'
4 |
--------------------------------------------------------------------------------
/server/oscpy/cli.py:
--------------------------------------------------------------------------------
1 | # coding: utf8
2 | """OSCPy command line tools"""
3 |
4 | from argparse import ArgumentParser
5 | from time import sleep
6 | from sys import exit, stderr
7 | from ast import literal_eval
8 |
9 | from oscpy.client import send_message
10 | from oscpy.server import OSCThreadServer
11 | from oscpy.stats import Stats
12 |
13 |
14 | def _send(options):
15 | def _parse(s):
16 | try:
17 | return literal_eval(s)
18 | except:
19 | return s
20 |
21 | stats = Stats()
22 | for i in range(options.repeat):
23 | stats += send_message(
24 | options.address,
25 | [_parse(x) for x in options.message],
26 | options.host,
27 | options.port,
28 | safer=options.safer,
29 | encoding=options.encoding,
30 | encoding_errors=options.encoding_errors
31 | )
32 | print(stats)
33 |
34 |
35 | def __dump(options):
36 | def dump(address, *values):
37 | print(u'{}: {}'.format(
38 | address.decode('utf8'),
39 | ', '.join(
40 | '{}'.format(
41 | v.decode(options.encoding or 'utf8')
42 | if isinstance(v, bytes)
43 | else v
44 | )
45 | for v in values if values
46 | )
47 | ))
48 |
49 | osc = OSCThreadServer(
50 | encoding=options.encoding,
51 | encoding_errors=options.encoding_errors,
52 | default_handler=dump
53 | )
54 | osc.listen(
55 | address=options.host,
56 | port=options.port,
57 | default=True
58 | )
59 | return osc
60 |
61 |
62 | def _dump(options): # pragma: no cover
63 | osc = __dump(options)
64 | try:
65 | while True:
66 | sleep(10)
67 | finally:
68 | osc.stop()
69 |
70 |
71 | def init_parser():
72 | parser = ArgumentParser(description='OSCPy command line interface')
73 | parser.set_defaults(func=lambda *x: parser.print_usage(stderr))
74 |
75 | subparser = parser.add_subparsers()
76 |
77 | send = subparser.add_parser('send', help='send an osc message to a server')
78 | send.set_defaults(func=_send)
79 | send.add_argument('--host', '-H', action='store', default='localhost',
80 | help='host (ip or name) to send message to.')
81 | send.add_argument('--port', '-P', action='store', type=int, default='8000',
82 | help='port to send message to.')
83 | send.add_argument('--encoding', '-e', action='store', default='utf-8',
84 | help='how to encode the strings')
85 | send.add_argument('--encoding_errors', '-E', action='store', default='replace',
86 | help='how to treat string encoding issues')
87 | send.add_argument('--safer', '-s', action='store_true',
88 | help='wait a little after sending message')
89 | send.add_argument('--repeat', '-r', action='store', type=int, default=1,
90 | help='how many times to send the message')
91 |
92 | send.add_argument('address', action='store',
93 | help='OSC address to send the message to.')
94 | send.add_argument('message', nargs='*',
95 | help='content of the message, separated by spaces.')
96 |
97 | dump = subparser.add_parser('dump', help='listen for messages and print them')
98 | dump.set_defaults(func=_dump)
99 | dump.add_argument('--host', '-H', action='store', default='localhost',
100 | help='host (ip or name) to send message to.')
101 | dump.add_argument('--port', '-P', action='store', type=int, default='8000',
102 | help='port to send message to.')
103 | dump.add_argument('--encoding', '-e', action='store', default='utf-8',
104 | help='how to encode the strings')
105 | dump.add_argument('--encoding_errors', '-E', action='store', default='replace',
106 | help='how to treat string encoding issues')
107 |
108 | # bridge = parser.add_parser('bridge', help='listen for messages and redirect them to a server')
109 | return parser
110 |
111 |
112 | def main(): # pragma: no cover
113 | parser = init_parser()
114 | options = parser.parse_args()
115 | exit(options.func(options))
116 |
--------------------------------------------------------------------------------
/server/oscpy/client.py:
--------------------------------------------------------------------------------
1 | """Client API.
2 |
3 | This module provides both a functional and an object oriented API.
4 |
5 | You can use directly `send_message`, `send_bundle` and the `SOCK` socket
6 | that is created by default, or use `OSCClient` to store parameters common
7 | to your requests and avoid repeating them in your code.
8 | """
9 |
10 | import socket
11 | from time import sleep
12 | from sys import platform
13 |
14 | from oscpy.parser import format_message, format_bundle
15 | from oscpy.stats import Stats
16 |
17 | SOCK = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
18 |
19 |
20 | def send_message(
21 | osc_address, values, ip_address, port, sock=SOCK, safer=False,
22 | encoding='', encoding_errors='strict'
23 | ):
24 | """Send an osc message to a socket address.
25 |
26 | - `osc_address` is the osc endpoint to send the data to (e.g b'/test')
27 | it should be a bytestring
28 | - `values` is the list of values to send, they can be any supported osc
29 | type (bytestring, float, int, blob...)
30 | - `ip_address` can either be an ip address if the used socket is of
31 | the AF_INET family, or a filename if the socket is of type AF_UNIX
32 | - `port` value will be ignored if socket is of type AF_UNIX
33 | - `sock` should be a socket object, the client's default socket can be
34 | used as default
35 | - the `safer` parameter allows to wait a little after sending, to make
36 | sure the message is actually sent before doing anything else,
37 | should only be useful in tight loop or cpu-busy code.
38 | - `encoding` if defined, will be used to encode/decode all
39 | strings sent/received to/from unicode/string objects, if left
40 | empty, the interface will only accept bytes and return bytes
41 | to callback functions.
42 | - `encoding_errors` if `encoding` is set, this value will be
43 | used as `errors` parameter in encode/decode calls.
44 |
45 | examples:
46 | send_message(b'/test', [b'hello', 1000, 1.234], 'localhost', 8000)
47 | send_message(b'/test', [], '192.168.0.1', 8000, safer=True)
48 |
49 | sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
50 | send_message(b'/test', [], '192.168.0.1', 8000, sock=sock, safer=True)
51 |
52 | # unix sockets work on linux and osx, and over unix platforms,
53 | # but not windows
54 | sock = socket.socket(socket.AF_UNIX, socket.SOCK_DGRAM)
55 | send_message(b'/some/address', [1, 2, 3], b'/tmp/sock')
56 |
57 | """
58 | if platform != 'win32' and sock.family == socket.AF_UNIX:
59 | address = ip_address
60 | else:
61 | address = (ip_address, port)
62 |
63 | message, stats = format_message(
64 | osc_address, values, encoding=encoding,
65 | encoding_errors=encoding_errors
66 | )
67 |
68 | sock.sendto(message, address)
69 | if safer:
70 | sleep(10e-9)
71 |
72 | return stats
73 |
74 |
75 | def send_bundle(
76 | messages, ip_address, port, timetag=None, sock=None, safer=False,
77 | encoding='', encoding_errors='strict'
78 | ):
79 | """Send a bundle built from the `messages` iterable.
80 |
81 | each item in the `messages` list should be a two-tuple of the form:
82 | (address, values).
83 |
84 | example:
85 | (
86 | ('/create', ['name', 'value']),
87 | ('/select', ['name']),
88 | ('/update', ['name', 'value2']),
89 | ('/delete', ['name']),
90 | )
91 |
92 | `timetag` is optional but can be a float of the number of seconds
93 | since 1970 when the events described in the bundle should happen.
94 |
95 | See `send_message` documentation for the other parameters.
96 | """
97 | if not sock:
98 | sock = SOCK
99 | bundle, stats = format_bundle(
100 | messages, timetag=timetag, encoding=encoding,
101 | encoding_errors=encoding_errors
102 | )
103 | sock.sendto(bundle, (ip_address, port))
104 | if safer:
105 | sleep(10e-9)
106 |
107 | return stats
108 |
109 |
110 | class OSCClient(object):
111 | """Class wrapper for the send_message and send_bundle functions.
112 |
113 | Allows to define `address`, `port` and `sock` parameters for all calls.
114 | If encoding is provided, all string values will be encoded
115 | into this encoding before being sent.
116 | """
117 |
118 | def __init__(
119 | self, address, port, sock=None, encoding='', encoding_errors='strict'
120 | ):
121 | """Create an OSCClient.
122 |
123 | `address` and `port` are the destination of messages sent
124 | by this client. See `send_message` and `send_bundle` documentation
125 | for more information.
126 | """
127 | self.address = address
128 | self.port = port
129 | self.sock = sock or SOCK
130 | self.encoding = encoding
131 | self.encoding_errors = encoding_errors
132 | self.stats = Stats()
133 |
134 | def send_message(self, address, values, safer=False):
135 | """Wrap the module level `send_message` function."""
136 | stats = send_message(
137 | address, values, self.address, self.port, self.sock,
138 | safer=safer, encoding=self.encoding,
139 | encoding_errors=self.encoding_errors
140 | )
141 | self.stats += stats
142 | return stats
143 |
144 | def send_bundle(self, messages, timetag=None, safer=False):
145 | """Wrap the module level `send_bundle` function."""
146 | stats = send_bundle(
147 | messages, self.address, self.port, timetag=timetag,
148 | sock=self.sock, safer=safer, encoding=self.encoding,
149 | encoding_errors=self.encoding_errors
150 | )
151 | self.stats += stats
152 | return stats
153 |
--------------------------------------------------------------------------------
/server/oscpy/parser.py:
--------------------------------------------------------------------------------
1 | """Parse and format data types, from and to packets that can be sent.
2 |
3 | types are automatically inferred using the `PARSERS` and `WRITERS` members.
4 |
5 | Allowed types are:
6 | int (but not *long* ints) -> osc int
7 | floats -> osc float
8 | bytes (encoded strings) -> osc strings
9 | bytearray (raw data) -> osc blob
10 |
11 | """
12 |
13 | __all__ = (
14 | 'parse',
15 | 'read_packet', 'read_message', 'read_bundle',
16 | 'format_bundle', 'format_message',
17 | 'MidiTuple',
18 | )
19 |
20 |
21 | from struct import Struct, pack, unpack_from, calcsize
22 | from time import time
23 | import sys
24 | from collections import Counter, namedtuple
25 | from oscpy.stats import Stats
26 |
27 | if sys.version_info.major > 2: # pragma: no cover
28 | UNICODE = str
29 | izip = zip
30 | else: # pragma: no cover
31 | UNICODE = unicode
32 | from itertools import izip
33 |
34 | INT = Struct('>i')
35 | FLOAT = Struct('>f')
36 | STRING = Struct('>s')
37 | TIME_TAG = Struct('>II')
38 |
39 | TP_PACKET_FORMAT = "!12I"
40 | # 1970-01-01 00:00:00
41 | NTP_DELTA = 2208988800
42 |
43 | NULL = b'\0'
44 | EMPTY = tuple()
45 | INF = float('inf')
46 |
47 | MidiTuple = namedtuple('MidiTuple', 'port_id status_byte data1 data2')
48 |
49 | def padded(l, n=4):
50 | """Return the size to pad a thing to.
51 |
52 | - `l` being the current size of the thing.
53 | - `n` being the desired divisor of the thing's padded size.
54 | """
55 | return n * (min(1, divmod(l, n)[1]) + l // n)
56 |
57 |
58 | def parse_int(value, offset=0, **kwargs):
59 | """Return an int from offset in value."""
60 | return INT.unpack_from(value, offset)[0], INT.size
61 |
62 |
63 | def parse_float(value, offset=0, **kwargs):
64 | """Return a float from offset in value."""
65 | return FLOAT.unpack_from(value, offset)[0], FLOAT.size
66 |
67 |
68 | def parse_string(value, offset=0, encoding='', encoding_errors='strict'):
69 | """Return a string from offset in value.
70 |
71 | If encoding is defined, the string will be decoded. `encoding_errors`
72 | will be used to manage encoding errors in decoding.
73 | """
74 | result = []
75 | count = 0
76 | ss = STRING.size
77 | while True:
78 | c = STRING.unpack_from(value, offset + count)[0]
79 | count += ss
80 |
81 | if c == NULL:
82 | break
83 | result.append(c)
84 |
85 | r = b''.join(result)
86 | if encoding:
87 | return r.decode(encoding, errors=encoding_errors), padded(count)
88 | else:
89 | return r, padded(count)
90 |
91 |
92 | def parse_blob(value, offset=0, **kwargs):
93 | """Return a blob from offset in value."""
94 | size = calcsize('>i')
95 | length = unpack_from('>i', value, offset)[0]
96 | data = unpack_from('>%iQ' % length, value, offset + size)
97 | return data, padded(length, 8)
98 |
99 |
100 | def parse_midi(value, offset=0, **kwargs):
101 | """Return a MIDI tuple from offset in value.
102 | A valid MIDI message: (port id, status byte, data1, data2).
103 | """
104 | val = unpack_from('>I', value, offset)[0]
105 | args = tuple((val & 0xFF << 8 * i) >> 8 * i for i in range(3, -1, -1))
106 | midi = MidiTuple(*args)
107 | return midi, len(midi)
108 |
109 |
110 | def format_midi(value):
111 | return sum((val & 0xFF) << 8 * (3 - pos) for pos, val in enumerate(value))
112 |
113 |
114 | def parse_true(*args, **kwargs):
115 | return True, 0
116 |
117 |
118 | def format_true(value):
119 | return EMPTY
120 |
121 |
122 | def parse_false(*args, **kwargs):
123 | return False, 0
124 |
125 |
126 | def format_false(value):
127 | return EMPTY
128 |
129 |
130 | def parse_nil(*args, **kwargs):
131 | return None, 0
132 |
133 |
134 | def format_nil(value):
135 | return EMPTY
136 |
137 |
138 | def parse_infinitum(*args, **kwargs):
139 | return INF, 0
140 |
141 |
142 | def format_infinitum(value):
143 | return EMPTY
144 |
145 |
146 | PARSERS = {
147 | b'i': parse_int,
148 | b'f': parse_float,
149 | b's': parse_string,
150 | b'S': parse_string,
151 | b'b': parse_blob,
152 | b'm': parse_midi,
153 | b'T': parse_true,
154 | b'F': parse_false,
155 | b'N': parse_nil,
156 | b'I': parse_infinitum,
157 | # b'h' = long, but we use int
158 | b'h': parse_int,
159 | # TODO
160 | # b'h': parse_long,
161 | # b't': parse_timetage,
162 | # b'd': parse_double,
163 | # b'c': parse_char,
164 | # b'r': parse_rgba,
165 | # b'[': parse_array_start,
166 | # b']': parse_array_end,
167 | }
168 |
169 |
170 | PARSERS.update({
171 | ord(k): v
172 | for k, v in PARSERS.items()
173 | })
174 |
175 |
176 | WRITERS = (
177 | (float, (b'f', b'f')),
178 | (int, (b'i', b'i')),
179 | (bytes, (b's', b'%is')),
180 | (UNICODE, (b's', b'%is')),
181 | (bytearray, (b'b', b'%ib')),
182 | (True, (b'T', b'')),
183 | (False, (b'F', b'')),
184 | (None, (b'N', b'')),
185 | (MidiTuple, (b'm', b'I')),
186 | )
187 |
188 |
189 | PADSIZES = {
190 | bytes: 4,
191 | bytearray: 8
192 | }
193 |
194 |
195 | def parse(hint, value, offset=0, encoding='', encoding_errors='strict'):
196 | """Call the correct parser function for the provided hint.
197 |
198 | `hint` will be used to determine the correct parser, other parameters
199 | will be passed to this parser.
200 | """
201 | parser = PARSERS.get(hint)
202 |
203 | if not parser:
204 | raise ValueError(
205 | "no known parser for type hint: {}, value: {}".format(hint, value)
206 | )
207 |
208 | return parser(
209 | value, offset=offset, encoding=encoding,
210 | encoding_errors=encoding_errors
211 | )
212 |
213 |
214 | def format_message(address, values, encoding='', encoding_errors='strict'):
215 | """Create a message."""
216 | tags = [b',']
217 | fmt = []
218 |
219 | encode_cache = {}
220 |
221 | lv = 0
222 | count = Counter()
223 |
224 | for value in values:
225 | lv += 1
226 | cls_or_value, writer = None, None
227 | for cls_or_value, writer in WRITERS:
228 | if (
229 | cls_or_value is value
230 | or isinstance(cls_or_value, type)
231 | and isinstance(value, cls_or_value)
232 | ):
233 | break
234 | else:
235 | raise TypeError(
236 | u'unable to find a writer for value {}, type not in: {}.'
237 | .format(value, [x[0] for x in WRITERS])
238 | )
239 |
240 | if cls_or_value == UNICODE:
241 | if not encoding:
242 | raise TypeError(u"Can't format unicode string without encoding")
243 |
244 | cls_or_value = bytes
245 | value = (
246 | encode_cache[value]
247 | if value in encode_cache else
248 | encode_cache.setdefault(
249 | value, value.encode(encoding, errors=encoding_errors)
250 | )
251 | )
252 |
253 | assert cls_or_value, writer
254 |
255 | tag, v_fmt = writer
256 | if b'%i' in v_fmt:
257 | v_fmt = v_fmt % padded(len(value) + 1, PADSIZES[cls_or_value])
258 |
259 | tags.append(tag)
260 | fmt.append(v_fmt)
261 | count[tag.decode('utf8')] += 1
262 |
263 | fmt = b''.join(fmt)
264 | tags = b''.join(tags + [NULL])
265 |
266 | if encoding and isinstance(address, UNICODE):
267 | address = address.encode(encoding, errors=encoding_errors)
268 |
269 | if not address.endswith(NULL):
270 | address += NULL
271 |
272 | fmt = b'>%is%is%s' % (padded(len(address)), padded(len(tags)), fmt)
273 | message = pack(
274 | fmt,
275 | address,
276 | tags,
277 | *(
278 | (
279 | encode_cache.get(v) + NULL if isinstance(v, UNICODE) and encoding
280 | else (v + NULL) if t in (b's', b'b')
281 | else format_midi(v) if isinstance(v, MidiTuple)
282 | else v
283 | )
284 | for t, v in
285 | izip(tags[1:], values)
286 | )
287 | )
288 | return message, Stats(1, len(message), lv, count)
289 |
290 |
291 | def read_message(data, offset=0, encoding='', encoding_errors='strict'):
292 | """Return address, tags, values, and length of a decoded message.
293 |
294 | Can be called either on a standalone message, or on a message
295 | extracted from a bundle.
296 | """
297 | address, size = parse_string(data, offset=offset)
298 | index = size
299 | if not address.startswith(b'/'):
300 | raise ValueError("address {} doesn't start with a '/'".format(address))
301 |
302 | tags, size = parse_string(data, offset=offset + index)
303 | if not tags.startswith(b','):
304 | raise ValueError("tag string {} doesn't start with a ','".format(tags))
305 | tags = tags[1:]
306 |
307 | index += size
308 |
309 | values = []
310 | for tag in tags:
311 | value, off = parse(
312 | tag, data, offset=offset + index, encoding=encoding,
313 | encoding_errors=encoding_errors
314 | )
315 | values.append(value)
316 | index += off
317 |
318 | return address, tags, values, index
319 |
320 |
321 | def time_to_timetag(value):
322 | """Create a timetag from a time.
323 |
324 | `time` is an unix timestamp (number of seconds since 1/1/1970).
325 | result is the equivalent time using the NTP format.
326 | """
327 | if value is None:
328 | return (0, 1)
329 | seconds, fract = divmod(value, 1)
330 | seconds += NTP_DELTA
331 | seconds = int(seconds)
332 | fract = int(fract * 2**32)
333 | return (seconds, fract)
334 |
335 |
336 | def timetag_to_time(timetag):
337 | """Decode a timetag to a time.
338 |
339 | `timetag` is an NTP formated time.
340 | retult is the equivalent unix timestamp (number of seconds since 1/1/1970).
341 | """
342 | if timetag == (0, 1):
343 | return time()
344 |
345 | seconds, fract = timetag
346 | return seconds + fract / 2. ** 32 - NTP_DELTA
347 |
348 |
349 | def format_bundle(data, timetag=None, encoding='', encoding_errors='strict'):
350 | """Create a bundle from a list of (address, values) tuples.
351 |
352 | String values will be encoded using `encoding` or must be provided
353 | as bytes.
354 | `encoding_errors` will be used to manage encoding errors.
355 | """
356 | timetag = time_to_timetag(timetag)
357 | bundle = [pack('8s', b'#bundle\0')]
358 | bundle.append(TIME_TAG.pack(*timetag))
359 |
360 | stats = Stats()
361 | for address, values in data:
362 | msg, st = format_message(
363 | address, values, encoding='',
364 | encoding_errors=encoding_errors
365 | )
366 | bundle.append(pack('>i', len(msg)))
367 | bundle.append(msg)
368 | stats += st
369 |
370 | return b''.join(bundle), stats
371 |
372 |
373 | def read_bundle(data, encoding='', encoding_errors='strict'):
374 | """Decode a bundle into a (timestamp, messages) tuple."""
375 | length = len(data)
376 |
377 | header = unpack_from('7s', data, 0)[0]
378 | offset = 8 * STRING.size
379 | if header != b'#bundle':
380 | raise ValueError(
381 | "the message doesn't start with '#bundle': {}".format(header))
382 |
383 | timetag = timetag_to_time(TIME_TAG.unpack_from(data, offset))
384 | offset += TIME_TAG.size
385 |
386 | messages = []
387 | while offset < length:
388 | # NOTE, we don't really care about the size of the message, our
389 | # parsing will compute it anyway
390 | # size = Int.unpack_from(data, offset)
391 | offset += INT.size
392 | address, tags, values, off = read_message(
393 | data, offset, encoding=encoding, encoding_errors=encoding_errors
394 | )
395 | offset += off
396 | messages.append((address, tags, values, offset))
397 |
398 | return (timetag, messages)
399 |
400 |
401 | def read_packet(data, drop_late=False, encoding='', encoding_errors='strict'):
402 | """Detect if the data received is a simple message or a bundle, read it.
403 |
404 | Always return a list of messages.
405 | If drop_late is true, and the received data is an expired bundle,
406 | then returns an empty list.
407 | """
408 | header = unpack_from('>c', data, 0)[0]
409 | if header == b'/':
410 | return [
411 | read_message(
412 | data, encoding=encoding,
413 | encoding_errors=encoding_errors
414 | )
415 | ]
416 |
417 | elif header == b'#':
418 | timetag, messages = read_bundle(
419 | data, encoding=encoding, encoding_errors=encoding_errors
420 | )
421 | if drop_late:
422 | if time() > timetag:
423 | return []
424 | return messages
425 | else:
426 | raise ValueError('packet is not a message or a bundle')
427 |
--------------------------------------------------------------------------------
/server/oscpy/stats.py:
--------------------------------------------------------------------------------
1 | "Simple utility class to gather stats about the volumes of data managed"
2 |
3 | from collections import Counter
4 |
5 |
6 | class Stats(object):
7 | def __init__(self, calls=0, bytes=0, params=0, types=None, **kwargs):
8 | self.calls = calls
9 | self.bytes = bytes
10 | self.params = params
11 | self.types = types or Counter()
12 | super(Stats, self).__init__(**kwargs)
13 |
14 | def to_tuple(self):
15 | types = self.types
16 | keys = types.keys()
17 | return (
18 | self.calls,
19 | self.bytes,
20 | self.params,
21 | ''.join(keys),
22 | ) + tuple(types[k] for k in keys)
23 |
24 | def __iadd__(self, other):
25 | assert isinstance(other, Stats)
26 | self.calls += other.calls
27 | self.bytes += other.bytes
28 | self.params += other.params
29 | self.types += other.types
30 | return self
31 |
32 | def __add__(self, other):
33 | assert isinstance(other, Stats)
34 | return Stats(
35 | calls=self.calls + other.calls,
36 | bytes=self.bytes + other.bytes,
37 | params=self.params + other.params,
38 | types=self.types + other.types
39 | )
40 |
41 | def __eq__(self, other):
42 | return other is self or (
43 | isinstance(other, Stats)
44 | and self.calls == self.calls
45 | and self.bytes == self.bytes
46 | and self.params == self.params
47 | and self.types == other.types
48 | )
49 |
50 | def __repr__(self):
51 | return 'Stats:\n' + '\n'.join(
52 | ' {}:{}{}'.format(
53 | k,
54 | '' if isinstance(v, str) and v.startswith('\n') else ' ',
55 | v
56 | )
57 | for k, v in (
58 | ('calls', self.calls),
59 | ('bytes', self.bytes),
60 | ('params', self.params),
61 | (
62 | 'types',
63 | ''.join(
64 | '\n {}: {}'.format(k, self.types[k])
65 | for k in sorted(self.types)
66 | )
67 | )
68 | )
69 | )
70 |
--------------------------------------------------------------------------------
/server/pythonosc/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/maybites/NodeOSC/54287fb5addbd6f69065aef63d7d83eb8ae06671/server/pythonosc/__init__.py
--------------------------------------------------------------------------------
/server/pythonosc/dispatcher.py:
--------------------------------------------------------------------------------
1 | """Class that maps OSC addresses to handlers."""
2 | import collections
3 | import logging
4 | import re
5 | import time
6 | from pythonosc import osc_packet
7 | from typing import overload, List, Union, Any, Generator
8 | from types import FunctionType
9 | from pythonosc.osc_message import OscMessage
10 |
11 |
12 | class Handler(object):
13 | def __init__(self, _callback: FunctionType, _args: Union[Any, List[Any]],
14 | _needs_reply_address: bool = False) -> None:
15 | self.callback = _callback
16 | self.args = _args
17 | self.needs_reply_address = _needs_reply_address
18 |
19 | # needed for test module
20 | def __eq__(self, other) -> bool:
21 | return (type(self) == type(other) and
22 | self.callback == other.callback and
23 | self.args == other.args and
24 | self.needs_reply_address == other.needs_reply_address)
25 |
26 | def invoke(self, client_address: str, message: OscMessage) -> None:
27 | if self.needs_reply_address:
28 | if self.args:
29 | self.callback(client_address, message.address, self.args, *message)
30 | else:
31 | self.callback(client_address, message.address, *message)
32 | else:
33 | if self.args:
34 | self.callback(message.address, self.args, *message)
35 | else:
36 | self.callback(message.address, *message)
37 |
38 |
39 | class Dispatcher(object):
40 | """Register addresses to handlers and can match vice-versa."""
41 |
42 | def __init__(self) -> None:
43 | self._map = collections.defaultdict(list)
44 | self._default_handler = None
45 |
46 | def map(self, address: str, handler: FunctionType, *args: Union[Any, List[Any]],
47 | needs_reply_address: bool = False) -> Handler:
48 | """Map a given address to a handler.
49 |
50 | Args:
51 | - address: An explicit endpoint.
52 | - handler: A function that will be run when the address matches with
53 | the OscMessage passed as parameter.
54 | - args: Any additional arguments that will be always passed to the
55 | handlers after the osc messages arguments if any.
56 | - needs_reply_address: True if the handler function needs the
57 | originating client address passed (as the first argument).
58 | Returns:
59 | - Handler object
60 | """
61 | # TODO: Check the spec:
62 | # http://opensoundcontrol.org/spec-1_0
63 | # regarding multiple mappings
64 | handlerobj = Handler(handler, list(args), needs_reply_address)
65 | self._map[address].append(handlerobj)
66 | return handlerobj
67 |
68 | @overload
69 | def unmap(self, address: str, handler: Handler) -> None:
70 | """Remove an already mapped handler from an address
71 |
72 | Args:
73 | - address: An explicit endpoint.
74 | - handler: A Handler object as returned from map().
75 | """
76 | pass
77 |
78 | @overload
79 | def unmap(self, address: str, handler: FunctionType, *args: Union[Any, List[Any]],
80 | needs_reply_address: bool = False) -> None:
81 | """Remove an already mapped handler from an address
82 |
83 | Args:
84 | - address: An explicit endpoint.
85 | - handler: A function that will be run when the address matches with
86 | the OscMessage passed as parameter.
87 | - args: Any additional arguments that will be always passed to the
88 | handlers after the osc messages arguments if any.
89 | - needs_reply_address: True if the handler function needs the
90 | originating client address passed (as the first argument).
91 | """
92 | pass
93 |
94 | def unmap(self, address, handler, *args, needs_reply_address=False):
95 | try:
96 | if isinstance(handler, Handler):
97 | self._map[address].remove(handler)
98 | else:
99 | self._map[address].remove(Handler(handler, list(args), needs_reply_address))
100 | except ValueError as e:
101 | if str(e) == "list.remove(x): x not in list":
102 | raise ValueError("Address '%s' doesn't have handler '%s' mapped to it" % (address, handler)) from e
103 |
104 | def handlers_for_address(self, address_pattern: str) -> Generator[None, Handler, None]:
105 | """yields Handler namedtuples matching the given OSC pattern."""
106 | # First convert the address_pattern into a matchable regexp.
107 | # '?' in the OSC Address Pattern matches any single character.
108 | # Let's consider numbers and _ "characters" too here, it's not said
109 | # explicitly in the specification but it sounds good.
110 | escaped_address_pattern = re.escape(address_pattern)
111 | pattern = escaped_address_pattern.replace('\\?', '\\w?')
112 | # '*' in the OSC Address Pattern matches any sequence of zero or more
113 | # characters.
114 | pattern = pattern.replace('\\*', '[\w|\+]*')
115 | # The rest of the syntax in the specification is like the re module so
116 | # we're fine.
117 | pattern = pattern + '$'
118 | patterncompiled = re.compile(pattern)
119 | matched = False
120 |
121 | for addr, handlers in self._map.items():
122 | if (patterncompiled.match(addr)
123 | or (('*' in addr) and re.match(addr.replace('*', '[^/]*?/*'), address_pattern))):
124 | yield from handlers
125 | matched = True
126 |
127 | if not matched and self._default_handler:
128 | logging.debug('No handler matched but default handler present, added it.')
129 | yield self._default_handler
130 |
131 | def call_handlers_for_packet(self, data, client_address) -> None:
132 | """
133 | This function calls the handlers registered to the dispatcher for
134 | every message it found in the packet.
135 | The process/thread granularity is thus the OSC packet, not the handler.
136 |
137 | If parameters were registered with the dispatcher, then the handlers are
138 | called this way:
139 | handler('/address that triggered the message',
140 | registered_param_list, osc_msg_arg1, osc_msg_arg2, ...)
141 | if no parameters were registered, then it is just called like this:
142 | handler('/address that triggered the message',
143 | osc_msg_arg1, osc_msg_arg2, osc_msg_param3, ...)
144 | """
145 |
146 | # Get OSC messages from all bundles or standalone message.
147 | try:
148 | packet = osc_packet.OscPacket(data)
149 | for timed_msg in packet.messages:
150 | now = time.time()
151 | handlers = self.handlers_for_address(
152 | timed_msg.message.address)
153 | if not handlers:
154 | continue
155 | # If the message is to be handled later, then so be it.
156 | if timed_msg.time > now:
157 | time.sleep(timed_msg.time - now)
158 | for handler in handlers:
159 | handler.invoke(client_address, timed_msg.message)
160 | except osc_packet.ParseError:
161 | pass
162 |
163 | def set_default_handler(self, handler: FunctionType, needs_reply_address: bool = False) -> None:
164 | """Sets the default handler.
165 |
166 | Must be a function with the same constaints as with the self.map method
167 | or None to unset the default handler.
168 | """
169 | self._default_handler = None if (handler is None) else Handler(handler, [], needs_reply_address)
170 |
--------------------------------------------------------------------------------
/server/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
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("Could not get the date from the datagram: %s" % pe)
37 | # Get the contents as a list of OscBundle and OscMessage.
38 | self._contents = self._parse_contents(index)
39 |
40 | # Return type is actually List[OscBundle], but that would require import annotations from __future__, which is
41 | # python 3.7+ only.
42 | def _parse_contents(self, index: int) -> Any:
43 | contents = []
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 | "Could not identify content type of dgram %s" % content_dgram)
65 | except (osc_types.ParseError, osc_message.ParseError, IndexError) as e:
66 | raise ParseError("Could not parse a content datagram: %s" % e)
67 |
68 | return contents
69 |
70 | @staticmethod
71 | def dgram_is_bundle(dgram: bytes) -> bool:
72 | """Returns whether this datagram starts like an OSC bundle."""
73 | return dgram.startswith(_BUNDLE_PREFIX)
74 |
75 | @property
76 | def timestamp(self) -> int:
77 | """Returns the timestamp associated with this bundle."""
78 | return self._timestamp
79 |
80 | @property
81 | def num_contents(self) -> int:
82 | """Shortcut for len(*bundle) returning the number of elements."""
83 | return len(self._contents)
84 |
85 | @property
86 | def size(self) -> int:
87 | """Returns the length of the datagram for this bundle."""
88 | return len(self._dgram)
89 |
90 | @property
91 | def dgram(self) -> bytes:
92 | """Returns the datagram from which this bundle was built."""
93 | return self._dgram
94 |
95 | def content(self, index) -> Any:
96 | """Returns the bundle's content 0-indexed."""
97 | return self._contents[index]
98 |
99 | def __iter__(self) -> Iterator[Any]:
100 | """Returns an iterator over the bundle's content."""
101 | return iter(self._contents)
102 |
--------------------------------------------------------------------------------
/server/pythonosc/osc_bundle_builder.py:
--------------------------------------------------------------------------------
1 | """Build OSC bundles for client applications."""
2 |
3 | from pythonosc import osc_bundle
4 | from pythonosc import osc_message
5 | from pythonosc.parsing import osc_types
6 |
7 | # Shortcut to specify an immediate execution of messages in the bundle.
8 | IMMEDIATELY = osc_types.IMMEDIATELY
9 |
10 |
11 | class BuildError(Exception):
12 | """Error raised when an error occurs building the bundle."""
13 |
14 |
15 | class OscBundleBuilder(object):
16 | """Builds arbitrary OscBundle instances."""
17 |
18 | def __init__(self, timestamp: int) -> None:
19 | """Build a new bundle with the associated timestamp.
20 |
21 | Args:
22 | - timestamp: system time represented as a floating point number of
23 | seconds since the epoch in UTC or IMMEDIATELY.
24 | """
25 | self._timestamp = timestamp
26 | self._contents = []
27 |
28 | def add_content(self, content: osc_bundle.OscBundle) -> None:
29 | """Add a new content to this bundle.
30 |
31 | Args:
32 | - content: Either an OscBundle or an OscMessage
33 | """
34 | self._contents.append(content)
35 |
36 | def build(self) -> osc_bundle.OscBundle:
37 | """Build an OscBundle with the current state of this builder.
38 |
39 | Raises:
40 | - BuildError: if we could not build the bundle.
41 | """
42 | dgram = b'#bundle\x00'
43 | try:
44 | dgram += osc_types.write_date(self._timestamp)
45 | for content in self._contents:
46 | if (type(content) == osc_message.OscMessage
47 | or type(content) == osc_bundle.OscBundle):
48 | size = content.size
49 | dgram += osc_types.write_int(size)
50 | dgram += content.dgram
51 | else:
52 | raise BuildError(
53 | "Content must be either OscBundle or OscMessage"
54 | "found {}".format(type(content)))
55 | return osc_bundle.OscBundle(dgram)
56 | except osc_types.BuildError as be:
57 | raise BuildError('Could not build the bundle {}'.format(be))
58 |
--------------------------------------------------------------------------------
/server/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 = []
23 | self._parse_datagram()
24 |
25 | def _parse_datagram(self) -> None:
26 | try:
27 | self._address_regexp, index = osc_types.get_string(self._dgram, 0)
28 | if not self._dgram[index:]:
29 | # No params is legit, just return now.
30 | return
31 |
32 | # Get the parameters types.
33 | type_tag, index = osc_types.get_string(self._dgram, index)
34 | if type_tag.startswith(','):
35 | type_tag = type_tag[1:]
36 |
37 | params = []
38 | param_stack = [params]
39 | # Parse each parameter given its type.
40 | for param in type_tag:
41 | if param == "i": # Integer.
42 | val, index = osc_types.get_int(self._dgram, index)
43 | elif param == "h": # long.
44 | val, index = osc_types.get_int(self._dgram, index)
45 | elif param == "f": # Float.
46 | val, index = osc_types.get_float(self._dgram, index)
47 | elif param == "d": # Double.
48 | val, index = osc_types.get_double(self._dgram, index)
49 | elif param == "s": # String.
50 | val, index = osc_types.get_string(self._dgram, index)
51 | elif param == "b": # Blob.
52 | val, index = osc_types.get_blob(self._dgram, index)
53 | elif param == "r": # RGBA.
54 | val, index = osc_types.get_rgba(self._dgram, index)
55 | elif param == "m": # MIDI.
56 | val, index = osc_types.get_midi(self._dgram, index)
57 | elif param == "t": # osc time tag:
58 | val, index = osc_types.get_ttag(self._dgram, index)
59 | elif param == "T": # True.
60 | val = True
61 | elif param == "F": # False.
62 | val = False
63 | elif param == "[": # Array start.
64 | array = []
65 | param_stack[-1].append(array)
66 | param_stack.append(array)
67 | elif param == "]": # Array stop.
68 | if len(param_stack) < 2:
69 | raise ParseError('Unexpected closing bracket in type tag: {0}'.format(type_tag))
70 | param_stack.pop()
71 | # TODO: Support more exotic types as described in the specification.
72 | else:
73 | logging.warning('Unhandled parameter type: {0}'.format(param))
74 | continue
75 | if param not in "[]":
76 | param_stack[-1].append(val)
77 | if len(param_stack) != 1:
78 | raise ParseError('Missing closing bracket in type tag: {0}'.format(type_tag))
79 | self._parameters = params
80 | except osc_types.ParseError as pe:
81 | raise ParseError('Found incorrect datagram, ignoring it', pe)
82 |
83 | @property
84 | def address(self) -> str:
85 | """Returns the OSC address regular expression."""
86 | return self._address_regexp
87 |
88 | @staticmethod
89 | def dgram_is_message(dgram: bytes) -> bool:
90 | """Returns whether this datagram starts as an OSC message."""
91 | return dgram.startswith(b'/')
92 |
93 | @property
94 | def size(self) -> int:
95 | """Returns the length of the datagram for this message."""
96 | return len(self._dgram)
97 |
98 | @property
99 | def dgram(self) -> bytes:
100 | """Returns the datagram from which this message was built."""
101 | return self._dgram
102 |
103 | @property
104 | def params(self) -> List[Any]:
105 | """Convenience method for list(self) to get the list of parameters."""
106 | return list(self)
107 |
108 | def __iter__(self) -> Iterator[float]:
109 | """Returns an iterator over the parameters of this message."""
110 | return iter(self._parameters)
111 |
--------------------------------------------------------------------------------
/server/pythonosc/osc_message_builder.py:
--------------------------------------------------------------------------------
1 | """Build OSC messages for client applications."""
2 |
3 | from pythonosc import osc_message
4 | from pythonosc.parsing import osc_types
5 |
6 | from typing import List, Tuple, Union, Any
7 |
8 | class BuildError(Exception):
9 | """Error raised when an incomplete message is trying to be built."""
10 |
11 |
12 | class OscMessageBuilder(object):
13 | """Builds arbitrary OscMessage instances."""
14 |
15 | ARG_TYPE_FLOAT = "f"
16 | ARG_TYPE_DOUBLE = "d"
17 | ARG_TYPE_INT = "i"
18 | ARG_TYPE_STRING = "s"
19 | ARG_TYPE_BLOB = "b"
20 | ARG_TYPE_RGBA = "r"
21 | ARG_TYPE_MIDI = "m"
22 | ARG_TYPE_TRUE = "T"
23 | ARG_TYPE_FALSE = "F"
24 |
25 | ARG_TYPE_ARRAY_START = "["
26 | ARG_TYPE_ARRAY_STOP = "]"
27 |
28 | _SUPPORTED_ARG_TYPES = (
29 | ARG_TYPE_FLOAT, ARG_TYPE_DOUBLE, ARG_TYPE_INT, ARG_TYPE_BLOB, ARG_TYPE_STRING,
30 | ARG_TYPE_RGBA, ARG_TYPE_MIDI, ARG_TYPE_TRUE, ARG_TYPE_FALSE)
31 |
32 | def __init__(self, address: str=None) -> None:
33 | """Initialize a new builder for a message.
34 |
35 | Args:
36 | - address: The osc address to send this message to.
37 | """
38 | self._address = address
39 | self._args = []
40 |
41 | @property
42 | def address(self) -> str:
43 | """Returns the OSC address this message will be sent to."""
44 | return self._address
45 |
46 | @address.setter
47 | def address(self, value: str) -> None:
48 | """Sets the OSC address this message will be sent to."""
49 | self._address = value
50 |
51 | @property
52 | def args(self) -> List[Tuple[str, Union[str, bytes, bool, int, float, tuple, list]]]: # TODO: Make 'tuple' more specific for it is a MIDI packet
53 | """Returns the (type, value) arguments list of this message."""
54 | return self._args
55 |
56 | def _valid_type(self, arg_type: str) -> bool:
57 | if arg_type in self._SUPPORTED_ARG_TYPES:
58 | return True
59 | elif isinstance(arg_type, list):
60 | for sub_type in arg_type:
61 | if not self._valid_type(sub_type):
62 | return False
63 | return True
64 | return False
65 |
66 | def add_arg(self, arg_value: Union[str, bytes, bool, int, float, tuple, list], arg_type: str=None) -> None: # TODO: Make 'tuple' more specific for it is a MIDI packet
67 | """Add a typed argument to this message.
68 |
69 | Args:
70 | - arg_value: The corresponding value for the argument.
71 | - arg_type: A value in ARG_TYPE_* defined in this class,
72 | if none then the type will be guessed.
73 | Raises:
74 | - ValueError: if the type is not supported.
75 | """
76 | if arg_type and not self._valid_type(arg_type):
77 | raise ValueError(
78 | 'arg_type must be one of {}, or an array of valid types'
79 | .format(self._SUPPORTED_ARG_TYPES))
80 | if not arg_type:
81 | arg_type = self._get_arg_type(arg_value)
82 | if isinstance(arg_type, list):
83 | self._args.append((self.ARG_TYPE_ARRAY_START, None))
84 | for v, t in zip(arg_value, arg_type):
85 | self.add_arg(v, t)
86 | self._args.append((self.ARG_TYPE_ARRAY_STOP, None))
87 | else:
88 | self._args.append((arg_type, arg_value))
89 |
90 | def _get_arg_type(self, arg_value: Union[str, bytes, bool, int, float, tuple, list]) -> str: # TODO: Make 'tuple' more specific for it is a MIDI packet
91 | """Guess the type of a value.
92 |
93 | Args:
94 | - arg_value: The value to guess the type of.
95 | Raises:
96 | - ValueError: if the type is not supported.
97 | """
98 | if isinstance(arg_value, str):
99 | arg_type = self.ARG_TYPE_STRING
100 | elif isinstance(arg_value, bytes):
101 | arg_type = self.ARG_TYPE_BLOB
102 | elif arg_value is True:
103 | arg_type = self.ARG_TYPE_TRUE
104 | elif arg_value is False:
105 | arg_type = self.ARG_TYPE_FALSE
106 | elif isinstance(arg_value, int):
107 | arg_type = self.ARG_TYPE_INT
108 | elif isinstance(arg_value, float):
109 | arg_type = self.ARG_TYPE_FLOAT
110 | elif isinstance(arg_value, tuple) and len(arg_value) == 4:
111 | arg_type = self.ARG_TYPE_MIDI
112 | elif isinstance(arg_value, list):
113 | arg_type = [self._get_arg_type(v) for v in arg_value]
114 | else:
115 | raise ValueError('Infered arg_value type is not supported')
116 | return arg_type
117 |
118 | def build(self) -> osc_message.OscMessage:
119 | """Builds an OscMessage from the current state of this builder.
120 |
121 | Raises:
122 | - BuildError: if the message could not be build or if the address
123 | was empty.
124 |
125 | Returns:
126 | - an osc_message.OscMessage instance.
127 | """
128 | if not self._address:
129 | raise BuildError('OSC addresses cannot be empty')
130 | dgram = b''
131 | try:
132 | # Write the address.
133 | dgram += osc_types.write_string(self._address)
134 | if not self._args:
135 | dgram += osc_types.write_string(',')
136 | return osc_message.OscMessage(dgram)
137 |
138 | # Write the parameters.
139 | arg_types = "".join([arg[0] for arg in self._args])
140 | dgram += osc_types.write_string(',' + arg_types)
141 | for arg_type, value in self._args:
142 | if arg_type == self.ARG_TYPE_STRING:
143 | dgram += osc_types.write_string(value)
144 | elif arg_type == self.ARG_TYPE_INT:
145 | dgram += osc_types.write_int(value)
146 | elif arg_type == self.ARG_TYPE_FLOAT:
147 | dgram += osc_types.write_float(value)
148 | elif arg_type == self.ARG_TYPE_DOUBLE:
149 | dgram += osc_types.write_double(value)
150 | elif arg_type == self.ARG_TYPE_BLOB:
151 | dgram += osc_types.write_blob(value)
152 | elif arg_type == self.ARG_TYPE_RGBA:
153 | dgram += osc_types.write_rgba(value)
154 | elif arg_type == self.ARG_TYPE_MIDI:
155 | dgram += osc_types.write_midi(value)
156 | elif arg_type in (self.ARG_TYPE_TRUE,
157 | self.ARG_TYPE_FALSE,
158 | self.ARG_TYPE_ARRAY_START,
159 | self.ARG_TYPE_ARRAY_STOP):
160 | continue
161 | else:
162 | raise BuildError('Incorrect parameter type found {}'.format(
163 | arg_type))
164 |
165 | return osc_message.OscMessage(dgram)
166 | except osc_types.BuildError as be:
167 | raise BuildError('Could not build the message: {}'.format(be))
168 |
--------------------------------------------------------------------------------
/server/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 calendar
7 | import collections
8 | import time
9 |
10 | from pythonosc.parsing import osc_types
11 | from pythonosc import osc_bundle
12 | from pythonosc import osc_message
13 |
14 | from typing import Union, List
15 |
16 | # A namedtuple as returned my the _timed_msg_of_bundle function.
17 | # 1) the system time at which the message should be executed
18 | # in seconds since the epoch.
19 | # 2) the actual message.
20 | TimedMessage = collections.namedtuple(
21 | typename='TimedMessage',
22 | field_names=('time', 'message'))
23 |
24 |
25 | def _timed_msg_of_bundle(bundle: osc_bundle.OscBundle, now: int) -> List[TimedMessage]:
26 | """Returns messages contained in nested bundles as a list of TimedMessage."""
27 | msgs = []
28 | for content in bundle:
29 | if type(content) == osc_message.OscMessage:
30 | if (bundle.timestamp == osc_types.IMMEDIATELY or bundle.timestamp < now):
31 | msgs.append(TimedMessage(now, content))
32 | else:
33 | msgs.append(TimedMessage(bundle.timestamp, content))
34 | else:
35 | msgs.extend(_timed_msg_of_bundle(content, now))
36 | return msgs
37 |
38 |
39 | class ParseError(Exception):
40 | """Base error thrown when a packet could not be parsed."""
41 |
42 |
43 | class OscPacket(object):
44 | """Unit of transmission of the OSC protocol.
45 |
46 | Any application that sends OSC Packets is an OSC Client.
47 | Any application that receives OSC Packets is an OSC Server.
48 | """
49 |
50 | def __init__(self, dgram: bytes) -> None:
51 | """Initialize an OdpPacket with the given UDP datagram.
52 |
53 | Args:
54 | - dgram: the raw UDP datagram holding the OSC packet.
55 |
56 | Raises:
57 | - ParseError if the datagram could not be parsed.
58 | """
59 | now = calendar.timegm(time.gmtime())
60 | try:
61 | if osc_bundle.OscBundle.dgram_is_bundle(dgram):
62 | self._messages = sorted(
63 | _timed_msg_of_bundle(osc_bundle.OscBundle(dgram), now),
64 | key=lambda x: x.time)
65 | elif osc_message.OscMessage.dgram_is_message(dgram):
66 | self._messages = [TimedMessage(now, osc_message.OscMessage(dgram))]
67 | else:
68 | # Empty packet, should not happen as per the spec but heh, UDP...
69 | raise ParseError(
70 | 'OSC Packet should at least contain an OscMessage or an '
71 | 'OscBundle.')
72 | except (osc_bundle.ParseError, osc_message.ParseError) as pe:
73 | raise ParseError('Could not parse packet %s' % pe)
74 |
75 | @property
76 | def messages(self) -> List[TimedMessage]:
77 | """Returns asc-time-sorted TimedMessages of the messages in this packet."""
78 | return self._messages
79 |
--------------------------------------------------------------------------------
/server/pythonosc/osc_server.py:
--------------------------------------------------------------------------------
1 | """OSC Servers that receive UDP 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 = ForkingOSCUDPServer((ip, port), dispatcher)
9 | server.serve_forever()
10 |
11 | or run the server on its own thread:
12 | server = ForkingOSCUDPServer((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 AsyncIOOSCUDPServer server can be integrated with an
24 | asyncio event loop:
25 |
26 | loop = asyncio.get_event_loop()
27 | server = AsyncIOOSCUDPServer(server_address, dispatcher, loop)
28 | server.serve()
29 | loop.run_forever()
30 |
31 | """
32 |
33 | import asyncio
34 | import os
35 | import socketserver
36 |
37 | from pythonosc import osc_bundle
38 | from pythonosc import osc_message
39 | from pythonosc.dispatcher import Dispatcher
40 |
41 | from asyncio import BaseEventLoop
42 |
43 | from typing import List, Tuple
44 | from types import coroutine
45 |
46 |
47 | class _UDPHandler(socketserver.BaseRequestHandler):
48 | """Handles correct UDP messages for all types of server.
49 |
50 | Whether this will be run on its own thread, the server's or a whole new
51 | process depends on the server you instanciated, look at their documentation.
52 |
53 | This method is called after a basic sanity check was done on the datagram,
54 | basically whether this datagram looks like an osc message or bundle,
55 | if not the server won't even bother to call it and so no new
56 | threads/processes will be spawned.
57 | """
58 |
59 | def handle(self) -> None:
60 | self.server.dispatcher.call_handlers_for_packet(self.request[0], self.client_address)
61 |
62 |
63 | def _is_valid_request(request: List[bytes]) -> bool:
64 | """Returns true if the request's data looks like an osc bundle or message."""
65 | data = request[0]
66 | return (
67 | osc_bundle.OscBundle.dgram_is_bundle(data)
68 | or osc_message.OscMessage.dgram_is_message(data))
69 |
70 |
71 | class OSCUDPServer(socketserver.UDPServer):
72 | """Superclass for different flavors of OSCUDPServer"""
73 |
74 | def __init__(self, server_address: Tuple[str, int], dispatcher: Dispatcher) -> None:
75 | super().__init__(server_address, _UDPHandler)
76 | self._dispatcher = dispatcher
77 |
78 | def verify_request(self, request: List[bytes], client_address: Tuple[str, int]) -> bool:
79 | """Returns true if the data looks like a valid OSC UDP datagram."""
80 | return _is_valid_request(request)
81 |
82 | @property
83 | def dispatcher(self) -> Dispatcher:
84 | """Dispatcher accessor for handlers to dispatch osc messages."""
85 | return self._dispatcher
86 |
87 |
88 | class BlockingOSCUDPServer(OSCUDPServer):
89 | """Blocking version of the UDP server.
90 |
91 | Each message will be handled sequentially on the same thread.
92 | Use this is you don't care about latency in your message handling or don't
93 | have a multiprocess/multithread environment (really?).
94 | """
95 |
96 |
97 | class ThreadingOSCUDPServer(socketserver.ThreadingMixIn, OSCUDPServer):
98 | """Threading version of the OSC UDP server.
99 |
100 | Each message will be handled in its own new thread.
101 | Use this when lightweight operations are done by each message handlers.
102 | """
103 |
104 |
105 | if hasattr(os, "fork"):
106 | class ForkingOSCUDPServer(socketserver.ForkingMixIn, OSCUDPServer):
107 | """Forking version of the OSC UDP server.
108 |
109 | Each message will be handled in its own new process.
110 | Use this when heavyweight operations are done by each message handlers
111 | and forking a whole new process for each of them is worth it.
112 | """
113 |
114 |
115 | class AsyncIOOSCUDPServer():
116 | """Asyncio version of the OSC UDP Server.
117 | Each UDP message is handled by call_handlers_for_packet, the same method as in the
118 | OSCUDPServer family of blocking, threading, and forking servers
119 | """
120 |
121 | def __init__(self, server_address: Tuple[str, int], dispatcher: Dispatcher, loop: BaseEventLoop) -> None:
122 | """
123 | :param server_address: tuple of (IP address to bind to, port)
124 | :param dispatcher: a pythonosc.dispatcher.Dispatcher
125 | :param loop: an asyncio event loop
126 | """
127 |
128 | self._server_address = server_address
129 | self._dispatcher = dispatcher
130 | self._loop = loop
131 |
132 | class _OSCProtocolFactory(asyncio.DatagramProtocol):
133 | """OSC protocol factory which passes datagrams to dispatcher"""
134 |
135 | def __init__(self, dispatcher: Dispatcher) -> None:
136 | self.dispatcher = dispatcher
137 |
138 | def datagram_received(self, data: bytes, client_address: Tuple[str, int]) -> None:
139 | self.dispatcher.call_handlers_for_packet(data, client_address)
140 |
141 | def serve(self) -> None:
142 | """Creates a datagram endpoint and registers it with our event loop.
143 |
144 | Use this only if you are not currently running your asyncio loop.
145 | (i.e. not from within a coroutine).
146 | """
147 | self._loop.run_until_complete(self.create_serve_endpoint())
148 |
149 | def create_serve_endpoint(self) -> coroutine:
150 | """Creates a datagram endpoint and registers it with our event loop as coroutine."""
151 | return self._loop.create_datagram_endpoint(
152 | lambda: self._OSCProtocolFactory(self.dispatcher),
153 | local_addr=self._server_address)
154 |
155 | @property
156 | def dispatcher(self) -> Dispatcher:
157 | return self._dispatcher
158 |
--------------------------------------------------------------------------------
/server/pythonosc/parsing/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/maybites/NodeOSC/54287fb5addbd6f69065aef63d7d83eb8ae06671/server/pythonosc/parsing/__init__.py
--------------------------------------------------------------------------------
/server/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 Union
8 |
9 | # conversion factor for fractional seconds (maximum value of fractional part)
10 | FRACTIONAL_CONVERSION = 2 ** 32
11 |
12 | # 63 zero bits followed by a one in the least signifigant bit is a special
13 | # case meaning "immediately."
14 | IMMEDIATELY = struct.pack('>q', 1)
15 |
16 | # From NTP lib.
17 | _SYSTEM_EPOCH = datetime.date(*time.gmtime(0)[0:3])
18 | _NTP_EPOCH = datetime.date(1900, 1, 1)
19 | # _NTP_DELTA is 2208988800
20 | _NTP_DELTA = (_SYSTEM_EPOCH - _NTP_EPOCH).days * 24 * 3600
21 |
22 |
23 | class NtpError(Exception):
24 | """Base class for ntp module errors."""
25 |
26 |
27 | def ntp_to_system_time(date: Union[int, float]) -> Union[int, float]:
28 | """Convert a NTP time to system time.
29 |
30 | System time is reprensented by seconds since the epoch in UTC.
31 | """
32 | return date - _NTP_DELTA
33 |
34 | def system_time_to_ntp(date: Union[int, float]) -> bytes:
35 | """Convert a system time to NTP time.
36 |
37 | System time is reprensented by seconds since the epoch in UTC.
38 | """
39 | try:
40 | num_secs = int(date)
41 | except ValueError as e:
42 | raise NtpError(e)
43 |
44 | num_secs_ntp = num_secs + _NTP_DELTA
45 |
46 | sec_frac = float(date - num_secs)
47 |
48 | picos = int(sec_frac * FRACTIONAL_CONVERSION)
49 |
50 | return struct.pack('>I', int(num_secs_ntp)) + struct.pack('>I', picos)
51 |
--------------------------------------------------------------------------------
/server/pythonosc/parsing/osc_types.py:
--------------------------------------------------------------------------------
1 | """Functions to get OSC types from datagrams and vice versa"""
2 |
3 | import decimal
4 | import struct
5 |
6 | from pythonosc.parsing import ntp
7 | from datetime import datetime, timedelta, date
8 |
9 | from typing import Union, Tuple
10 |
11 |
12 | class ParseError(Exception):
13 | """Base exception for when a datagram parsing error occurs."""
14 |
15 |
16 | class BuildError(Exception):
17 | """Base exception for when a datagram building error occurs."""
18 |
19 |
20 | # Constant for special ntp datagram sequences that represent an immediate time.
21 | IMMEDIATELY = 0
22 |
23 | # Datagram length in bytes for types that have a fixed size.
24 | _INT_DGRAM_LEN = 4
25 | _FLOAT_DGRAM_LEN = 4
26 | _DOUBLE_DGRAM_LEN = 8
27 | _DATE_DGRAM_LEN = _INT_DGRAM_LEN * 2
28 | # Strings and blob dgram length is always a multiple of 4 bytes.
29 | _STRING_DGRAM_PAD = 4
30 | _BLOB_DGRAM_PAD = 4
31 |
32 |
33 | def write_string(val: str) -> bytes:
34 | """Returns the OSC string equivalent of the given python string.
35 |
36 | Raises:
37 | - BuildError if the string could not be encoded.
38 | """
39 | try:
40 | dgram = val.encode('utf-8') # Default, but better be explicit.
41 | except (UnicodeEncodeError, AttributeError) as e:
42 | raise BuildError('Incorrect string, could not encode {}'.format(e))
43 | diff = _STRING_DGRAM_PAD - (len(dgram) % _STRING_DGRAM_PAD)
44 | dgram += (b'\x00' * diff)
45 | return dgram
46 |
47 |
48 | def get_string(dgram: bytes, start_index: int) -> Tuple[str, int]:
49 | """Get a python string from the datagram, starting at pos start_index.
50 |
51 | According to the specifications, a string is:
52 | "A sequence of non-null ASCII characters followed by a null,
53 | followed by 0-3 additional null characters to make the total number
54 | of bits a multiple of 32".
55 |
56 | Args:
57 | dgram: A datagram packet.
58 | start_index: An index where the string starts in the datagram.
59 |
60 | Returns:
61 | A tuple containing the string and the new end index.
62 |
63 | Raises:
64 | ParseError if the datagram could not be parsed.
65 | """
66 | offset = 0
67 | try:
68 | while dgram[start_index + offset] != 0:
69 | offset += 1
70 | if offset == 0:
71 | raise ParseError(
72 | 'OSC string cannot begin with a null byte: %s' % dgram[start_index:])
73 | # Align to a byte word.
74 | if (offset) % _STRING_DGRAM_PAD == 0:
75 | offset += _STRING_DGRAM_PAD
76 | else:
77 | offset += (-offset % _STRING_DGRAM_PAD)
78 | # Python slices do not raise an IndexError past the last index,
79 | # do it ourselves.
80 | if offset > len(dgram[start_index:]):
81 | raise ParseError('Datagram is too short')
82 | data_str = dgram[start_index:start_index + offset]
83 | return data_str.replace(b'\x00', b'').decode('utf-8'), start_index + offset
84 | except IndexError as ie:
85 | raise ParseError('Could not parse datagram %s' % ie)
86 | except TypeError as te:
87 | raise ParseError('Could not parse datagram %s' % te)
88 |
89 |
90 | def write_int(val: int) -> bytes:
91 | """Returns the datagram for the given integer parameter value
92 |
93 | Raises:
94 | - BuildError if the int could not be converted.
95 | """
96 | try:
97 | return struct.pack('>i', val)
98 | except struct.error as e:
99 | raise BuildError('Wrong argument value passed: {}'.format(e))
100 |
101 |
102 | def get_int(dgram: bytes, start_index: int) -> Tuple[int, int]:
103 | """Get a 32-bit big-endian two's complement integer from the datagram.
104 |
105 | Args:
106 | dgram: A datagram packet.
107 | start_index: An index where the integer starts in the datagram.
108 |
109 | Returns:
110 | A tuple containing the integer and the new end index.
111 |
112 | Raises:
113 | ParseError if the datagram could not be parsed.
114 | """
115 | try:
116 | if len(dgram[start_index:]) < _INT_DGRAM_LEN:
117 | raise ParseError('Datagram is too short')
118 | return (
119 | struct.unpack('>i',
120 | dgram[start_index:start_index + _INT_DGRAM_LEN])[0],
121 | start_index + _INT_DGRAM_LEN)
122 | except (struct.error, TypeError) as e:
123 | raise ParseError('Could not parse datagram %s' % e)
124 |
125 |
126 | def get_ttag(dgram: bytes, start_index: int) -> Tuple[datetime, int]:
127 | """Get a 64-bit OSC time tag from the datagram.
128 |
129 | Args:
130 | dgram: A datagram packet.
131 | start_index: An index where the osc time tag starts in the datagram.
132 |
133 | Returns:
134 | A tuple containing the tuple of time of sending in utc as datetime and the
135 | fraction of the current second and the new end index.
136 |
137 | Raises:
138 | ParseError if the datagram could not be parsed.
139 | """
140 |
141 | _TTAG_DGRAM_LEN = 8
142 |
143 | try:
144 | if len(dgram[start_index:]) < _TTAG_DGRAM_LEN:
145 | raise ParseError('Datagram is too short')
146 |
147 | seconds, idx = get_int(dgram, start_index)
148 | second_decimals, _ = get_int(dgram, idx)
149 |
150 | if seconds < 0:
151 | seconds += ntp.FRACTIONAL_CONVERSION
152 |
153 | if second_decimals < 0:
154 | second_decimals += ntp.FRACTIONAL_CONVERSION
155 |
156 | hours, seconds = seconds // 3600, seconds % 3600
157 | minutes, seconds = seconds // 60, seconds % 60
158 |
159 | utc = datetime.combine(ntp._NTP_EPOCH, datetime.min.time()) + timedelta(hours=hours, minutes=minutes,
160 | seconds=seconds)
161 |
162 | return (utc, second_decimals), start_index + _TTAG_DGRAM_LEN
163 | except (struct.error, TypeError) as e:
164 | raise ParseError('Could not parse datagram %s' % e)
165 |
166 |
167 | def write_float(val: float) -> bytes:
168 | """Returns the datagram for the given float parameter value
169 |
170 | Raises:
171 | - BuildError if the float could not be converted.
172 | """
173 | try:
174 | return struct.pack('>f', val)
175 | except struct.error as e:
176 | raise BuildError('Wrong argument value passed: {}'.format(e))
177 |
178 |
179 | def get_float(dgram: bytes, start_index: int) -> Tuple[float, int]:
180 | """Get a 32-bit big-endian IEEE 754 floating point number from the datagram.
181 |
182 | Args:
183 | dgram: A datagram packet.
184 | start_index: An index where the float starts in the datagram.
185 |
186 | Returns:
187 | A tuple containing the float and the new end index.
188 |
189 | Raises:
190 | ParseError if the datagram could not be parsed.
191 | """
192 | try:
193 | if len(dgram[start_index:]) < _FLOAT_DGRAM_LEN:
194 | # Noticed that Reaktor doesn't send the last bunch of \x00 needed to make
195 | # the float representation complete in some cases, thus we pad here to
196 | # account for that.
197 | dgram = dgram + b'\x00' * (_FLOAT_DGRAM_LEN - len(dgram[start_index:]))
198 | return (
199 | struct.unpack('>f',
200 | dgram[start_index:start_index + _FLOAT_DGRAM_LEN])[0],
201 | start_index + _FLOAT_DGRAM_LEN)
202 | except (struct.error, TypeError) as e:
203 | raise ParseError('Could not parse datagram %s' % e)
204 |
205 |
206 | def write_double(val: float) -> bytes:
207 | """Returns the datagram for the given double parameter value
208 |
209 | Raises:
210 | - BuildError if the double could not be converted.
211 | """
212 | try:
213 | return struct.pack('>d', val)
214 | except struct.error as e:
215 | raise BuildError('Wrong argument value passed: {}'.format(e))
216 |
217 |
218 | def get_double(dgram: bytes, start_index: int) -> Tuple[float, int]:
219 | """Get a 64-bit big-endian IEEE 754 floating point number from the datagram.
220 |
221 | Args:
222 | dgram: A datagram packet.
223 | start_index: An index where the double starts in the datagram.
224 |
225 | Returns:
226 | A tuple containing the double and the new end index.
227 |
228 | Raises:
229 | ParseError if the datagram could not be parsed.
230 | """
231 | try:
232 | if len(dgram[start_index:]) < _DOUBLE_DGRAM_LEN:
233 | raise ParseError('Datagram is too short')
234 | return (
235 | struct.unpack('>d',
236 | dgram[start_index:start_index + _DOUBLE_DGRAM_LEN])[0],
237 | start_index + _DOUBLE_DGRAM_LEN)
238 | except (struct.error, TypeError) as e:
239 | raise ParseError('Could not parse datagram {}'.format(e))
240 |
241 |
242 | def get_blob(dgram: bytes, start_index: int) -> Tuple[bytes, int]:
243 | """ Get a blob from the datagram.
244 |
245 | According to the specifications, a blob is made of
246 | "an int32 size count, followed by that many 8-bit bytes of arbitrary
247 | binary data, followed by 0-3 additional zero bytes to make the total
248 | number of bits a multiple of 32".
249 |
250 | Args:
251 | dgram: A datagram packet.
252 | start_index: An index where the float starts in the datagram.
253 |
254 | Returns:
255 | A tuple containing the blob and the new end index.
256 |
257 | Raises:
258 | ParseError if the datagram could not be parsed.
259 | """
260 | size, int_offset = get_int(dgram, start_index)
261 | # Make the size a multiple of 32 bits.
262 | total_size = size + (-size % _BLOB_DGRAM_PAD)
263 | end_index = int_offset + size
264 | if end_index - start_index > len(dgram[start_index:]):
265 | raise ParseError('Datagram is too short.')
266 | return dgram[int_offset:int_offset + size], int_offset + total_size
267 |
268 |
269 | def write_blob(val: bytes) -> bytes:
270 | """Returns the datagram for the given blob parameter value.
271 |
272 | Raises:
273 | - BuildError if the value was empty or if its size didn't fit an OSC int.
274 | """
275 | if not val:
276 | raise BuildError('Blob value cannot be empty')
277 | dgram = write_int(len(val))
278 | dgram += val
279 | while len(dgram) % _BLOB_DGRAM_PAD != 0:
280 | dgram += b'\x00'
281 | return dgram
282 |
283 |
284 | def get_date(dgram: bytes, start_index: int) -> Tuple[Union[int, float], int]:
285 | """Get a 64-bit big-endian fixed-point time tag as a date from the datagram.
286 |
287 | According to the specifications, a date is represented as is:
288 | "the first 32 bits specify the number of seconds since midnight on
289 | January 1, 1900, and the last 32 bits specify fractional parts of a second
290 | to a precision of about 200 picoseconds".
291 |
292 | Args:
293 | dgram: A datagram packet.
294 | start_index: An index where the date starts in the datagram.
295 |
296 | Returns:
297 | A tuple containing the system date and the new end index.
298 | returns osc_immediately (0) if the corresponding OSC sequence was found.
299 |
300 | Raises:
301 | ParseError if the datagram could not be parsed.
302 | """
303 | # Check for the special case first.
304 | if dgram[start_index:start_index + _DATE_DGRAM_LEN] == ntp.IMMEDIATELY:
305 | return IMMEDIATELY, start_index + _DATE_DGRAM_LEN
306 | if len(dgram[start_index:]) < _DATE_DGRAM_LEN:
307 | raise ParseError('Datagram is too short')
308 | num_secs, start_index = get_int(dgram, start_index)
309 | fraction, start_index = get_int(dgram, start_index)
310 | # Sum seconds and fraction of second:
311 | system_time = num_secs + (fraction / ntp.FRACTIONAL_CONVERSION)
312 |
313 | return ntp.ntp_to_system_time(system_time), start_index
314 |
315 |
316 | def write_date(system_time: Union[int, float]) -> bytes:
317 | if system_time == IMMEDIATELY:
318 | return ntp.IMMEDIATELY
319 |
320 | try:
321 | return ntp.system_time_to_ntp(system_time)
322 | except ntp.NtpError as ntpe:
323 | raise BuildError(ntpe)
324 |
325 |
326 | def write_rgba(val: bytes) -> bytes:
327 | """Returns the datagram for the given rgba32 parameter value
328 |
329 | Raises:
330 | - BuildError if the int could not be converted.
331 | """
332 | try:
333 | return struct.pack('>I', val)
334 | except struct.error as e:
335 | raise BuildError('Wrong argument value passed: {}'.format(e))
336 |
337 |
338 | def get_rgba(dgram: bytes, start_index: int) -> Tuple[bytes, int]:
339 | """Get an rgba32 integer from the datagram.
340 |
341 | Args:
342 | dgram: A datagram packet.
343 | start_index: An index where the integer starts in the datagram.
344 |
345 | Returns:
346 | A tuple containing the integer and the new end index.
347 |
348 | Raises:
349 | ParseError if the datagram could not be parsed.
350 | """
351 | try:
352 | if len(dgram[start_index:]) < _INT_DGRAM_LEN:
353 | raise ParseError('Datagram is too short')
354 | return (
355 | struct.unpack('>I',
356 | dgram[start_index:start_index + _INT_DGRAM_LEN])[0],
357 | start_index + _INT_DGRAM_LEN)
358 | except (struct.error, TypeError) as e:
359 | raise ParseError('Could not parse datagram %s' % e)
360 |
361 |
362 | def write_midi(val: Tuple[Tuple[int, int, int, int], int]) -> bytes:
363 | """Returns the datagram for the given MIDI message parameter value
364 |
365 | A valid MIDI message: (port id, status byte, data1, data2).
366 |
367 | Raises:
368 | - BuildError if the MIDI message could not be converted.
369 |
370 | """
371 | if len(val) != 4:
372 | raise BuildError('MIDI message length is invalid')
373 | try:
374 | value = sum((value & 0xFF) << 8 * (3 - pos) for pos, value in enumerate(val))
375 | return struct.pack('>I', value)
376 | except struct.error as e:
377 | raise BuildError('Wrong argument value passed: {}'.format(e))
378 |
379 |
380 | def get_midi(dgram: bytes, start_index: int) -> Tuple[Tuple[int, int, int, int], int]:
381 | """Get a MIDI message (port id, status byte, data1, data2) from the datagram.
382 |
383 | Args:
384 | dgram: A datagram packet.
385 | start_index: An index where the MIDI message starts in the datagram.
386 |
387 | Returns:
388 | A tuple containing the MIDI message and the new end index.
389 |
390 | Raises:
391 | ParseError if the datagram could not be parsed.
392 | """
393 | try:
394 | if len(dgram[start_index:]) < _INT_DGRAM_LEN:
395 | raise ParseError('Datagram is too short')
396 | val = struct.unpack('>I',
397 | dgram[start_index:start_index + _INT_DGRAM_LEN])[0]
398 | midi_msg = tuple((val & 0xFF << 8 * i) >> 8 * i for i in range(3, -1, -1))
399 | return (midi_msg, start_index + _INT_DGRAM_LEN)
400 | except (struct.error, TypeError) as e:
401 | raise ParseError('Could not parse datagram %s' % e)
402 |
--------------------------------------------------------------------------------
/server/pythonosc/test/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/maybites/NodeOSC/54287fb5addbd6f69065aef63d7d83eb8ae06671/server/pythonosc/test/__init__.py
--------------------------------------------------------------------------------
/server/pythonosc/test/parsing/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/maybites/NodeOSC/54287fb5addbd6f69065aef63d7d83eb8ae06671/server/pythonosc/test/parsing/__init__.py
--------------------------------------------------------------------------------
/server/pythonosc/test/parsing/test_ntp.py:
--------------------------------------------------------------------------------
1 | import unittest
2 |
3 | from pythonosc.parsing import ntp
4 |
5 |
6 | class TestNTP(unittest.TestCase):
7 | """ TODO: Write real tests for this when I get time..."""
8 |
9 | def test_nto_to_system_time(self):
10 | self.assertGreater(0, ntp.ntp_to_system_time(0))
11 |
12 | def test_system_time_to_ntp(self):
13 | self.assertTrue(ntp.system_time_to_ntp(0.0))
14 |
15 |
16 | if __name__ == "__main__":
17 | unittest.main()
18 |
--------------------------------------------------------------------------------
/server/pythonosc/test/parsing/test_osc_types.py:
--------------------------------------------------------------------------------
1 | """Unit tests for the osc_types module."""
2 | import unittest
3 |
4 | from pythonosc.parsing import ntp
5 | from pythonosc.parsing import osc_types
6 |
7 | from datetime import datetime
8 |
9 |
10 | class TestString(unittest.TestCase):
11 | def test_get_string(self):
12 | cases = {
13 | b"A\x00\x00\x00": ("A", 4),
14 | b"AB\x00\x00": ("AB", 4),
15 | b"ABC\x00": ("ABC", 4),
16 | b"ABCD\x00\x00\x00\x00": ("ABCD", 8),
17 |
18 | b"ABCD\x00\x00\x00\x00GARBAGE": ("ABCD", 8),
19 | }
20 |
21 | for dgram, expected in cases.items():
22 | self.assertEqual(expected, osc_types.get_string(dgram, 0))
23 |
24 | def test_get_string_raises_on_wrong_dgram(self):
25 | cases = [
26 | b"\x00\x00\x00\x00",
27 | b'blablaba',
28 | b'',
29 | b'\x00',
30 | True,
31 | ]
32 |
33 | for case in cases:
34 | self.assertRaises(
35 | osc_types.ParseError, osc_types.get_string, case, 0)
36 |
37 | def test_get_string_raises_when_datagram_too_short(self):
38 | self.assertRaises(
39 | osc_types.ParseError, osc_types.get_string, b'abc\x00', 1)
40 |
41 | def test_get_string_raises_on_wrong_start_index_negative(self):
42 | self.assertRaises(
43 | osc_types.ParseError, osc_types.get_string, b'abc\x00', -1)
44 |
45 |
46 | class TestInteger(unittest.TestCase):
47 | def test_get_integer(self):
48 | cases = {
49 | b"\x00\x00\x00\x00": (0, 4),
50 | b"\x00\x00\x00\x01": (1, 4),
51 | b"\x00\x00\x00\x02": (2, 4),
52 | b"\x00\x00\x00\x03": (3, 4),
53 |
54 | b"\x00\x00\x01\x00": (256, 4),
55 | b"\x00\x01\x00\x00": (65536, 4),
56 | b"\x01\x00\x00\x00": (16777216, 4),
57 |
58 | b"\x00\x00\x00\x01GARBAGE": (1, 4),
59 | }
60 |
61 | for dgram, expected in cases.items():
62 | self.assertEqual(
63 | expected, osc_types.get_int(dgram, 0))
64 |
65 | def test_get_integer_raises_on_type_error(self):
66 | cases = [b'', True]
67 |
68 | for case in cases:
69 | self.assertRaises(osc_types.ParseError, osc_types.get_int, case, 0)
70 |
71 | def test_get_integer_raises_on_wrong_start_index(self):
72 | self.assertRaises(
73 | osc_types.ParseError, osc_types.get_int, b'\x00\x00\x00\x11', 1)
74 |
75 | def test_get_integer_raises_on_wrong_start_index_negative(self):
76 | self.assertRaises(
77 | osc_types.ParseError, osc_types.get_int, b'\x00\x00\x00\x00', -1)
78 |
79 | def test_datagram_too_short(self):
80 | dgram = b'\x00' * 3
81 | self.assertRaises(osc_types.ParseError, osc_types.get_int, dgram, 2)
82 |
83 |
84 | class TestRGBA(unittest.TestCase):
85 | def test_get_rgba(self):
86 | cases = {
87 | b"\x00\x00\x00\x00": (0, 4),
88 | b"\x00\x00\x00\x01": (1, 4),
89 | b"\x00\x00\x00\x02": (2, 4),
90 | b"\x00\x00\x00\x03": (3, 4),
91 |
92 | b"\xFF\x00\x00\x00": (4278190080, 4),
93 | b"\x00\xFF\x00\x00": (16711680, 4),
94 | b"\x00\x00\xFF\x00": (65280, 4),
95 | b"\x00\x00\x00\xFF": (255, 4),
96 |
97 | b"\x00\x00\x00\x01GARBAGE": (1, 4),
98 | }
99 |
100 | for dgram, expected in cases.items():
101 | self.assertEqual(
102 | expected, osc_types.get_rgba(dgram, 0))
103 |
104 | def test_get_rgba_raises_on_type_error(self):
105 | cases = [b'', True]
106 |
107 | for case in cases:
108 | self.assertRaises(osc_types.ParseError, osc_types.get_rgba, case, 0)
109 |
110 | def test_get_rgba_raises_on_wrong_start_index(self):
111 | self.assertRaises(
112 | osc_types.ParseError, osc_types.get_rgba, b'\x00\x00\x00\x11', 1)
113 |
114 | def test_get_rgba_raises_on_wrong_start_index_negative(self):
115 | self.assertRaises(
116 | osc_types.ParseError, osc_types.get_rgba, b'\x00\x00\x00\x00', -1)
117 |
118 | def test_datagram_too_short(self):
119 | dgram = b'\x00' * 3
120 | self.assertRaises(osc_types.ParseError, osc_types.get_rgba, dgram, 2)
121 |
122 |
123 | class TestMidi(unittest.TestCase):
124 | def test_get_midi(self):
125 | cases = {
126 | b"\x00\x00\x00\x00": ((0, 0, 0, 0), 4),
127 | b"\x00\x00\x00\x02": ((0, 0, 0, 1), 4),
128 | b"\x00\x00\x00\x02": ((0, 0, 0, 2), 4),
129 | b"\x00\x00\x00\x03": ((0, 0, 0, 3), 4),
130 |
131 | b"\x00\x00\x01\x00": ((0, 0, 1, 0), 4),
132 | b"\x00\x01\x00\x00": ((0, 1, 0, 0), 4),
133 | b"\x01\x00\x00\x00": ((1, 0, 0, 0), 4),
134 |
135 | b"\x00\x00\x00\x01GARBAGE": ((0, 0, 0, 1), 4),
136 | }
137 |
138 | for dgram, expected in cases.items():
139 | self.assertEqual(
140 | expected, osc_types.get_midi(dgram, 0))
141 |
142 | def test_get_midi_raises_on_type_error(self):
143 | cases = [b'', True]
144 |
145 | for case in cases:
146 | self.assertRaises(osc_types.ParseError, osc_types.get_midi, case, 0)
147 |
148 | def test_get_midi_raises_on_wrong_start_index(self):
149 | self.assertRaises(
150 | osc_types.ParseError, osc_types.get_midi, b'\x00\x00\x00\x11', 1)
151 |
152 | def test_get_midi_raises_on_wrong_start_index_negative(self):
153 | self.assertRaises(
154 | osc_types.ParseError, osc_types.get_midi, b'\x00\x00\x00\x00', -1)
155 |
156 | def test_datagram_too_short(self):
157 | dgram = b'\x00' * 3
158 | self.assertRaises(osc_types.ParseError, osc_types.get_midi, dgram, 2)
159 |
160 |
161 | class TestDate(unittest.TestCase):
162 | def test_get_ttag(self):
163 | cases = {
164 | b"\xde\x9c\x91\xbf\x00\x01\x00\x00": ((datetime(2018, 5, 8, 21, 14, 39), 65536), 8),
165 | b"\x00\x00\x00\x00\x00\x00\x00\x00": ((datetime(1900, 1, 1, 0, 0, 0), 0), 8),
166 | b"\x83\xaa\x7E\x80\x0A\x00\xB0\x0C": ((datetime(1970, 1, 1, 0, 0, 0), 167817228), 8)
167 | }
168 |
169 | for dgram, expected in cases.items():
170 | self.assertEqual(expected, osc_types.get_ttag(dgram, 0))
171 |
172 | def test_get_ttag_raises_on_wrong_start_index_negative(self):
173 | self.assertRaises(
174 | osc_types.ParseError, osc_types.get_ttag, b'\x00\x00\x00\x00\x00\x00\x00\x00', -1)
175 |
176 | def test_get_ttag_raises_on_type_error(self):
177 | cases = [b'', True]
178 |
179 | for case in cases:
180 | self.assertRaises(osc_types.ParseError, osc_types.get_ttag, case, 0)
181 |
182 | def test_get_ttag_raises_on_wrong_start_index(self):
183 | self.assertRaises(
184 | osc_types.ParseError, osc_types.get_date, b'\x00\x00\x00\x11\x00\x00\x00\x11', 1)
185 |
186 | def test_ttag_datagram_too_short(self):
187 | dgram = b'\x00' * 7
188 | self.assertRaises(osc_types.ParseError, osc_types.get_ttag, dgram, 6)
189 |
190 | dgram = b'\x00' * 2
191 | self.assertRaises(osc_types.ParseError, osc_types.get_ttag, dgram, 1)
192 |
193 | dgram = b'\x00' * 5
194 | self.assertRaises(osc_types.ParseError, osc_types.get_ttag, dgram, 4)
195 |
196 | dgram = b'\x00' * 1
197 | self.assertRaises(osc_types.ParseError, osc_types.get_ttag, dgram, 0)
198 |
199 |
200 | class TestFloat(unittest.TestCase):
201 | def test_get_float(self):
202 | cases = {
203 | b"\x00\x00\x00\x00": (0.0, 4),
204 | b"?\x80\x00\x00'": (1.0, 4),
205 | b'@\x00\x00\x00': (2.0, 4),
206 |
207 | b"\x00\x00\x00\x00GARBAGE": (0.0, 4),
208 | }
209 |
210 | for dgram, expected in cases.items():
211 | self.assertAlmostEqual(expected, osc_types.get_float(dgram, 0))
212 |
213 | def test_get_float_raises_on_wrong_dgram(self):
214 | cases = [True]
215 |
216 | for case in cases:
217 | self.assertRaises(osc_types.ParseError, osc_types.get_float, case, 0)
218 |
219 | def test_get_float_raises_on_type_error(self):
220 | cases = [None]
221 |
222 | for case in cases:
223 | self.assertRaises(osc_types.ParseError, osc_types.get_float, case, 0)
224 |
225 | def test_datagram_too_short_pads(self):
226 | dgram = b'\x00' * 2
227 | self.assertEqual((0, 4), osc_types.get_float(dgram, 0))
228 |
229 |
230 | class TestDouble(unittest.TestCase):
231 | def test_get_double(self):
232 | cases = {
233 | b'\x00\x00\x00\x00\x00\x00\x00\x00': (0.0, 8),
234 | b'?\xf0\x00\x00\x00\x00\x00\x00': (1.0, 8),
235 | b'@\x00\x00\x00\x00\x00\x00\x00': (2.0, 8),
236 | b'\xbf\xf0\x00\x00\x00\x00\x00\x00': (-1.0, 8),
237 | b'\xc0\x00\x00\x00\x00\x00\x00\x00': (-2.0, 8),
238 |
239 | b"\x00\x00\x00\x00\x00\x00\x00\x00GARBAGE": (0.0, 8),
240 | }
241 |
242 | for dgram, expected in cases.items():
243 | self.assertAlmostEqual(expected, osc_types.get_double(dgram, 0))
244 |
245 | def test_get_double_raises_on_wrong_dgram(self):
246 | cases = [True]
247 |
248 | for case in cases:
249 | self.assertRaises(osc_types.ParseError, osc_types.get_double, case, 0)
250 |
251 | def test_get_double_raises_on_type_error(self):
252 | cases = [None]
253 |
254 | for case in cases:
255 | self.assertRaises(osc_types.ParseError, osc_types.get_double, case, 0)
256 |
257 | def test_datagram_too_short_pads(self):
258 | dgram = b'\x00' * 2
259 | self.assertRaises(osc_types.ParseError, osc_types.get_double, dgram, 0)
260 |
261 |
262 | class TestBlob(unittest.TestCase):
263 | def test_get_blob(self):
264 | cases = {
265 | b"\x00\x00\x00\x00": (b"", 4),
266 | b"\x00\x00\x00\x08stuff\x00\x00\x00": (b"stuff\x00\x00\x00", 12),
267 | b"\x00\x00\x00\x04\x00\x00\x00\x00": (b"\x00\x00\x00\x00", 8),
268 | b"\x00\x00\x00\x02\x00\x00\x00\x00": (b"\x00\x00", 8),
269 |
270 | b"\x00\x00\x00\x08stuff\x00\x00\x00datagramcontinues": (
271 | b"stuff\x00\x00\x00", 12),
272 | }
273 |
274 | for dgram, expected in cases.items():
275 | self.assertEqual(expected, osc_types.get_blob(dgram, 0))
276 |
277 | def test_get_blob_raises_on_wrong_dgram(self):
278 | cases = [b'', True, b"\x00\x00\x00\x08"]
279 |
280 | for case in cases:
281 | self.assertRaises(osc_types.ParseError, osc_types.get_blob, case, 0)
282 |
283 | def test_get_blob_raises_on_wrong_start_index(self):
284 | self.assertRaises(
285 | osc_types.ParseError, osc_types.get_blob, b'\x00\x00\x00\x11', 1)
286 |
287 | def test_get_blob_raises_too_short_buffer(self):
288 | self.assertRaises(
289 | osc_types.ParseError,
290 | osc_types.get_blob,
291 | b'\x00\x00\x00\x11\x00\x00', 1)
292 |
293 | def test_get_blog_raises_on_wrong_start_index_negative(self):
294 | self.assertRaises(
295 | osc_types.ParseError, osc_types.get_blob, b'\x00\x00\x00\x00', -1)
296 |
297 |
298 | class TestNTPTimestamp(unittest.TestCase):
299 | def test_immediately_dgram(self):
300 | dgram = ntp.IMMEDIATELY
301 | self.assertEqual(osc_types.IMMEDIATELY, osc_types.get_date(dgram, 0)[0])
302 |
303 | def test_origin_of_time(self):
304 | dgram = b'\x00' * 8
305 | self.assertGreater(0, osc_types.get_date(dgram, 0)[0])
306 |
307 | def test_datagram_too_short(self):
308 | dgram = b'\x00' * 8
309 | self.assertRaises(osc_types.ParseError, osc_types.get_date, dgram, 2)
310 |
311 | def test_write_date(self):
312 | self.assertEqual(b'\x83\xaa~\x83\":)\xc7', osc_types.write_date(3.1337))
313 |
314 |
315 | class TestBuildMethods(unittest.TestCase):
316 | def test_string(self):
317 | self.assertEqual(b'\x00\x00\x00\x00', osc_types.write_string(''))
318 | self.assertEqual(b'A\x00\x00\x00', osc_types.write_string('A'))
319 | self.assertEqual(b'AB\x00\x00', osc_types.write_string('AB'))
320 | self.assertEqual(b'ABC\x00', osc_types.write_string('ABC'))
321 | self.assertEqual(b'ABCD\x00\x00\x00\x00', osc_types.write_string('ABCD'))
322 |
323 | def test_string_raises(self):
324 | self.assertRaises(osc_types.BuildError, osc_types.write_string, 123)
325 |
326 | def test_int(self):
327 | self.assertEqual(b'\x00\x00\x00\x00', osc_types.write_int(0))
328 | self.assertEqual(b'\x00\x00\x00\x01', osc_types.write_int(1))
329 |
330 | def test_int_raises(self):
331 | self.assertRaises(osc_types.BuildError, osc_types.write_int, 'no int')
332 |
333 | def test_float(self):
334 | self.assertEqual(b'\x00\x00\x00\x00', osc_types.write_float(0.0))
335 | self.assertEqual(b'?\x00\x00\x00', osc_types.write_float(0.5))
336 | self.assertEqual(b'?\x80\x00\x00', osc_types.write_float(1.0))
337 | self.assertEqual(b'?\x80\x00\x00', osc_types.write_float(1))
338 |
339 | def test_float_raises(self):
340 | self.assertRaises(osc_types.BuildError, osc_types.write_float, 'no float')
341 |
342 | def test_blob(self):
343 | self.assertEqual(
344 | b'\x00\x00\x00\x02\x00\x01\x00\x00',
345 | osc_types.write_blob(b'\x00\x01'))
346 | self.assertEqual(
347 | b'\x00\x00\x00\x04\x00\x01\x02\x03',
348 | osc_types.write_blob(b'\x00\x01\x02\x03'))
349 |
350 | def test_blob_raises(self):
351 | self.assertRaises(osc_types.BuildError, osc_types.write_blob, b'')
352 |
353 |
354 | if __name__ == "__main__":
355 | unittest.main()
356 |
--------------------------------------------------------------------------------
/server/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([], self.dispatcher.handlers_for_address('/test'))
19 |
20 | def test_use_default_handler_when_set_and_no_match(self):
21 | handler = object()
22 | self.dispatcher.set_default_handler(handler)
23 |
24 | self.sortAndAssertSequenceEqual([Handler(handler, [])], self.dispatcher.handlers_for_address('/test'))
25 |
26 | def test_simple_map_and_match(self):
27 | handler = object()
28 | self.dispatcher.map('/test', handler, 1, 2, 3)
29 | self.dispatcher.map('/test2', handler)
30 | self.sortAndAssertSequenceEqual(
31 | [Handler(handler, [1, 2, 3])], self.dispatcher.handlers_for_address('/test'))
32 | self.sortAndAssertSequenceEqual(
33 | [Handler(handler, [])], self.dispatcher.handlers_for_address('/test2'))
34 |
35 | def test_example_from_spec(self):
36 | addresses = [
37 | "/first/this/one",
38 | "/second/1",
39 | "/second/2",
40 | "/third/a",
41 | "/third/b",
42 | "/third/c",
43 | ]
44 | for index, address in enumerate(addresses):
45 | self.dispatcher.map(address, index)
46 |
47 | for index, address in enumerate(addresses):
48 | self.sortAndAssertSequenceEqual(
49 | [Handler(index, [])], self.dispatcher.handlers_for_address(address))
50 |
51 | self.sortAndAssertSequenceEqual(
52 | [Handler(1, []), Handler(2, [])], self.dispatcher.handlers_for_address("/second/?"))
53 |
54 | self.sortAndAssertSequenceEqual(
55 | [Handler(3, []), Handler(4, []), Handler(5, [])],
56 | self.dispatcher.handlers_for_address("/third/*"))
57 |
58 | def test_do_not_match_over_slash(self):
59 | self.dispatcher.map('/foo/bar/1', 1)
60 | self.dispatcher.map('/foo/bar/2', 2)
61 |
62 | self.sortAndAssertSequenceEqual(
63 | [], self.dispatcher.handlers_for_address("/*"))
64 |
65 | def test_match_middle_star(self):
66 | self.dispatcher.map('/foo/bar/1', 1)
67 | self.dispatcher.map('/foo/bar/2', 2)
68 |
69 | self.sortAndAssertSequenceEqual(
70 | [Handler(2, [])], self.dispatcher.handlers_for_address("/foo/*/2"))
71 |
72 | def test_match_multiple_stars(self):
73 | self.dispatcher.map('/foo/bar/1', 1)
74 | self.dispatcher.map('/foo/bar/2', 2)
75 |
76 | self.sortAndAssertSequenceEqual(
77 | [Handler(1, []), Handler(2, [])], self.dispatcher.handlers_for_address("/*/*/*"))
78 |
79 | def test_match_address_contains_plus_as_character(self):
80 | self.dispatcher.map('/footest/bar+tender/1', 1)
81 |
82 | self.sortAndAssertSequenceEqual(
83 | [Handler(1, [])], self.dispatcher.handlers_for_address("/foo*/bar+*/*"))
84 | self.sortAndAssertSequenceEqual(
85 | [Handler(1, [])], self.dispatcher.handlers_for_address("/foo*/bar*/*"))
86 |
87 | def test_call_correct_dispatcher_on_star(self):
88 | self.dispatcher.map('/a+b', 1)
89 | self.dispatcher.map('/aaab', 2)
90 | self.sortAndAssertSequenceEqual(
91 | [Handler(2, [])], self.dispatcher.handlers_for_address('/aaab'))
92 | self.sortAndAssertSequenceEqual(
93 | [Handler(1, [])], self.dispatcher.handlers_for_address('/a+b'))
94 |
95 | def test_map_star(self):
96 | self.dispatcher.map('/starbase/*', 1)
97 | self.sortAndAssertSequenceEqual(
98 | [Handler(1, [])], self.dispatcher.handlers_for_address("/starbase/bar"))
99 |
100 | def test_map_root_star(self):
101 | self.dispatcher.map('/*', 1)
102 | self.sortAndAssertSequenceEqual(
103 | [Handler(1, [])], self.dispatcher.handlers_for_address("/anything/matches"))
104 |
105 | def test_map_double_stars(self):
106 | self.dispatcher.map('/foo/*/bar/*', 1)
107 | self.sortAndAssertSequenceEqual(
108 | [Handler(1, [])], self.dispatcher.handlers_for_address("/foo/wild/bar/wild"))
109 | self.sortAndAssertSequenceEqual(
110 | [], self.dispatcher.handlers_for_address("/foo/wild/nomatch/wild"))
111 |
112 | def test_multiple_handlers(self):
113 | self.dispatcher.map('/foo/bar', 1)
114 | self.dispatcher.map('/foo/bar', 2)
115 | self.sortAndAssertSequenceEqual(
116 | [Handler(1, []), Handler(2, [])], self.dispatcher.handlers_for_address("/foo/bar"))
117 |
118 | def test_multiple_handlers_with_wildcard_map(self):
119 | self.dispatcher.map('/foo/bar', 1)
120 | self.dispatcher.map('/*', 2)
121 | self.sortAndAssertSequenceEqual(
122 | [Handler(1, []), Handler(2, [])], self.dispatcher.handlers_for_address("/foo/bar"))
123 |
124 | def test_unmap(self):
125 | def dummyhandler():
126 | pass
127 |
128 | # Test with handler returned by map
129 | returnedhandler = self.dispatcher.map("/map/me", dummyhandler)
130 | self.sortAndAssertSequenceEqual([Handler(dummyhandler, [])], self.dispatcher.handlers_for_address("/map/me"))
131 | self.dispatcher.unmap("/map/me", returnedhandler)
132 | self.sortAndAssertSequenceEqual([], self.dispatcher.handlers_for_address("/map/me"))
133 |
134 | # Test with reconstructing handler
135 | self.dispatcher.map("/map/me/too", dummyhandler)
136 | self.sortAndAssertSequenceEqual([Handler(dummyhandler, [])],
137 | self.dispatcher.handlers_for_address("/map/me/too"))
138 | self.dispatcher.unmap("/map/me/too", dummyhandler)
139 | self.sortAndAssertSequenceEqual([], self.dispatcher.handlers_for_address("/map/me/too"))
140 |
141 | def test_unmap_exception(self):
142 | def dummyhandler():
143 | pass
144 |
145 | with self.assertRaises(ValueError) as context:
146 | self.dispatcher.unmap("/unmap/exception", dummyhandler)
147 |
148 | handlerobj = self.dispatcher.map("/unmap/somethingelse", dummyhandler())
149 | with self.assertRaises(ValueError) as context:
150 | self.dispatcher.unmap("/unmap/exception", handlerobj)
151 |
152 |
153 | if __name__ == "__main__":
154 | unittest.main()
155 |
--------------------------------------------------------------------------------
/server/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 | _DGRAM_SWITCH_GOES_OFF = (
16 | b"#bundle\x00"
17 | b"\x00\x00\x00\x00\x00\x00\x00\x01"
18 | b"\x00\x00\x00\x10"
19 | b"/SYNC\x00\x00\x00"
20 | b",f\x00\x00"
21 | b"\x00\x00\x00\x00")
22 |
23 | _DGRAM_SWITCH_GOES_ON = (
24 | b"#bundle\x00"
25 | b"\x00\x00\x00\x00\x00\x00\x00\x01"
26 | b"\x00\x00\x00\x10"
27 | b"/SYNC\x00\x00\x00"
28 | b",f\x00\x00"
29 | b"?\x00\x00\x00")
30 |
31 | _DGRAM_TWO_MESSAGES_IN_BUNDLE = (
32 | b"#bundle\x00"
33 | b"\x00\x00\x00\x00\x00\x00\x00\x01"
34 | # First message.
35 | b"\x00\x00\x00\x10"
36 | b"/SYNC\x00\x00\x00"
37 | b",f\x00\x00"
38 | b"?\x00\x00\x00"
39 | # Second message, same.
40 | b"\x00\x00\x00\x10"
41 | b"/SYNC\x00\x00\x00"
42 | b",f\x00\x00"
43 | b"?\x00\x00\x00")
44 |
45 | _DGRAM_EMPTY_BUNDLE = (
46 | b"#bundle\x00"
47 | b"\x00\x00\x00\x00\x00\x00\x00\x01")
48 |
49 | _DGRAM_BUNDLE_IN_BUNDLE = (
50 | b"#bundle\x00"
51 | b"\x00\x00\x00\x00\x00\x00\x00\x01"
52 | b"\x00\x00\x00(" # length of sub bundle: 40 bytes.
53 | b"#bundle\x00"
54 | b"\x00\x00\x00\x00\x00\x00\x00\x01"
55 | b"\x00\x00\x00\x10"
56 | b"/SYNC\x00\x00\x00"
57 | b",f\x00\x00"
58 | b"?\x00\x00\x00")
59 |
60 | _DGRAM_INVALID = (
61 | b"#bundle\x00"
62 | b"\x00\x00\x00")
63 |
64 | _DGRAM_INVALID_INDEX = (
65 | b"#bundle\x00"
66 | b"\x00\x00\x00\x00\x00\x00\x00\x01"
67 | b"\x00\x00\x00\x20"
68 | b"/SYNC\x00\x00\x00\x00")
69 |
70 | _DGRAM_UNKNOWN_TYPE = (
71 | b"#bundle\x00"
72 | b"\x00\x00\x00\x00\x00\x00\x00\x01"
73 | b"\x00\x00\x00\x10"
74 | b"iamnotaslash")
75 |
76 |
77 | class TestOscBundle(unittest.TestCase):
78 | def test_switch_goes_off(self):
79 | bundle = osc_bundle.OscBundle(_DGRAM_SWITCH_GOES_OFF)
80 | self.assertEqual(1, bundle.num_contents)
81 | self.assertEqual(len(_DGRAM_SWITCH_GOES_OFF), bundle.size)
82 | self.assertEqual(osc_types.IMMEDIATELY, bundle.timestamp)
83 |
84 | def test_switch_goes_on(self):
85 | bundle = osc_bundle.OscBundle(_DGRAM_SWITCH_GOES_ON)
86 | self.assertEqual(1, bundle.num_contents)
87 | self.assertEqual(len(_DGRAM_SWITCH_GOES_ON), bundle.size)
88 | self.assertEqual(osc_types.IMMEDIATELY, bundle.timestamp)
89 |
90 | def test_datagram_length(self):
91 | bundle = osc_bundle.OscBundle(_DGRAM_KNOB_ROTATES_BUNDLE)
92 | self.assertEqual(1, bundle.num_contents)
93 | self.assertEqual(len(_DGRAM_KNOB_ROTATES_BUNDLE), bundle.size)
94 | self.assertEqual(osc_types.IMMEDIATELY, bundle.timestamp)
95 |
96 | def test_two_messages_in_bundle(self):
97 | bundle = osc_bundle.OscBundle(_DGRAM_TWO_MESSAGES_IN_BUNDLE)
98 | self.assertEqual(2, bundle.num_contents)
99 | self.assertEqual(osc_types.IMMEDIATELY, bundle.timestamp)
100 | for content in bundle:
101 | self.assertEqual(osc_message.OscMessage, type(content))
102 |
103 | def test_empty_bundle(self):
104 | bundle = osc_bundle.OscBundle(_DGRAM_EMPTY_BUNDLE)
105 | self.assertEqual(0, bundle.num_contents)
106 | self.assertEqual(osc_types.IMMEDIATELY, bundle.timestamp)
107 |
108 | def test_bundle_in_bundle_we_must_go_deeper(self):
109 | bundle = osc_bundle.OscBundle(_DGRAM_BUNDLE_IN_BUNDLE)
110 | self.assertEqual(1, bundle.num_contents)
111 | self.assertEqual(osc_types.IMMEDIATELY, bundle.timestamp)
112 | self.assertEqual(osc_bundle.OscBundle, type(bundle.content(0)))
113 |
114 | def test_dgram_is_bundle(self):
115 | self.assertTrue(osc_bundle.OscBundle.dgram_is_bundle(
116 | _DGRAM_SWITCH_GOES_ON))
117 | self.assertFalse(osc_bundle.OscBundle.dgram_is_bundle(b'junk'))
118 |
119 | def test_raises_on_invalid_datagram(self):
120 | self.assertRaises(
121 | osc_bundle.ParseError, osc_bundle.OscBundle, _DGRAM_INVALID)
122 | self.assertRaises(
123 | osc_bundle.ParseError, osc_bundle.OscBundle, _DGRAM_INVALID_INDEX)
124 |
125 | def test_unknown_type(self):
126 | bundle = osc_bundle.OscBundle(_DGRAM_UNKNOWN_TYPE)
127 |
128 |
129 | if __name__ == "__main__":
130 | unittest.main()
131 |
--------------------------------------------------------------------------------
/server/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).build()
11 | self.assertEqual(0, bundle.num_contents)
12 |
13 | def test_raises_on_build(self):
14 | bundle = osc_bundle_builder.OscBundleBuilder(0.0)
15 | bundle.add_content(None)
16 | self.assertRaises(osc_bundle_builder.BuildError, bundle.build)
17 |
18 | def test_raises_on_invalid_timestamp(self):
19 | bundle = osc_bundle_builder.OscBundleBuilder("I am not a timestamp")
20 | self.assertRaises(osc_bundle_builder.BuildError, bundle.build)
21 |
22 | def test_build_complex_bundle(self):
23 | bundle = osc_bundle_builder.OscBundleBuilder(
24 | 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 |
--------------------------------------------------------------------------------
/server/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 = (
9 | b"/FB\x00"
10 | b",f\x00\x00"
11 | b">xca=q")
12 |
13 | _DGRAM_SWITCH_GOES_OFF = (
14 | b"/SYNC\x00\x00\x00"
15 | b",f\x00\x00"
16 | b"\x00\x00\x00\x00")
17 |
18 | _DGRAM_SWITCH_GOES_ON = (
19 | b"/SYNC\x00\x00\x00"
20 | b",f\x00\x00"
21 | b"?\x00\x00\x00")
22 |
23 | _DGRAM_NO_PARAMS = b"/SYNC\x00\x00\x00"
24 |
25 | _DGRAM_ALL_STANDARD_TYPES_OF_PARAMS = (
26 | b"/SYNC\x00\x00\x00"
27 | b",ifsb\x00\x00\x00"
28 | b"\x00\x00\x00\x03" # 3
29 | b"@\x00\x00\x00" # 2.0
30 | b"ABC\x00" # "ABC"
31 | b"\x00\x00\x00\x08stuff\x00\x00\x00") # b"stuff\x00\x00\x00"
32 |
33 | _DGRAM_ALL_NON_STANDARD_TYPES_OF_PARAMS = (
34 | b"/SYNC\x00\x00\x00"
35 | b"T" # True
36 | b"F" # False
37 | b"[]\x00\x00\x00" # Empty array
38 | b"t\x00\x00\x00\x00\x00\x00\x00\x00"
39 | )
40 |
41 | _DGRAM_COMPLEX_ARRAY_PARAMS = (
42 | b"/SYNC\x00\x00\x00"
43 | b",[i][[ss]][[i][i[s]]]\x00\x00\x00"
44 | b"\x00\x00\x00\x01" # 1
45 | b"ABC\x00" # "ABC"
46 | b"DEF\x00" # "DEF"
47 | b"\x00\x00\x00\x02" # 2
48 | b"\x00\x00\x00\x03" # 3
49 | b"GHI\x00") # "GHI"
50 |
51 | _DGRAM_UNKNOWN_PARAM_TYPE = (
52 | b"/SYNC\x00\x00\x00"
53 | b",fx\x00" # x is an unknown param type.
54 | b"?\x00\x00\x00")
55 |
56 | # range(512) param list.
57 | _DGRAM_LONG_LIST = (
58 | 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'
59 | )
60 |
61 |
62 | class TestOscMessage(unittest.TestCase):
63 | def test_switch_goes_off(self):
64 | msg = osc_message.OscMessage(_DGRAM_SWITCH_GOES_OFF)
65 | self.assertEqual("/SYNC", msg.address)
66 | self.assertEqual(1, len(msg.params))
67 | self.assertTrue(type(msg.params[0]) == float)
68 | self.assertAlmostEqual(0.0, msg.params[0])
69 |
70 | def test_switch_goes_on(self):
71 | msg = osc_message.OscMessage(_DGRAM_SWITCH_GOES_ON)
72 | self.assertEqual("/SYNC", msg.address)
73 | self.assertEqual(1, len(msg.params))
74 | self.assertTrue(type(msg.params[0]) == float)
75 | self.assertAlmostEqual(0.5, msg.params[0])
76 |
77 | def test_knob_rotates(self):
78 | msg = osc_message.OscMessage(_DGRAM_KNOB_ROTATES)
79 | self.assertEqual("/FB", msg.address)
80 | self.assertEqual(1, len(msg.params))
81 | self.assertTrue(type(msg.params[0]) == float)
82 |
83 | def test_no_params(self):
84 | msg = osc_message.OscMessage(_DGRAM_NO_PARAMS)
85 | self.assertEqual("/SYNC", msg.address)
86 | self.assertEqual(0, len(msg.params))
87 |
88 | def test_all_standard_types_off_params(self):
89 | msg = osc_message.OscMessage(_DGRAM_ALL_STANDARD_TYPES_OF_PARAMS)
90 | self.assertEqual("/SYNC", msg.address)
91 | self.assertEqual(4, len(msg.params))
92 | self.assertEqual(3, msg.params[0])
93 | self.assertAlmostEqual(2.0, msg.params[1])
94 | self.assertEqual("ABC", msg.params[2])
95 | self.assertEqual(b"stuff\x00\x00\x00", msg.params[3])
96 | self.assertEqual(4, len(list(msg)))
97 |
98 | def test_all_non_standard_params(self):
99 | msg = osc_message.OscMessage(_DGRAM_ALL_NON_STANDARD_TYPES_OF_PARAMS)
100 |
101 | self.assertEqual("/SYNC", msg.address)
102 | self.assertEqual(4, len(msg.params))
103 | self.assertEqual(True, msg.params[0])
104 | self.assertEqual(False, msg.params[1])
105 | self.assertEqual([], msg.params[2])
106 | self.assertEqual((datetime(1900, 1, 1, 0, 0, 0), 0), msg.params[3])
107 | self.assertEqual(4, len(list(msg)))
108 |
109 | def test_complex_array_params(self):
110 | msg = osc_message.OscMessage(_DGRAM_COMPLEX_ARRAY_PARAMS)
111 | self.assertEqual("/SYNC", msg.address)
112 | self.assertEqual(3, len(msg.params))
113 | self.assertEqual([1], msg.params[0])
114 | self.assertEqual([["ABC", "DEF"]], msg.params[1])
115 | self.assertEqual([[2], [3, ["GHI"]]], msg.params[2])
116 | self.assertEqual(3, len(list(msg)))
117 |
118 | def test_raises_on_empty_datargram(self):
119 | self.assertRaises(osc_message.ParseError, osc_message.OscMessage, b'')
120 |
121 | def test_ignores_unknown_param(self):
122 | msg = osc_message.OscMessage(_DGRAM_UNKNOWN_PARAM_TYPE)
123 | self.assertEqual("/SYNC", msg.address)
124 | self.assertEqual(1, len(msg.params))
125 | self.assertTrue(type(msg.params[0]) == float)
126 | self.assertAlmostEqual(0.5, msg.params[0])
127 |
128 | def test_raises_on_invalid_array(self):
129 | self.assertRaises(osc_message.ParseError,
130 | osc_message.OscMessage,
131 | b"/SYNC\x00\x00\x00[]]\x00")
132 | self.assertRaises(osc_message.ParseError,
133 | osc_message.OscMessage,
134 | b"/SYNC\x00\x00\x00[[]\x00")
135 |
136 | def test_raises_on_incorrect_datargram(self):
137 | self.assertRaises(
138 | osc_message.ParseError, osc_message.OscMessage, b'foobar')
139 |
140 | def test_parse_long_params_list(self):
141 | msg = osc_message.OscMessage(_DGRAM_LONG_LIST)
142 | self.assertEqual("/SYNC", msg.address)
143 | self.assertEqual(1, len(msg.params))
144 | self.assertEqual(512, len(msg.params[0]))
145 |
146 |
147 | if __name__ == "__main__":
148 | unittest.main()
149 |
--------------------------------------------------------------------------------
/server/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_infered_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("value")
31 | builder.add_arg(True)
32 | builder.add_arg(False)
33 | builder.add_arg(b"\x01\x02\x03")
34 | builder.add_arg([1, ["abc"]])
35 | # The same args but with explicit types.
36 | builder.add_arg(4.0, builder.ARG_TYPE_FLOAT)
37 | builder.add_arg(2, builder.ARG_TYPE_INT)
38 | builder.add_arg("value", builder.ARG_TYPE_STRING)
39 | builder.add_arg(True)
40 | builder.add_arg(False)
41 | builder.add_arg(b"\x01\x02\x03", builder.ARG_TYPE_BLOB)
42 | builder.add_arg([1, ["abc"]], [builder.ARG_TYPE_INT, [builder.ARG_TYPE_STRING]])
43 | builder.add_arg(4278255360, builder.ARG_TYPE_RGBA)
44 | builder.add_arg((1, 145, 36, 125), builder.ARG_TYPE_MIDI)
45 | builder.add_arg(1e-9, builder.ARG_TYPE_DOUBLE)
46 | self.assertEqual(len("fisTFb[i[s]]") * 2 + 3, len(builder.args))
47 | self.assertEqual("/SYNC", builder.address)
48 | builder.address = '/SEEK'
49 | msg = builder.build()
50 | self.assertEqual("/SEEK", msg.address)
51 | self.assertSequenceEqual(
52 | [4.0, 2, "value", True, False, b"\x01\x02\x03", [1, ["abc"]]] * 2 +
53 | [4278255360, (1, 145, 36, 125), 1e-9],
54 | msg.params)
55 |
56 | def test_long_list(self):
57 | huge_list = list(range(512))
58 | builder = osc_message_builder.OscMessageBuilder(address="/SYNC")
59 | builder.add_arg(huge_list)
60 | msg = builder.build()
61 | print(msg._dgram)
62 | self.assertSequenceEqual([huge_list], msg.params)
63 |
64 | def test_build_wrong_type_raises(self):
65 | builder = osc_message_builder.OscMessageBuilder(address="/SYNC")
66 | builder.add_arg('this is not a float', builder.ARG_TYPE_FLOAT)
67 | self.assertRaises(osc_message_builder.BuildError, builder.build)
68 |
69 | def test_build_noarg_message(self):
70 | msg = osc_message_builder.OscMessageBuilder(address='/SYNC').build()
71 | # This reference message was generated with Cycling 74's Max software
72 | # and then was intercepted with Wireshark
73 | reference = bytearray.fromhex('2f53594e430000002c000000')
74 | self.assertSequenceEqual(msg._dgram, reference)
75 |
76 | def test_bool_encoding(self):
77 | builder = osc_message_builder.OscMessageBuilder('')
78 | builder.add_arg(0)
79 | builder.add_arg(1)
80 | builder.add_arg(False)
81 | builder.add_arg(True)
82 | self.assertEqual(builder.args, [("i", 0), ("i", 1), ("F", False), ("T", True)])
83 |
84 |
85 | if __name__ == "__main__":
86 | unittest.main()
87 |
--------------------------------------------------------------------------------
/server/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 | _DGRAM_EMPTY_BUNDLE = (
20 | b"#bundle\x00"
21 | b"\x00\x00\x00\x00\x00\x00\x00\x01")
22 |
23 | _DGRAM_NESTED_MESS = (
24 | b"#bundle\x00"
25 | b"\x10\x00\x00\x00\x00\x00\x00\x00"
26 | # First message.
27 | b"\x00\x00\x00\x10" # 16 bytes
28 | b"/1111\x00\x00\x00"
29 | b",f\x00\x00"
30 | b"?\x00\x00\x00"
31 | # Second message, same.
32 | b"\x00\x00\x00\x10" # 16 bytes
33 | b"/2222\x00\x00\x00"
34 | b",f\x00\x00"
35 | b"?\x00\x00\x00"
36 | # Now another bundle within it, oh my...
37 | b"\x00\x00\x00$" # 36 bytes.
38 | b"#bundle\x00"
39 | b"\x20\x00\x00\x00\x00\x00\x00\x00"
40 | # First message.
41 | b"\x00\x00\x00\x10"
42 | b"/3333\x00\x00\x00"
43 | b",f\x00\x00"
44 | b"?\x00\x00\x00"
45 | # And another final bundle.
46 | b"\x00\x00\x00$" # 36 bytes.
47 | b"#bundle\x00"
48 | b"\x15\x00\x00\x00\x00\x00\x00\x01" # Immediately this one.
49 | # First message.
50 | b"\x00\x00\x00\x10"
51 | b"/4444\x00\x00\x00"
52 | b",f\x00\x00"
53 | b"?\x00\x00\x00")
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 |
--------------------------------------------------------------------------------
/server/pythonosc/test/test_osc_server.py:
--------------------------------------------------------------------------------
1 | import unittest
2 | import unittest.mock
3 |
4 | from pythonosc import dispatcher
5 | from pythonosc import osc_server
6 |
7 | _SIMPLE_PARAM_INT_MSG = (
8 | b"/SYNC\x00\x00\x00"
9 | b",i\x00\x00"
10 | b"\x00\x00\x00\x04")
11 |
12 | # Regression test for a datagram that should NOT be stripped, ever...
13 | _SIMPLE_PARAM_INT_9 = b'/debug\x00\x00,i\x00\x00\x00\x00\x00\t'
14 |
15 | _SIMPLE_MSG_NO_PARAMS = b"/SYNC\x00\x00\x00"
16 |
17 |
18 | class TestOscServer(unittest.TestCase):
19 | def test_is_valid_request(self):
20 | self.assertTrue(
21 | osc_server._is_valid_request([b'#bundle\x00foobar']))
22 | self.assertTrue(
23 | osc_server._is_valid_request([b'/address/1/2/3,foobar']))
24 | self.assertFalse(
25 | osc_server._is_valid_request([b'']))
26 |
27 |
28 | class TestUDPHandler(unittest.TestCase):
29 | def setUp(self):
30 | super().setUp()
31 | self.dispatcher = dispatcher.Dispatcher()
32 | # We do not want to create real UDP connections during unit tests.
33 | self.server = unittest.mock.Mock(spec=osc_server.BlockingOSCUDPServer)
34 | # Need to attach property mocks to types, not objects... weird.
35 | type(self.server).dispatcher = unittest.mock.PropertyMock(
36 | return_value=self.dispatcher)
37 | self.client_address = ("127.0.0.1", 8080)
38 |
39 | def test_no_match(self):
40 | mock_meth = unittest.mock.MagicMock()
41 | self.dispatcher.map("/foobar", mock_meth)
42 | osc_server._UDPHandler(
43 | [_SIMPLE_PARAM_INT_MSG, None], self.client_address, self.server)
44 | self.assertFalse(mock_meth.called)
45 |
46 | def test_match_with_args(self):
47 | mock_meth = unittest.mock.MagicMock()
48 | self.dispatcher.map("/SYNC", mock_meth, 1, 2, 3)
49 | osc_server._UDPHandler(
50 | [_SIMPLE_PARAM_INT_MSG, None], self.client_address, self.server)
51 | mock_meth.assert_called_with("/SYNC", [1, 2, 3], 4)
52 |
53 | def test_match_int9(self):
54 | mock_meth = unittest.mock.MagicMock()
55 | self.dispatcher.map("/debug", mock_meth)
56 | osc_server._UDPHandler(
57 | [_SIMPLE_PARAM_INT_9, None], self.client_address, self.server)
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 | self.dispatcher.map("/SYNC", mock_meth)
64 | osc_server._UDPHandler(
65 | [_SIMPLE_MSG_NO_PARAMS, None], self.client_address, self.server)
66 | mock_meth.assert_called_with("/SYNC")
67 |
68 | def test_match_default_handler(self):
69 | mock_meth = unittest.mock.MagicMock()
70 | self.dispatcher.set_default_handler(mock_meth)
71 | osc_server._UDPHandler(
72 | [_SIMPLE_MSG_NO_PARAMS, None], self.client_address, self.server)
73 | mock_meth.assert_called_with("/SYNC")
74 |
75 |
76 | if __name__ == "__main__":
77 | unittest.main()
78 |
--------------------------------------------------------------------------------
/server/pythonosc/test/test_udp_client.py:
--------------------------------------------------------------------------------
1 | import socket
2 | import unittest
3 | from unittest import mock
4 |
5 | from pythonosc import osc_message_builder
6 | from pythonosc import udp_client
7 |
8 |
9 | class TestUdpClient(unittest.TestCase):
10 | @mock.patch('socket.socket')
11 | def test_send(self, mock_socket_ctor):
12 | mock_socket = mock_socket_ctor.return_value
13 | client = udp_client.UDPClient('::1', 31337)
14 |
15 | msg = osc_message_builder.OscMessageBuilder('/').build()
16 | client.send(msg)
17 |
18 | self.assertTrue(mock_socket.sendto.called)
19 | mock_socket.sendto.assert_called_once_with(msg.dgram, ('::1', 31337))
20 |
21 |
22 | class TestSimpleUdpClient(unittest.TestCase):
23 | def setUp(self):
24 | self.patcher = mock.patch('pythonosc.udp_client.OscMessageBuilder')
25 | self.patcher.start()
26 | self.builder = udp_client.OscMessageBuilder.return_value
27 | self.msg = self.builder.build.return_value
28 | self.client = mock.Mock()
29 |
30 | def tearDown(self):
31 | self.patcher.stop()
32 |
33 | def test_send_message_calls_send_with_msg(self):
34 | udp_client.SimpleUDPClient.send_message(self.client, '/address', 1)
35 | self.client.send.assert_called_once_with(self.msg)
36 |
37 | def test_send_message_calls_add_arg_with_value(self):
38 | udp_client.SimpleUDPClient.send_message(self.client, '/address', 1)
39 | self.builder.add_arg.assert_called_once_with(1)
40 |
41 | def test_send_message_calls_add_arg_once_with_string(self):
42 | udp_client.SimpleUDPClient.send_message(self.client, '/address', 'hello')
43 | self.builder.add_arg.assert_called_once_with('hello')
44 |
45 | def test_send_message_calls_add_arg_multiple_times_with_list(self):
46 | udp_client.SimpleUDPClient.send_message(self.client, '/address',
47 | [1, 'john', True])
48 | self.assertEqual(self.builder.add_arg.call_count, 3)
49 |
50 |
51 | if __name__ == "__main__":
52 | unittest.main()
53 |
--------------------------------------------------------------------------------
/server/pythonosc/udp_client.py:
--------------------------------------------------------------------------------
1 | """Client to send OSC datagrams to an OSC server via UDP."""
2 |
3 | from collections.abc import Iterable
4 | import socket
5 |
6 | from .osc_message_builder import OscMessageBuilder
7 | from pythonosc import osc_message
8 |
9 | from typing import Union
10 |
11 |
12 | class UDPClient(object):
13 | """OSC client to send OscMessages or OscBundles via UDP."""
14 |
15 | def __init__(self, address: str, port: int, allow_broadcast: bool = False):
16 | """Initialize the client.
17 |
18 | As this is UDP it will not actually make any attempt to connect to the
19 | given server at ip:port until the send() method is called.
20 | """
21 | self._sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
22 | self._sock.setblocking(0)
23 | if allow_broadcast:
24 | self._sock.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1)
25 | self._address = address
26 | self._port = port
27 |
28 | def send(self, content: osc_message.OscMessage) -> None:
29 | """Sends an OscBundle or OscMessage to the server."""
30 | self._sock.sendto(content.dgram, (self._address, self._port))
31 |
32 |
33 | class SimpleUDPClient(UDPClient):
34 | """Simple OSC client with a `send_message` method."""
35 |
36 | def send_message(self, address: str, value: Union[int, float, bytes, str, bool, tuple, list]) -> None:
37 | """Compose an OSC message and send it."""
38 | builder = OscMessageBuilder(address=address)
39 | if not isinstance(value, Iterable) or isinstance(value, (str, bytes)):
40 | values = [value]
41 | else:
42 | values = value
43 | for val in values:
44 | builder.add_arg(val)
45 | msg = builder.build()
46 | self.send(msg)
47 |
--------------------------------------------------------------------------------
/server/server.py:
--------------------------------------------------------------------------------
1 | import bpy
2 | import types
3 | import sys
4 | from select import select
5 | import socket
6 | import errno
7 | import mathutils
8 | import traceback
9 | from math import radians
10 | from bpy.props import *
11 | from ast import literal_eval as make_tuple
12 |
13 | import os
14 | import platform
15 | script_file = os.path.realpath(__file__)
16 | directory = os.path.dirname(script_file)
17 | if directory not in sys.path:
18 | sys.path.append(directory)
19 |
20 | from oscpy.client import OSCClient
21 | from oscpy.server import OSCThreadServer
22 |
23 | from pythonosc import osc_message_builder
24 | from pythonosc import udp_client
25 | from pythonosc import osc_bundle
26 | from pythonosc import osc_message
27 | from pythonosc import osc_packet
28 | from pythonosc import dispatcher
29 | from pythonosc import osc_server
30 |
31 | import threading
32 | import socketserver
33 |
34 | from ._base import *
35 |
36 | from .callbacks import *
37 | from ..nodes.nodes import *
38 |
39 | #######################################
40 | # Setup OSCPy Server #
41 | #######################################
42 |
43 | class OSC_OT_OSCPyServer(OSC_OT_OSCServer):
44 | bl_idname = "nodeosc.oscpy_operator"
45 | bl_label = "OSCMainThread"
46 |
47 | _timer = None
48 | count = 0
49 |
50 | #####################################
51 | # CUSTOMIZEABLE FUNCTIONS:
52 |
53 | inputServer = "" #for the receiving socket
54 | outputServer = "" #for the sending socket
55 |
56 | # setup the sending server
57 | def setupInputServer(self, context, envars):
58 | self.dispatcher = dispatcher.Dispatcher()
59 |
60 | # setup the receiving server
61 | def setupOutputServer(self, context, envars):
62 | #For sending
63 | self.outputServer = OSCClient(envars.udp_out, envars.port_out)
64 | self.outputServer.send_message(b'/NodeOSC', [b'Python server started up'])
65 | print("OSCPy Server sended test message to " + envars.udp_out + " on port " + str(envars.port_out))
66 |
67 | def sendingOSC(self, context, event):
68 |
69 | oscMessage = {}
70 |
71 | # gather all the ouput bound osc messages
72 | make_osc_messages(bpy.context.scene.NodeOSC_outputs, oscMessage)
73 |
74 | # and send them
75 | for key, args in oscMessage.items():
76 | values = []
77 | if isinstance(args, (tuple, list)):
78 | for argum in args:
79 | if type(argum) == str:
80 | argum = bytes(argum, encoding='utf-8')
81 | values.append(argum)
82 | else:
83 | if type(args) == str:
84 | args = bytes(args, encoding='utf-8')
85 | values.append(args)
86 | self.outputServer.send_message(bytes(key, encoding='utf-8'), values)
87 |
88 | # add method
89 | def addMethod(self, address, data):
90 | pass #already set during creation of inputserver
91 |
92 | # add default method
93 | def addDefaultMethod(self):
94 | pass #already set during creation of inputserver
95 |
96 | # start receiving
97 | def startupInputServer(self, context, envars):
98 | print("Create OscPy Thread...")
99 | # creating a blocking UDP Server
100 | # Each message will be handled sequentially on the same thread.
101 | self.inputServer = OSCThreadServer(encoding='utf8', default_handler=OSC_callback_oscpy)
102 | sock = self.inputServer.listen(address=envars.udp_in, port=envars.port_in, default=True)
103 | print("... server started on ", envars.port_in)
104 |
105 | # stop receiving
106 | def shutDownInputServer(self, context, envars):
107 | print("OSCPy Server is shutting down...")
108 | self.inputServer.stop() # Stop default socket
109 | print(" stopping all sockets...")
110 | self.inputServer.stop_all() # Stop all sockets
111 | print(" terminating server...")
112 | self.inputServer.terminate_server() # Request the handler thread to stop looping
113 | self.inputServer.join_server() # Wait for the handler thread to finish pending tasks and exit
114 | print("... OSCPy Server is shutdown")
115 |
116 |
117 | #######################################
118 | # Setup PythonOSC Server #
119 | #######################################
120 |
121 | class OSC_OT_PythonOSCServer(OSC_OT_OSCServer):
122 | bl_idname = "nodeosc.pythonosc_operator"
123 | bl_label = "OSCMainThread"
124 |
125 | _timer = None
126 | count = 0
127 |
128 | #####################################
129 | # CUSTOMIZEABLE FUNCTIONS:
130 |
131 | inputServer = "" #for the receiving socket
132 | outputServer = "" #for the sending socket
133 | dispatcher = "" #dispatcher function
134 |
135 | # setup the sending server
136 | def setupInputServer(self, context, envars):
137 | self.dispatcher = dispatcher.Dispatcher()
138 |
139 | # setup the receiving server
140 | def setupOutputServer(self, context, envars):
141 | #For sending
142 | self.outputServer = udp_client.UDPClient(envars.udp_out, envars.port_out)
143 | msg = osc_message_builder.OscMessageBuilder(address="/NodeOSC")
144 | msg.add_arg("Python server started up")
145 | msg = msg.build()
146 | self.outputServer.send(msg)
147 | print("Python Server sended test message to " + envars.udp_out + " on port " + str(envars.port_out))
148 |
149 | def sendingOSC(self, context, event):
150 |
151 | oscMessage = {}
152 |
153 | # gather all the ouput bound osc messages
154 | make_osc_messages(bpy.context.scene.NodeOSC_outputs, oscMessage)
155 |
156 | # and send them
157 | for key, args in oscMessage.items():
158 | msg = osc_message_builder.OscMessageBuilder(address=key)
159 | if isinstance(args, (tuple, list)):
160 | for argum in args:
161 | msg.add_arg(argum)
162 | else:
163 | msg.add_arg(args)
164 | msg = msg.build()
165 | self.outputServer.send(msg)
166 |
167 | # add method
168 | def addMethod(self, address, data):
169 | self.dispatcher.map(address, OSC_callback_pythonosc, data)
170 |
171 | # add default method
172 | def addDefaultMethod(self):
173 | self.dispatcher.set_default_handler(OSC_callback_pythonosc_undef)
174 |
175 | # start receiving
176 | def startupInputServer(self, context, envars):
177 | print("Create Python Server Thread...")
178 | # creating a blocking UDP Server
179 | # Each message will be handled sequentially on the same thread.
180 | # the alternative:
181 | # ThreadingOSCUDPServer creates loads of threads
182 | # that are not cleaned up properly
183 | self.inputServer = osc_server.BlockingOSCUDPServer((envars.udp_in, envars.port_in), self.dispatcher)
184 | self.server_thread = threading.Thread(target=self.inputServer.serve_forever)
185 | self.server_thread.start()
186 | print("... server started on ", envars.port_in)
187 |
188 | # stop receiving
189 | def shutDownInputServer(self, context, envars):
190 | self.inputServer.shutdown()
191 | print("Python Server is shutdown")
192 |
193 |
194 | panel_classes = (
195 | OSC_OT_OSCPyServer,
196 | OSC_OT_PythonOSCServer,
197 | )
198 |
199 | def register():
200 | for cls in panel_classes:
201 | bpy.utils.register_class(cls)
202 |
203 | def unregister():
204 | for cls in reversed(panel_classes):
205 | bpy.utils.unregister_class(cls)
206 |
--------------------------------------------------------------------------------
/ui/panels.py:
--------------------------------------------------------------------------------
1 | import bpy
2 | import platform
3 | from pathlib import Path
4 |
5 | def prettyTime(seconds):
6 | if seconds > 1.5: return "{:.2f} s".format(seconds)
7 | else: return "{:.4f} ms".format(seconds * 1000)
8 |
9 | #######################################
10 | # MAIN GUI PANEL #
11 | #######################################
12 |
13 | class OSC_PT_Settings(bpy.types.Panel):
14 | bl_category = "NodeOSC"
15 | bl_label = "NodeOSC Server"
16 | bl_space_type = "VIEW_3D"
17 | bl_region_type = "UI"
18 |
19 | def draw(self, context):
20 | preferences = context.preferences
21 | addon_prefs = preferences.addons[self.bl_category].preferences
22 |
23 | envars = bpy.context.scene.nodeosc_envars
24 | layout = self.layout
25 | column = layout.column(align=True)
26 | col_box = column.column()
27 | col = col_box.box()
28 | if envars.isServerRunning == False:
29 | row = col.row(align=True)
30 | row.prop(envars, 'isUIExpanded', text = "",
31 | icon='DISCLOSURE_TRI_DOWN' if envars.isUIExpanded else 'DISCLOSURE_TRI_RIGHT',
32 | emboss = False)
33 | if addon_prefs.usePyLiblo == False:
34 | row.operator("nodeosc.oscpy_operator", text='Start', icon='PLAY')
35 | else:
36 | row.operator("nodeosc.pythonosc_operator", text='Start', icon='PLAY')
37 | row.prop(addon_prefs, "usePyLiblo", text = '', icon='CHECKBOX_HLT' if addon_prefs.usePyLiblo else 'CHECKBOX_DEHLT')
38 |
39 | if envars.isUIExpanded:
40 | col1 = col.column(align=True)
41 | row1 = col1.row(align=True)
42 | row1.prop(envars, 'udp_in', text="In")
43 | row1.prop(envars, 'port_in', text="Port")
44 | col2 = col.column(align=True)
45 | row2 = col2.row(align=True)
46 | row2.prop(envars, 'udp_out', text="Out")
47 | row2.prop(envars, 'port_out', text="Port")
48 | col.prop(envars, 'input_rate', text="input rate(ms)")
49 | col.prop(envars, 'output_rate', text="output rate(ms)")
50 | col.prop(envars, 'repeat_address_filter_IN', text="Filter incoming")
51 | col.prop(envars, 'repeat_argument_filter_OUT', text="Filter outgoing")
52 | col.prop(envars, 'autorun', text="Start at Launch")
53 | else:
54 | row = col.row(align=True)
55 | row.prop(envars, 'isUIExpanded', text = "",
56 | icon='DISCLOSURE_TRI_DOWN' if envars.isUIExpanded else 'DISCLOSURE_TRI_RIGHT',
57 | emboss = False)
58 | if addon_prefs.usePyLiblo == False:
59 | row.operator("nodeosc.oscpy_operator", text='osc py server is running...', icon='PAUSE')
60 | else:
61 | row.operator("nodeosc.pythonosc_operator", text='python osc server is running..', icon='PAUSE')
62 |
63 | if envars.isUIExpanded:
64 | col.label(text=" listening at " + envars.udp_in + " on port " + str(envars.port_in))
65 | col.label(text=" sending to " + envars.udp_out + " on port " + str(envars.port_out))
66 |
67 | col.prop(envars, 'input_rate', text="input rate(ms)")
68 |
69 | col.prop(bpy.context.scene.nodeosc_envars, 'message_monitor', text="Monitoring and Error reporting")
70 | col.prop(envars, 'repeat_address_filter_IN', text="Filter incoming")
71 | col.prop(envars, 'repeat_argument_filter_OUT', text="Filter outgoing")
72 | col.prop(envars, 'debug_monitor')
73 |
74 | if bpy.context.scene.nodeosc_envars.message_monitor == True:
75 | box = col.box()
76 | row5 = box.column(align=True)
77 | row5.label(text = "input: " + prettyTime(envars.executionTimeInput), icon = "TIME")
78 | row5.label(text = "output: " + prettyTime(envars.executionTimeOutput), icon = "TIME")
79 | row6 = box.column(align=True)
80 | if addon_prefs.usePyLiblo == True:
81 | row6.label(text="the other osc library can printout unmapped osc messages..", icon="ERROR")
82 | if addon_prefs.usePyLiblo == False:
83 | row6.label(text="Last OSC message:")
84 | row6.prop(envars, 'lastaddr', text="address")
85 | row6.prop(envars, 'lastpayload', text="values")
86 | row6.prop(envars, 'enable_incomming_message_printout', text="printout unmapped messages")
87 |
88 |
89 |
90 | #######################################
91 | # CUSTOM RX PANEL #
92 | #######################################
93 |
94 | class OSC_PT_Operations(bpy.types.Panel):
95 | bl_category = "NodeOSC"
96 | bl_label = "Custom Messages"
97 | bl_space_type = "VIEW_3D"
98 | bl_region_type = "UI"
99 |
100 | def draw(self, context):
101 | envars = bpy.context.scene.nodeosc_envars
102 | layout = self.layout
103 | if envars.isServerRunning == False:
104 | layout.label(text="Message handlers:")
105 | else:
106 | layout.label(text="Message handlers: (stop server for changes)")
107 | index = 0
108 | col = layout.column()
109 | for item in bpy.context.scene.NodeOSC_keys:
110 | col_box = col.column()
111 | box = col_box.box()
112 | #box.enabled = not envars.isServerRunning
113 | colsub = box.column()
114 | row = colsub.row(align=True)
115 |
116 | row.prop(item, "ui_expanded", text = "",
117 | icon='DISCLOSURE_TRI_DOWN' if item.ui_expanded else 'DISCLOSURE_TRI_RIGHT',
118 | emboss = False)
119 |
120 | sub1 = row.row()
121 | sub1.enabled = not envars.isServerRunning
122 | sub1.prop(item, "enabled", text = "",
123 | icon='CHECKBOX_HLT' if item.enabled else 'CHECKBOX_DEHLT',
124 | emboss = False)
125 | if item.osc_direction != 'INPUT' and item.dp_format_enable:
126 | sub1.label(icon='ERROR')
127 | sub1.prop(item, "osc_direction", text = "", emboss = False, icon_only = True)
128 |
129 | sub2 = row.row()
130 | sub2.active = item.enabled
131 | sub2.label(text=item.osc_address)
132 |
133 | submove = sub2.row(align=True)
134 | submove.operator("nodeosc.moveitem_up", icon='TRIA_UP', text='').index = index
135 | submove.operator("nodeosc.moveitem_down", icon='TRIA_DOWN', text = '').index = index
136 |
137 | subsub = sub2.row(align=True)
138 | if not envars.isServerRunning:
139 | subsub.operator("nodeosc.createitem", icon='ADD', text='').copy = index
140 | subsub.operator("nodeosc.deleteitem", icon='PANEL_CLOSE', text = "").index = index
141 |
142 | if envars.isServerRunning and envars.message_monitor:
143 | subsub.operator("nodeosc.pick", text='', icon='EYEDROPPER').i_addr = item.osc_address
144 |
145 | if item.ui_expanded:
146 | dataColumn = colsub.column(align=True)
147 | dataColumn.enabled = not envars.isServerRunning
148 | dataSplit = dataColumn.split(factor = 0.2)
149 |
150 | colLabel = dataSplit.column(align = True)
151 | colData = dataSplit.column(align = True)
152 |
153 | colLabel.label(text='address')
154 | address_row = colData.row(align = True)
155 | address_row.prop(item, 'osc_address',text='', icon_only = True)
156 | if item.osc_direction != "INPUT":
157 | address_row.prop(item, 'filter_repetition',text='', icon='CHECKBOX_HLT' if item.filter_repetition else 'CHECKBOX_DEHLT',
158 | emboss = False)
159 | if item.osc_direction != "OUTPUT":
160 | address_row.prop(item, 'filter_enable',text='', icon='MODIFIER' if item.filter_enable else 'MODIFIER_DATA',
161 | emboss = False)
162 |
163 | if item.filter_enable and item.osc_direction != "OUTPUT":
164 | colLabel.label(text='')
165 | colData.prop(item,'filter_eval',text='filter')
166 |
167 | colLabel.label(text='datapath')
168 | datapath_row = colData.row(align = True)
169 | datapath_row.prop(item, 'data_path',text='')
170 |
171 | if item.osc_direction == "INPUT":
172 | datapath_row.prop(item, 'dp_format_enable',text='', icon='MODIFIER' if item.dp_format_enable else 'MODIFIER_DATA',
173 | emboss = False)
174 | if item.osc_direction != 'INPUT' and item.dp_format_enable:
175 | datapath_row.label(icon='ERROR')
176 |
177 | if item.dp_format_enable and item.osc_direction == "INPUT":
178 | colLabel.label(text='')
179 | colData.prop(item,'dp_format',text='format')
180 |
181 | if item.data_path.find('script(') == -1:
182 | colLabel.label(text='args[idx]')
183 | args_row = colData.row(align = True)
184 | args_row.prop(item, 'osc_index',text='')
185 | if item.dp_format_enable and item.osc_direction == "INPUT":
186 | args_row.prop(item, 'loop_enable',text='', icon='MODIFIER' if item.loop_enable else 'MODIFIER_DATA',
187 | emboss = False)
188 | if item.loop_enable:
189 | colLabel.label(text='')
190 | colData.prop(item,'loop_range',text='range')
191 |
192 | index = index + 1
193 |
194 | if envars.isServerRunning == False:
195 | layout.operator("nodeosc.createitem", icon='PRESET_NEW', text='Create new message handler').copy = -1
196 |
197 | layout.separator()
198 |
199 | row = layout.row(align=True)
200 | row.operator("nodeosc.export", text='Export OSC Config')
201 | row.operator("nodeosc.import", text='Import OSC Config')
202 | layout.operator("nodeosc.importks", text='Import Keying Set')
203 |
204 | #######################################
205 | # NODES RX PANEL #
206 | #######################################
207 |
208 | class OSC_PT_Nodes(bpy.types.Panel):
209 | bl_category = "NodeOSC"
210 | bl_label = "Node Messages"
211 | bl_space_type = "VIEW_3D"
212 | bl_region_type = "UI"
213 |
214 | def draw(self, context):
215 | envars = bpy.context.scene.nodeosc_envars
216 | layout = self.layout
217 | if envars.isServerRunning == False:
218 | layout.label(text="Node tree execute mode:")
219 | layout.prop(envars, 'node_update', text="execute ")
220 | if envars.node_update == "MESSAGE":
221 | layout.prop(envars, 'node_frameMessage', text="message")
222 | else:
223 | layout.label(text="Node tree execute mode:" + envars.node_update)
224 | if envars.node_update == "MESSAGE":
225 | layout.label(text="Execute on message: " + envars.node_frameMessage)
226 | layout.label(text="Node message handlers:")
227 | col = layout.column()
228 | for item in bpy.context.scene.NodeOSC_nodes:
229 | col_box = col.column()
230 | box = col_box.box()
231 | colsub = box.column()
232 | row = colsub.row(align=True)
233 |
234 | row.prop(item, "ui_expanded", text = "",
235 | icon='DISCLOSURE_TRI_DOWN' if item.ui_expanded else 'DISCLOSURE_TRI_RIGHT',
236 | emboss = False)
237 | row.label(text = "",
238 | icon='EXPORT' if item.osc_direction == "OUTPUT" else 'IMPORT')
239 |
240 | sub = row.row()
241 | sub.active = item.enabled
242 | sub.label(text=item.osc_address)
243 |
244 | if item.ui_expanded:
245 | split = colsub.row().split(factor=0.2)
246 | split.label(text="direction:")
247 | split.label(text=item.osc_direction)
248 |
249 | split = colsub.row().split(factor=0.2)
250 | split.label(text="address:")
251 | split.label(text=item.osc_address)
252 |
253 | split = colsub.row().split(factor=0.2)
254 | split.label(text="datapath:")
255 | split.label(text=item.data_path)
256 |
257 | #layout.label(text="Works only if \'Auto Execution\' and \'Porperty Changed\' is toggled on", icon="ERROR")
258 | layout.label(text="Works only with AnimationNodes if ", icon="ERROR")
259 | layout.label(text=" \'Auto Execution\' and")
260 | layout.label(text=" \'Property Changed\' is toggled on")
261 |
262 |
263 | panel_classes = (
264 | OSC_PT_Settings,
265 | OSC_PT_Operations,
266 | OSC_PT_Nodes,
267 | )
268 |
269 | def register():
270 | for cls in panel_classes:
271 | bpy.utils.register_class(cls)
272 |
273 | def unregister():
274 | for cls in reversed(panel_classes):
275 | bpy.utils.unregister_class(cls)
276 |
--------------------------------------------------------------------------------
/utils/keys.py:
--------------------------------------------------------------------------------
1 | import bpy
2 |
3 | from .utils import *
4 |
5 | class NodeOSCMsgValues(bpy.types.PropertyGroup):
6 | #key_path = bpy.props.StringProperty(name="Key", default="Unknown")
7 | osc_address: bpy.props.StringProperty(name="OSC Address", default="/custom")
8 | osc_type: bpy.props.StringProperty(name="Type", default="f")
9 | osc_index: bpy.props.StringProperty(name="Argument indices.", description = "Indicate in which order the arriving arguments will be applied. Have to be in the format \'() or (0 [, 1, 2])\' with 0...n integers, separated by a comma, and inside two parantheses \'()\'. There should be no more indices than arriving arguments, otherwise the message will be ignored", default="())")
10 | osc_direction: bpy.props.EnumProperty(name = "RX/TX", default = "INPUT", items = dataDirectionItems)
11 | filter_repetition: bpy.props.BoolProperty(name = "Filter argument repetition", default = False, description = "Avoid sending messages with repeating arguments")
12 | dp_format_enable: bpy.props.BoolProperty(name = "Format", default = False, description = "enable realtime evaluation of datapath with python string-format functionality")
13 | dp_format: bpy.props.StringProperty(name="Format", default="args", description = "enter the format values separated by commas. available keywords: string 'address' for osc-address, array 'addr' for individual address segments - converted to int or floats if applicable, array 'args' for all arguments, 'length' for args length, 'index' if loop is enabled" )
14 | loop_enable: bpy.props.BoolProperty(name = "Loop", default = False, description = "enable looping through the arguments")
15 | loop_range: bpy.props.StringProperty(name="Range", default="0, length, 1", description = "enter the range values for the loop. Maximal 3 values, separated by commas. Default: first value = start index, second value = end index, third value = step. Available keywords: 'args' for all arguments, 'length' for args length")
16 | filter_enable: bpy.props.BoolProperty(name = "Filter arguments", default = False, description = "enable filtering of incomming messages based on the incomming arguments")
17 | filter_eval: bpy.props.StringProperty(name="Argument evaluation", default="args[0] == 'string' or addr[1] == 'Cube'", description = "the filter condition to be evaluated. The result of the evaluated condition must be a 'true' or 'false'. Available keywords: string 'address', array 'args' for all incomming arguments, array 'addr' for individual address segments - converted to int or floats if applicable. BE AWARE: 'Filter repetition' should be disabled to guarrantee work properly")
18 | data_path: bpy.props.StringProperty(name="Datapath", description = "Use Ctrl-Alt-Shift-C to copy-paste the full datapath from your property you desire to controll", default="bpy.data.objects['Cube']")
19 | props: bpy.props.StringProperty(name="Property", default="", description = "NOT USED ANYMORE")
20 | value: bpy.props.StringProperty(name="value", default="Unknown")
21 | idx: bpy.props.IntProperty(name="Index", min=0, default=0)
22 | enabled: bpy.props.BoolProperty(name="Enabled", default=True)
23 | ui_expanded: bpy.props.BoolProperty(name="Expanded", default=True)
24 | node_data_type: bpy.props.EnumProperty(name = "Node data type", default = "LIST", items = nodeDataTypeItems)
25 | node_type: bpy.props.IntProperty(name = "Node type", default = 0)
26 |
27 | key_classes = (
28 | NodeOSCMsgValues,
29 | )
30 |
31 | def register():
32 | for cls in key_classes:
33 | bpy.utils.register_class(cls)
34 | bpy.types.Scene.NodeOSC_keys = bpy.props.CollectionProperty(type=NodeOSCMsgValues, description='collection of custom osc handler')
35 | bpy.types.Scene.NodeOSC_keys_tmp = bpy.props.CollectionProperty(type=NodeOSCMsgValues)
36 | bpy.types.Scene.NodeOSC_nodes = bpy.props.CollectionProperty(type=NodeOSCMsgValues, description='collection of all osc handler that are created by nodes')
37 | bpy.types.Scene.NodeOSC_outputs = bpy.props.CollectionProperty(type=NodeOSCMsgValues, description='collection of all osc handler that send messages to output')
38 |
39 |
40 | def unregister():
41 | del bpy.types.Scene.NodeOSC_outputs
42 | del bpy.types.Scene.NodeOSC_keys
43 | del bpy.types.Scene.NodeOSC_nodes
44 | del bpy.types.Scene.NodeOSC_keys_tmp
45 | for cls in reversed(key_classes):
46 | bpy.utils.unregister_class(cls)
47 |
48 |
49 |
--------------------------------------------------------------------------------
/utils/utils.py:
--------------------------------------------------------------------------------
1 | import bpy
2 |
3 | dataDirectionItems = {
4 | ("INPUT", "Input", "Receive the OSC message from somewhere else", "IMPORT", 0),
5 | ("OUTPUT", "Output", "Send the OSC message from this node", "EXPORT", 1),
6 | ("BOTH", "Both", "Send and Reveive this OSC message", "FILE_REFRESH", 2) }
7 |
8 | dataNodeDirectionItems = {
9 | ("INPUT", "Input", "Receive the OSC message from somewhere else", "IMPORT", 0),
10 | ("OUTPUT", "Output", "Send the OSC message from this node", "EXPORT", 1) }
11 |
12 | nodeDataTypeItems = {
13 | ("LIST", "List", "Expects List", "IMPORT", 0),
14 | ("SINGLE", "Single", "Expects single value", "IMPORT", 1) }
15 |
16 | nodeTypeItems = {
17 | ("NONE", 0),
18 | ("AN", 1),
19 | ("SORCAR", 2) }
20 |
21 | def sorcarTreeUpdate():
22 | bpy.context.scene.nodeosc_SORCAR_needsUpdate = True
23 |
--------------------------------------------------------------------------------