├── .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 | NodeOSC Part One 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 | [![Coverage Status](https://coveralls.io/repos/github/kivy/oscpy/badge.svg?branch=master)](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 | --------------------------------------------------------------------------------