├── .deepsource.toml ├── .gitattributes ├── .github └── workflows │ ├── publish-to-pypi.yml │ └── run tests.yml ├── .gitignore ├── LICENSE ├── README.md ├── examples ├── example.py ├── example_server.py └── tkinter_slider_to_artnet.py ├── setup.py ├── stupidArtnet ├── .pylintrc ├── ArtnetUtils.py ├── StupidArtnet.py ├── StupidArtnetServer.py └── __init__.py └── tests ├── __init__.py ├── test_client.py ├── test_server.py └── test_utils.py /.deepsource.toml: -------------------------------------------------------------------------------- 1 | version = 1 2 | 3 | [[analyzers]] 4 | name = "python" 5 | enabled = true 6 | 7 | [analyzers.meta] 8 | runtime_version = "3.x.x" -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | # Auto detect text files and perform LF normalization 2 | * text=auto 3 | -------------------------------------------------------------------------------- /.github/workflows/publish-to-pypi.yml: -------------------------------------------------------------------------------- 1 | name: Publish to PyPI 🐍 2 | on: 3 | push: 4 | # Only publish on new tags 5 | tags: 6 | - '*' 7 | 8 | # Allows you to run this workflow manually from the Actions tab 9 | workflow_dispatch: 10 | 11 | jobs: 12 | test-build-publish: 13 | runs-on: ubuntu-latest 14 | 15 | steps: 16 | - uses: actions/checkout@master 17 | - name: Set up Python 3 18 | uses: actions/setup-python@v1 19 | with: 20 | python-version: 3 21 | 22 | - name: Run test 23 | run: python -m unittest discover --v 24 | 25 | - name: Install pypa/build 26 | working-directory: / 27 | run: >- 28 | python -m 29 | pip install 30 | build 31 | --user 32 | 33 | - name: Build a binary wheel and a source tarball 34 | run: >- 35 | python -m 36 | build 37 | --sdist 38 | --wheel 39 | --outdir dist/ 40 | . 41 | 42 | - name: Publish distribution to PyPI 🐍 43 | if: ${{ !env.ACT }} 44 | uses: pypa/gh-action-pypi-publish@release/v1 45 | with: 46 | user: __token__ 47 | password: ${{ secrets.PYPI_API_TOKEN }} 48 | -------------------------------------------------------------------------------- /.github/workflows/run tests.yml: -------------------------------------------------------------------------------- 1 | name: Run tests 2 | on: [pull_request] 3 | 4 | jobs: 5 | test: 6 | runs-on: ubuntu-latest 7 | 8 | steps: 9 | - uses: actions/checkout@master 10 | - name: Set up Python 3 11 | uses: actions/setup-python@v1 12 | with: 13 | python-version: 3 14 | 15 | - name: Run test 16 | run: python -m unittest discover --v -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | # C extensions 6 | *.so 7 | # Distribution / packaging 8 | .Python 9 | build/ 10 | develop-eggs/ 11 | dist/ 12 | downloads/ 13 | eggs/ 14 | .eggs/ 15 | lib/ 16 | lib64/ 17 | parts/ 18 | sdist/ 19 | var/ 20 | wheels/ 21 | *.egg-info/ 22 | .installed.cfg 23 | *.egg 24 | MANIFEST 25 | # PyInstaller 26 | # Usually these files are written by a python script from a template 27 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 28 | *.manifest 29 | *.spec 30 | # Installer logs 31 | pip-log.txt 32 | pip-delete-this-directory.txt 33 | # Unit test / coverage reports 34 | htmlcov/ 35 | .tox/ 36 | .coverage 37 | .coverage.* 38 | .cache 39 | nosetests.xml 40 | coverage.xml 41 | *.cover 42 | .hypothesis/ 43 | .pytest_cache/ 44 | # Translations 45 | *.mo 46 | *.pot 47 | # Django stuff: 48 | *.log 49 | .static_storage/ 50 | .media/ 51 | local_settings.py 52 | # Flask stuff: 53 | instance/ 54 | .webassets-cache 55 | # Scrapy stuff: 56 | .scrapy 57 | # Sphinx documentation 58 | docs/_build/ 59 | # PyBuilder 60 | target/ 61 | # Jupyter Notebook 62 | .ipynb_checkpoints 63 | # pyenv 64 | .python-version 65 | # celery beat schedule file 66 | celerybeat-schedule 67 | # SageMath parsed files 68 | *.sage.py 69 | # Environments 70 | .env 71 | .venv 72 | env/ 73 | venv/ 74 | ENV/ 75 | env.bak/ 76 | venv.bak/ 77 | # Spyder project settings 78 | .spyderproject 79 | .spyproject 80 | # Rope project settings 81 | .ropeproject 82 | # mkdocs documentation 83 | /site 84 | # mypy 85 | .mypy_cache/ 86 | # osx ds files 87 | **/.DS_Store 88 | # VSCode 89 | *.code-workspace 90 | .vscode/ 91 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 CV_ 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![.github/workflows/publish-to-pypi.yml](https://github.com/cpvalente/stupidArtnet/actions/workflows/publish-to-pypi.yml/badge.svg)](https://github.com/cpvalente/stupidArtnet/actions/workflows/publish-to-pypi.yml) 2 | [![License: MIT](https://img.shields.io/badge/License-MIT-green.svg)](https://opensource.org/licenses/MIT) 3 | 4 | # StupidArtnet 5 | 6 | (Very) Simple Art-Net implementation in Python (compatible with micro python) 7 | 8 | #### Table of Contents 9 | - [Installing from github](#installing-from-github) 10 | - [Installing from Pip](#installing-from-pip) 11 | - [Server Basics](#receiving-data) 12 | - [Client Basics](#basics) 13 | - [Persistent sending](#persistent-sending) 14 | - [Example code](#example-code) 15 | - [Notes](#notes) 16 | - [Art-Net](#art-net) 17 | - [Nets and Subnets](#nets-and-subnets) 18 | - [License](#license) 19 | 20 | ### Installing from github 21 | You can get up and running quickly cloning from github. 22 | Run the example file to make sure everything is up to scratch 23 | ```bash 24 | $ git clone https://github.com/cpvalente/stupidArtnet.git 25 | $ cd stupidArtnet 26 | $ python3 examples/example.py 27 | ``` 28 | ### Installing from Pip 29 | The project is now available in [Pip](https://pypi.org/project/stupidArtnet/) and can be installed with 30 | ```pip install stupidartnet``` 31 | 32 | ### Receiving Data 33 | You can use the server module to receive Art-Net data 34 | ```python 35 | # a StupidArtnetServer can listen to a specific universe 36 | # and return new data to a user defined callback 37 | a = StupidArtnetServer(universe=0, callback_function=test_callback) 38 | 39 | # if you prefer, you can also inspect the latest 40 | # received data yourself 41 | buffer = a.get_buffer() 42 | 43 | ``` 44 | ### Persistent sending 45 | Usually Artnet devices (and DMX in general) transmit data at a rate of no less than 30Hz. 46 | You can do this with StupidArtnet by using its threaded abilities 47 | 48 | ```python 49 | # TO SEND PERSISTENT SIGNAL YOU CAN START THE THREAD 50 | a.start() 51 | 52 | # AND MODIFY THE DATA AS YOU GO 53 | for x in range(100): 54 | for i in range(packet_size): # Fill buffer with random stuff 55 | packet[i] = random.randint(0, 255) 56 | a.set(packet) 57 | time.sleep(.2) 58 | 59 | # ... REMEMBER TO CLOSE THE THREAD ONCE YOU ARE DONE 60 | a.stop() 61 | 62 | ``` 63 | ### Example code 64 | See examples folder inside the package directory 65 | - [x] Use with Tkinter 66 | - [x] Send Art-Net (client) 67 | - [x] Receive Art-Net (server) 68 | 69 | ### Notes 70 | 71 | Artnet libraries tend to be complicated and hard to get off the ground. Sources were either too simple and didn't explain the workings or become too complex by fully implementing the protocol.
72 | This is meant as an implementation focusing on DMX over Artnet only (ArtDMX). 73 | 74 | I am also doing my best to comment the sections where the packets is build. In an effort to help you understand the protocol and be able to extend it for a more case specific use. 75 | 76 | Users looking to send a few channels to control a couple of LEDs, projectors or media servers can use this as reference. 77 | 78 | Are you running several universes with different fixture types? I would recommend [ArtnetLibs](https://github.com/OpenLightingProject/libartnet) or the [Python Wrapper for Artnet Libs](https://github.com/haum/libartnet) 79 | 80 | ### Art-Net 81 | 82 | Getting things running with protocol is pretty simple. just shove the protocol header into your data array and send it to the right place. 83 | Usually Artnet devices are in the range of 2.x.x.x or 10.x.x.x. This is a convention however is not forcefully implemented. 84 | I have filled the data to represent a ArtDmx packet 85 | 86 | 87 | | Byte | Value | Description | 88 | | -----: | :----: | ------------ | 89 | | 0 | A | Header | 90 | | 1 | r | " | 91 | | 2 | t | " | 92 | | 3 | - | " | 93 | | 4 | N | " | 94 | | 5 | e | " | 95 | | 6 | t | " | 96 | | 7 | 0x00 | " | 97 | | 8 | 0x00 | OpCode Low | 98 | | 9 | 0x500 | OpCode High (ArtDmx) | 99 | | 10 | 0x00 | Protocol V High | 100 | | 11 | 14 | Protocol V Low (currently 14) | 101 | | 12 | 0x00 | Sequence** (0x00 to disable) | 102 | | 13 | int 8 | Physical | 103 | | 14 | int 8 | Sub + Uni *** | 104 | | 15 | int 8 | Net *** | 105 | | 16 | int 8 | Length High (typically 512) | 106 | | 17 | int 8 | Length Low | 107 | | - | - | Append your packet here | 108 | 109 | ** To allow the receiver to ensure packets are received in the right order
110 | *** By spec should look like this: 111 | | Bit 15 | Bits 14-8 | Bits 7-4 | Bits 3-0 | 112 | | :------- | :--------- | :-------- | :-------- | 113 | | 0 | Net | Subnet | Universe | 114 | 115 | Note: This is true for the current version of Artnet 4 (v14), as [defined here](https://artisticlicence.com/WebSiteMaster/User%20Guides/art-net.pdf) 116 | 117 | ### Nets and Subnets 118 | 119 | Artnet uses the concept of Universes and Subnets for data routing. I simplified here defaulting to use a 256 value universe. This will then be divided into low and high uint8 and look correct either way (Universe 17 will be Universe 1 of Subnet 2). In this case the net will always be 0. 120 | This will look correct and behave fine for smaller networks, wanting to be able to specify Universes, Subnets and Nets you can disable simplification and give values as needed.
121 | The spec for Artnet 4 applies here: 128 Nets contain 16 Subnets which contain 16 Universes. 128 * 16 * 16 = 32 768 Universes 122 | 123 | ```python 124 | # Create a StupidArtnet instance with the relevant values 125 | 126 | # By default universe is simplified to a value between 0 - 255 127 | # this should suffice for anything not using subnets 128 | # on sending universe will be masked to two values 129 | # making the use of subnets invisible 130 | 131 | universe = 17 # equivalent to universe 1 subnet 1 132 | a = StupidArtnet(target_ip, universe, packet_size) 133 | 134 | 135 | ############################################# 136 | 137 | # You can also disable simplification 138 | a.set_simplified(False) 139 | 140 | # Add net and subnet value 141 | # Values here are 0 based 142 | a.set_universe(15) 143 | a.set_subnet(15) 144 | a.set_net(127) 145 | ``` 146 | 147 | ### License 148 | 149 | This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details 150 | -------------------------------------------------------------------------------- /examples/example.py: -------------------------------------------------------------------------------- 1 | from stupidArtnet import StupidArtnet 2 | import time 3 | import random 4 | 5 | # THESE ARE MOST LIKELY THE VALUES YOU WILL BE NEEDING 6 | target_ip = '127.0.0.1' # typically in 2.x or 10.x range 7 | universe = 0 # see docs 8 | packet_size = 100 # it is not necessary to send whole universe 9 | # CREATING A STUPID ARTNET OBJECT 10 | # SETUP NEEDS A FEW ELEMENTS 11 | # TARGET_IP = DEFAULT 127.0.0.1 12 | # UNIVERSE = DEFAULT 0 13 | # PACKET_SIZE = DEFAULT 512 14 | # FRAME_RATE = DEFAULT 30 15 | # ISBROADCAST = DEFAULT FALSE 16 | 17 | # By default, the server uses port 6454, no need to specify it. 18 | # If you need to change the Art-Net port, ensure the port is within the valid range for UDP ports (1024-65535). 19 | # Be sure that no other application is using the selected port on your network. 20 | # To specify a different port, for example port 6455, you can do it like this: 21 | # a = StupidArtnetServer(target_ip, universe, packet_size, 30, True, True, port=6455 ) # Change 6455 to any valid port number between 1024 and 65535. 22 | 23 | a = StupidArtnet( target_ip, universe, packet_size, 30, True, True ) 24 | 25 | # MORE ADVANCED CAN BE SET WITH SETTERS IF NEEDED 26 | # NET = DEFAULT 0 27 | # SUBNET = DEFAULT 0 28 | 29 | # CHECK INIT 30 | print(a) 31 | 32 | # YOU CAN CREATE YOUR OWN BYTE ARRAY OF PACKET_SIZE 33 | packet = bytearray(packet_size) # create packet for Artnet 34 | for i in range(packet_size): # fill packet with sequential values 35 | packet[i] = (i % 256) 36 | 37 | # ... AND SET IT TO STUPID ARTNET 38 | a.set(packet) # only on changes 39 | 40 | # ALL PACKETS ARE SAVED IN THE CLASS, YOU CAN CHANGE SINGLE VALUES 41 | a.set_single_value(1, 255) # set channel 1 to 255 42 | 43 | # ... AND SEND 44 | a.show() # send data 45 | 46 | # OR USE STUPIDARTNET FUNCTIONS 47 | a.flash_all() # send single packet with all channels at 255 48 | 49 | time.sleep(1) # wait a bit, 1 sec 50 | 51 | a.blackout() # send single packet with all channels at 0 52 | a.see_buffer() 53 | 54 | # ALL THE ABOVE EXAMPLES SEND A SINGLE DATAPACKET 55 | # STUPIDARTNET IS ALSO THREADABLE 56 | # TO SEND PERSISTANT SIGNAL YOU CAN START THE THREAD 57 | a.start() # start continuos sendin 58 | 59 | # AND MODIFY THE DATA AS YOU GO 60 | for x in range(100): 61 | for i in range(packet_size): # Fill buffer with random stuff 62 | packet[i] = random.randint(0, 255) 63 | a.set(packet) 64 | time.sleep(.2) 65 | 66 | # SOME DEVICES WOULD HOLD LAST DATA, TURN ALL OFF WHEN DONE 67 | 68 | a.blackout() 69 | 70 | # ... REMEMBER TO CLOSE THE THREAD ONCE YOU ARE DONE 71 | a.stop() 72 | 73 | # CLEANUP IN THE END 74 | del a 75 | -------------------------------------------------------------------------------- /examples/example_server.py: -------------------------------------------------------------------------------- 1 | from stupidArtnet import StupidArtnetServer 2 | import time 3 | 4 | 5 | # create a callback to handle data when received 6 | def test_callback(data): 7 | """Test function to receive callback data.""" 8 | # the received data is an array 9 | # of the channels value (no headers) 10 | print('Received new data \n', data) 11 | 12 | 13 | # a Server object initializes with the following data 14 | # universe = DEFAULT 0 15 | # subnet = DEFAULT 0 16 | # net = DEFAULT 0 17 | # setSimplified = DEFAULT True 18 | # callback_function = DEFAULT None 19 | 20 | # You can use universe only 21 | universe = 0 22 | 23 | # By default, the server uses port 6454, no need to specify it. 24 | # If you need to change the Art-Net port, ensure the port is within the valid range for UDP ports (1024-65535) 25 | # Be sure that no other application is using the selected port on your network. 26 | # To specify a different port, for example port 6455, you can do it like this: 27 | # a = StupidArtnetServer(port=6455) # Change 6455 to any valid port number between 1024 and 65535. 28 | 29 | a = StupidArtnetServer() #Create a server with the default port 6454 30 | 31 | 32 | # For every universe we would like to receive, 33 | # add a new listener with a optional callback 34 | # the return is an id for the listener 35 | u1_listener = a.register_listener( 36 | universe, callback_function=test_callback) 37 | 38 | 39 | # or disable simplified mode to use nets and subnets as per spec 40 | # subnet = 1 (would have been universe 17 in simplified mode) 41 | # net = 0 42 | # a.register_listener(universe, sub=subnet, net=net, 43 | # setSimplified=False, callback_function=test_callback) 44 | 45 | 46 | # print object state 47 | print(a) 48 | 49 | # giving it some time for the demo 50 | time.sleep(3) 51 | 52 | # if you prefer not using callbacks, the channel data 53 | # is also available in the method get_buffer() 54 | # use the given id to access it 55 | buffer = a.get_buffer(u1_listener) 56 | 57 | # Remember to check the buffer size, as this may vary from 512 58 | n_data = len(buffer) 59 | if n_data > 0: 60 | # in which channel 1 would be 61 | print('Channel 1: ', buffer[0]) 62 | 63 | # and channel 20 would be 64 | print('Channel 20: ', buffer[19]) 65 | 66 | # Cleanup when you are done 67 | del a 68 | -------------------------------------------------------------------------------- /examples/tkinter_slider_to_artnet.py: -------------------------------------------------------------------------------- 1 | """(Very) Simple Example of using Tkinter with StupidArtnet. 2 | 3 | It creates a simple window with a slider of value 0-255 4 | This value is streamed in universe 0 channel 1 5 | 6 | Note: The example imports stupid artnet locally from 7 | a parent folder, real use import would be simpler 8 | 9 | """ 10 | 11 | from tkinter import * 12 | 13 | from stupidArtnet import StupidArtnet 14 | 15 | # Declare globals 16 | stupid = None 17 | window = None 18 | 19 | 20 | def updateValue(slider_value): 21 | """Callback from slider onchange. 22 | Sends the value of the slider to the artnet channel. 23 | """ 24 | global stupid 25 | stupid.set_single_value(1, int(slider_value)) 26 | 27 | 28 | def cleanup(): 29 | """Cleanup function for when window is closed. 30 | Closes socket and destroys object. 31 | """ 32 | print('cleanup') 33 | 34 | global stupid 35 | stupid.stop() 36 | del stupid 37 | 38 | global window 39 | window.destroy() 40 | 41 | 42 | # ARTNET CODE 43 | # ------------- 44 | 45 | # Create artnet object 46 | stupid = StupidArtnet() 47 | 48 | # Start persistent thread 49 | stupid.start() 50 | 51 | 52 | # TKINTER CODE 53 | # -------------- 54 | 55 | # Create window object 56 | window = Tk() 57 | 58 | # Hold value of the slider 59 | slider_val = IntVar() 60 | 61 | # Create slider 62 | scale = Scale(window, variable=slider_val, 63 | command=updateValue, from_=255, to=0) 64 | scale.pack(anchor=CENTER) 65 | 66 | # Create label with value 67 | label = Label(window) 68 | label.pack() 69 | 70 | # Cleanup on exit 71 | window.protocol("WM_DELETE_WINDOW", cleanup) 72 | 73 | # Start 74 | window.mainloop() 75 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from setuptools import setup 2 | 3 | with open('README.md', 'r', encoding='utf-8') as fh: 4 | long_description = fh.read() 5 | 6 | setup(name='stupidArtnet', 7 | author='cpvalente', 8 | version='1.6.0', 9 | license='MIT', 10 | description='(Very) Simple implementation of the Art-Net protocol', 11 | long_description=long_description, 12 | long_description_content_type='text/markdown', 13 | url='https://github.com/cpvalente/stupidArtnet', 14 | project_urls={ 15 | 'Bug Tracker': 'https://github.com/cpvalente/stupidArtnet/issues', 16 | }, 17 | classifiers=[ 18 | 'Programming Language :: Python :: 3', 19 | 'License :: OSI Approved :: MIT License', 20 | 'Operating System :: OS Independent', 21 | ], 22 | keywords=['LIGHTING', 'DMX', 'LIGHTING CONTROL'], 23 | packages=['stupidArtnet'], 24 | package_data={'stupidArtnet': ['examples/*']}, 25 | python_requires=">=3.6") 26 | -------------------------------------------------------------------------------- /stupidArtnet/.pylintrc: -------------------------------------------------------------------------------- 1 | [MASTER] 2 | 3 | # A comma-separated list of package or module names from where C extensions may 4 | # be loaded. Extensions are loading into the active Python interpreter and may 5 | # run arbitrary code. 6 | extension-pkg-allow-list= 7 | 8 | # A comma-separated list of package or module names from where C extensions may 9 | # be loaded. Extensions are loading into the active Python interpreter and may 10 | # run arbitrary code. (This is an alternative name to extension-pkg-allow-list 11 | # for backward compatibility.) 12 | extension-pkg-whitelist= 13 | 14 | # Return non-zero exit code if any of these messages/categories are detected, 15 | # even if score is above --fail-under value. Syntax same as enable. Messages 16 | # specified are enabled, while categories only check already-enabled messages. 17 | fail-on= 18 | 19 | # Specify a score threshold to be exceeded before program exits with error. 20 | fail-under=10.0 21 | 22 | # Files or directories to be skipped. They should be base names, not paths. 23 | ignore=CVS 24 | 25 | # Add files or directories matching the regex patterns to the ignore-list. The 26 | # regex matches against paths and can be in Posix or Windows format. 27 | ignore-paths= 28 | 29 | # Files or directories matching the regex patterns are skipped. The regex 30 | # matches against base names, not paths. 31 | ignore-patterns= 32 | 33 | # Python code to execute, usually for sys.path manipulation such as 34 | # pygtk.require(). 35 | #init-hook= 36 | 37 | # Use multiple processes to speed up Pylint. Specifying 0 will auto-detect the 38 | # number of processors available to use. 39 | jobs=1 40 | 41 | # Control the amount of potential inferred values when inferring a single 42 | # object. This can help the performance when dealing with large functions or 43 | # complex, nested conditions. 44 | limit-inference-results=100 45 | 46 | # List of plugins (as comma separated values of python module names) to load, 47 | # usually to register additional checkers. 48 | load-plugins= 49 | 50 | # Pickle collected data for later comparisons. 51 | persistent=yes 52 | 53 | # Minimum Python version to use for version dependent checks. Will default to 54 | # the version used to run pylint. 55 | py-version=3.8 56 | 57 | # When enabled, pylint would attempt to guess common misconfiguration and emit 58 | # user-friendly hints instead of false-positive error messages. 59 | suggestion-mode=yes 60 | 61 | # Allow loading of arbitrary C extensions. Extensions are imported into the 62 | # active Python interpreter and may run arbitrary code. 63 | unsafe-load-any-extension=no 64 | 65 | 66 | [MESSAGES CONTROL] 67 | 68 | # Only show warnings with the listed confidence levels. Leave empty to show 69 | # all. Valid levels: HIGH, INFERENCE, INFERENCE_FAILURE, UNDEFINED. 70 | confidence= 71 | 72 | # Disable the message, report, category or checker with the given id(s). You 73 | # can either give multiple identifiers separated by comma (,) or put this 74 | # option multiple times (only on the command line, not in the configuration 75 | # file where it should appear only once). You can also use "--disable=all" to 76 | # disable everything first and then reenable specific checks. For example, if 77 | # you want to run only the similarities checker, you can use "--disable=all 78 | # --enable=similarities". If you want to run only the classes checker, but have 79 | # no Warning level messages displayed, use "--disable=all --enable=classes 80 | # --disable=W". 81 | disable=raw-checker-failed, 82 | bad-inline-option, 83 | locally-disabled, 84 | file-ignored, 85 | suppressed-message, 86 | useless-suppression, 87 | deprecated-pragma, 88 | use-symbolic-message-instead 89 | 90 | # Enable the message, report, category or checker with the given id(s). You can 91 | # either give multiple identifier separated by comma (,) or put this option 92 | # multiple time (only on the command line, not in the configuration file where 93 | # it should appear only once). See also the "--disable" option for examples. 94 | enable=c-extension-no-member 95 | 96 | 97 | [REPORTS] 98 | 99 | # Python expression which should return a score less than or equal to 10. You 100 | # have access to the variables 'error', 'warning', 'refactor', and 'convention' 101 | # which contain the number of messages in each category, as well as 'statement' 102 | # which is the total number of statements analyzed. This score is used by the 103 | # global evaluation report (RP0004). 104 | evaluation=10.0 - ((float(5 * error + warning + refactor + convention) / statement) * 10) 105 | 106 | # Template used to display messages. This is a python new-style format string 107 | # used to format the message information. See doc for all details. 108 | #msg-template= 109 | 110 | # Set the output format. Available formats are text, parseable, colorized, json 111 | # and msvs (visual studio). You can also give a reporter class, e.g. 112 | # mypackage.mymodule.MyReporterClass. 113 | output-format=text 114 | 115 | # Tells whether to display a full report or only the messages. 116 | reports=no 117 | 118 | # Activate the evaluation score. 119 | score=yes 120 | 121 | 122 | [REFACTORING] 123 | 124 | # Maximum number of nested blocks for function / method body 125 | max-nested-blocks=5 126 | 127 | # Complete name of functions that never returns. When checking for 128 | # inconsistent-return-statements if a never returning function is called then 129 | # it will be considered as an explicit return statement and no message will be 130 | # printed. 131 | never-returning-functions=sys.exit,argparse.parse_error 132 | 133 | 134 | [BASIC] 135 | 136 | # Naming style matching correct argument names. 137 | argument-naming-style=snake_case 138 | 139 | # Regular expression matching correct argument names. Overrides argument- 140 | # naming-style. 141 | #argument-rgx= 142 | 143 | # Naming style matching correct attribute names. 144 | attr-naming-style=snake_case 145 | 146 | # Regular expression matching correct attribute names. Overrides attr-naming- 147 | # style. 148 | #attr-rgx= 149 | 150 | # Bad variable names which should always be refused, separated by a comma. 151 | bad-names=foo, 152 | bar, 153 | baz, 154 | toto, 155 | tutu, 156 | tata 157 | 158 | # Bad variable names regexes, separated by a comma. If names match any regex, 159 | # they will always be refused 160 | bad-names-rgxs= 161 | 162 | # Naming style matching correct class attribute names. 163 | class-attribute-naming-style=any 164 | 165 | # Regular expression matching correct class attribute names. Overrides class- 166 | # attribute-naming-style. 167 | #class-attribute-rgx= 168 | 169 | # Naming style matching correct class constant names. 170 | class-const-naming-style=UPPER_CASE 171 | 172 | # Regular expression matching correct class constant names. Overrides class- 173 | # const-naming-style. 174 | #class-const-rgx= 175 | 176 | # Naming style matching correct class names. 177 | class-naming-style=PascalCase 178 | 179 | # Regular expression matching correct class names. Overrides class-naming- 180 | # style. 181 | #class-rgx= 182 | 183 | # Naming style matching correct constant names. 184 | const-naming-style=UPPER_CASE 185 | 186 | # Regular expression matching correct constant names. Overrides const-naming- 187 | # style. 188 | #const-rgx= 189 | 190 | # Minimum line length for functions/classes that require docstrings, shorter 191 | # ones are exempt. 192 | docstring-min-length=-1 193 | 194 | # Naming style matching correct function names. 195 | function-naming-style=snake_case 196 | 197 | # Regular expression matching correct function names. Overrides function- 198 | # naming-style. 199 | #function-rgx= 200 | 201 | # Good variable names which should always be accepted, separated by a comma. 202 | good-names=i, 203 | j, 204 | k, 205 | ex, 206 | Run, 207 | _ 208 | 209 | # Good variable names regexes, separated by a comma. If names match any regex, 210 | # they will always be accepted 211 | good-names-rgxs= 212 | 213 | # Include a hint for the correct naming format with invalid-name. 214 | include-naming-hint=no 215 | 216 | # Naming style matching correct inline iteration names. 217 | inlinevar-naming-style=any 218 | 219 | # Regular expression matching correct inline iteration names. Overrides 220 | # inlinevar-naming-style. 221 | #inlinevar-rgx= 222 | 223 | # Naming style matching correct method names. 224 | method-naming-style=snake_case 225 | 226 | # Regular expression matching correct method names. Overrides method-naming- 227 | # style. 228 | #method-rgx= 229 | 230 | # Naming style matching correct module names. 231 | module-naming-style=snake_case 232 | 233 | # Regular expression matching correct module names. Overrides module-naming- 234 | # style. 235 | #module-rgx= 236 | 237 | # Colon-delimited sets of names that determine each other's naming style when 238 | # the name regexes allow several styles. 239 | name-group= 240 | 241 | # Regular expression which should only match function or class names that do 242 | # not require a docstring. 243 | no-docstring-rgx=^_ 244 | 245 | # List of decorators that produce properties, such as abc.abstractproperty. Add 246 | # to this list to register other decorators that produce valid properties. 247 | # These decorators are taken in consideration only for invalid-name. 248 | property-classes=abc.abstractproperty 249 | 250 | # Naming style matching correct variable names. 251 | variable-naming-style=snake_case 252 | 253 | # Regular expression matching correct variable names. Overrides variable- 254 | # naming-style. 255 | #variable-rgx= 256 | 257 | 258 | [FORMAT] 259 | 260 | # Expected format of line ending, e.g. empty (any line ending), LF or CRLF. 261 | expected-line-ending-format= 262 | 263 | # Regexp for a line that is allowed to be longer than the limit. 264 | ignore-long-lines=^\s*(# )??$ 265 | 266 | # Number of spaces of indent required inside a hanging or continued line. 267 | indent-after-paren=4 268 | 269 | # String used as indentation unit. This is usually " " (4 spaces) or "\t" (1 270 | # tab). 271 | indent-string=' ' 272 | 273 | # Maximum number of characters on a single line. 274 | max-line-length=100 275 | 276 | # Maximum number of lines in a module. 277 | max-module-lines=1000 278 | 279 | # Allow the body of a class to be on the same line as the declaration if body 280 | # contains single statement. 281 | single-line-class-stmt=no 282 | 283 | # Allow the body of an if to be on the same line as the test if there is no 284 | # else. 285 | single-line-if-stmt=no 286 | 287 | 288 | [LOGGING] 289 | 290 | # The type of string formatting that logging methods do. `old` means using % 291 | # formatting, `new` is for `{}` formatting. 292 | logging-format-style=old 293 | 294 | # Logging modules to check that the string format arguments are in logging 295 | # function parameter format. 296 | logging-modules=logging 297 | 298 | 299 | [MISCELLANEOUS] 300 | 301 | # List of note tags to take in consideration, separated by a comma. 302 | notes=FIXME, 303 | XXX, 304 | TODO 305 | 306 | # Regular expression of note tags to take in consideration. 307 | #notes-rgx= 308 | 309 | 310 | [SIMILARITIES] 311 | 312 | # Comments are removed from the similarity computation 313 | ignore-comments=yes 314 | 315 | # Docstrings are removed from the similarity computation 316 | ignore-docstrings=yes 317 | 318 | # Imports are removed from the similarity computation 319 | ignore-imports=no 320 | 321 | # Signatures are removed from the similarity computation 322 | ignore-signatures=no 323 | 324 | # Minimum lines number of a similarity. 325 | min-similarity-lines=4 326 | 327 | 328 | [SPELLING] 329 | 330 | # Limits count of emitted suggestions for spelling mistakes. 331 | max-spelling-suggestions=4 332 | 333 | # Spelling dictionary name. Available dictionaries: none. To make it work, 334 | # install the 'python-enchant' package. 335 | spelling-dict= 336 | 337 | # List of comma separated words that should be considered directives if they 338 | # appear and the beginning of a comment and should not be checked. 339 | spelling-ignore-comment-directives=fmt: on,fmt: off,noqa:,noqa,nosec,isort:skip,mypy: 340 | 341 | # List of comma separated words that should not be checked. 342 | spelling-ignore-words= 343 | 344 | # A path to a file that contains the private dictionary; one word per line. 345 | spelling-private-dict-file= 346 | 347 | # Tells whether to store unknown words to the private dictionary (see the 348 | # --spelling-private-dict-file option) instead of raising a message. 349 | spelling-store-unknown-words=no 350 | 351 | 352 | [STRING] 353 | 354 | # This flag controls whether inconsistent-quotes generates a warning when the 355 | # character used as a quote delimiter is used inconsistently within a module. 356 | check-quote-consistency=no 357 | 358 | # This flag controls whether the implicit-str-concat should generate a warning 359 | # on implicit string concatenation in sequences defined over several lines. 360 | check-str-concat-over-line-jumps=no 361 | 362 | 363 | [TYPECHECK] 364 | 365 | # List of decorators that produce context managers, such as 366 | # contextlib.contextmanager. Add to this list to register other decorators that 367 | # produce valid context managers. 368 | contextmanager-decorators=contextlib.contextmanager 369 | 370 | # List of members which are set dynamically and missed by pylint inference 371 | # system, and so shouldn't trigger E1101 when accessed. Python regular 372 | # expressions are accepted. 373 | generated-members= 374 | 375 | # Tells whether missing members accessed in mixin class should be ignored. A 376 | # class is considered mixin if its name matches the mixin-class-rgx option. 377 | ignore-mixin-members=yes 378 | 379 | # Tells whether to warn about missing members when the owner of the attribute 380 | # is inferred to be None. 381 | ignore-none=yes 382 | 383 | # This flag controls whether pylint should warn about no-member and similar 384 | # checks whenever an opaque object is returned when inferring. The inference 385 | # can return multiple potential results while evaluating a Python object, but 386 | # some branches might not be evaluated, which results in partial inference. In 387 | # that case, it might be useful to still emit no-member and other checks for 388 | # the rest of the inferred objects. 389 | ignore-on-opaque-inference=yes 390 | 391 | # List of class names for which member attributes should not be checked (useful 392 | # for classes with dynamically set attributes). This supports the use of 393 | # qualified names. 394 | ignored-classes=optparse.Values,thread._local,_thread._local 395 | 396 | # List of module names for which member attributes should not be checked 397 | # (useful for modules/projects where namespaces are manipulated during runtime 398 | # and thus existing member attributes cannot be deduced by static analysis). It 399 | # supports qualified module names, as well as Unix pattern matching. 400 | ignored-modules= 401 | 402 | # Show a hint with possible names when a member name was not found. The aspect 403 | # of finding the hint is based on edit distance. 404 | missing-member-hint=yes 405 | 406 | # The minimum edit distance a name should have in order to be considered a 407 | # similar match for a missing member name. 408 | missing-member-hint-distance=1 409 | 410 | # The total number of similar names that should be taken in consideration when 411 | # showing a hint for a missing member. 412 | missing-member-max-choices=1 413 | 414 | # Regex pattern to define which classes are considered mixins ignore-mixin- 415 | # members is set to 'yes' 416 | mixin-class-rgx=.*[Mm]ixin 417 | 418 | # List of decorators that change the signature of a decorated function. 419 | signature-mutators= 420 | 421 | 422 | [VARIABLES] 423 | 424 | # List of additional names supposed to be defined in builtins. Remember that 425 | # you should avoid defining new builtins when possible. 426 | additional-builtins= 427 | 428 | # Tells whether unused global variables should be treated as a violation. 429 | allow-global-unused-variables=yes 430 | 431 | # List of names allowed to shadow builtins 432 | allowed-redefined-builtins= 433 | 434 | # List of strings which can identify a callback function by name. A callback 435 | # name must start or end with one of those strings. 436 | callbacks=cb_, 437 | _cb 438 | 439 | # A regular expression matching the name of dummy variables (i.e. expected to 440 | # not be used). 441 | dummy-variables-rgx=_+$|(_[a-zA-Z0-9_]*[a-zA-Z0-9]+?$)|dummy|^ignored_|^unused_ 442 | 443 | # Argument names that match this expression will be ignored. Default to name 444 | # with leading underscore. 445 | ignored-argument-names=_.*|^ignored_|^unused_ 446 | 447 | # Tells whether we should check for unused import in __init__ files. 448 | init-import=no 449 | 450 | # List of qualified module names which can have objects that can redefine 451 | # builtins. 452 | redefining-builtins-modules=six.moves,past.builtins,future.builtins,builtins,io 453 | 454 | 455 | [CLASSES] 456 | 457 | # Warn about protected attribute access inside special methods 458 | check-protected-access-in-special-methods=no 459 | 460 | # List of method names used to declare (i.e. assign) instance attributes. 461 | defining-attr-methods=__init__, 462 | __new__, 463 | setUp, 464 | __post_init__ 465 | 466 | # List of member names, which should be excluded from the protected access 467 | # warning. 468 | exclude-protected=_asdict, 469 | _fields, 470 | _replace, 471 | _source, 472 | _make 473 | 474 | # List of valid names for the first argument in a class method. 475 | valid-classmethod-first-arg=cls 476 | 477 | # List of valid names for the first argument in a metaclass class method. 478 | valid-metaclass-classmethod-first-arg=cls 479 | 480 | 481 | [DESIGN] 482 | 483 | # List of regular expressions of class ancestor names to ignore when counting 484 | # public methods (see R0903) 485 | exclude-too-few-public-methods= 486 | 487 | # List of qualified class names to ignore when counting class parents (see 488 | # R0901) 489 | ignored-parents= 490 | 491 | # Maximum number of arguments for function / method. 492 | max-args=5 493 | 494 | # Maximum number of attributes for a class (see R0902). 495 | max-attributes=7 496 | 497 | # Maximum number of boolean expressions in an if statement (see R0916). 498 | max-bool-expr=5 499 | 500 | # Maximum number of branch for function / method body. 501 | max-branches=12 502 | 503 | # Maximum number of locals for function / method body. 504 | max-locals=15 505 | 506 | # Maximum number of parents for a class (see R0901). 507 | max-parents=7 508 | 509 | # Maximum number of public methods for a class (see R0904). 510 | max-public-methods=20 511 | 512 | # Maximum number of return / yield for function / method body. 513 | max-returns=6 514 | 515 | # Maximum number of statements in function / method body. 516 | max-statements=50 517 | 518 | # Minimum number of public methods for a class (see R0903). 519 | min-public-methods=2 520 | 521 | 522 | [IMPORTS] 523 | 524 | # List of modules that can be imported at any level, not just the top level 525 | # one. 526 | allow-any-import-level= 527 | 528 | # Allow wildcard imports from modules that define __all__. 529 | allow-wildcard-with-all=no 530 | 531 | # Analyse import fallback blocks. This can be used to support both Python 2 and 532 | # 3 compatible code, which means that the block might have code that exists 533 | # only in one or another interpreter, leading to false positives when analysed. 534 | analyse-fallback-blocks=no 535 | 536 | # Deprecated modules which should not be used, separated by a comma. 537 | deprecated-modules= 538 | 539 | # Output a graph (.gv or any supported image format) of external dependencies 540 | # to the given file (report RP0402 must not be disabled). 541 | ext-import-graph= 542 | 543 | # Output a graph (.gv or any supported image format) of all (i.e. internal and 544 | # external) dependencies to the given file (report RP0402 must not be 545 | # disabled). 546 | import-graph= 547 | 548 | # Output a graph (.gv or any supported image format) of internal dependencies 549 | # to the given file (report RP0402 must not be disabled). 550 | int-import-graph= 551 | 552 | # Force import order to recognize a module as part of the standard 553 | # compatibility libraries. 554 | known-standard-library= 555 | 556 | # Force import order to recognize a module as part of a third party library. 557 | known-third-party=enchant 558 | 559 | # Couples of modules and preferred modules, separated by a comma. 560 | preferred-modules= 561 | 562 | 563 | [EXCEPTIONS] 564 | 565 | # Exceptions that will emit a warning when being caught. Defaults to 566 | # "BaseException, Exception". 567 | overgeneral-exceptions=BaseException, 568 | Exception 569 | -------------------------------------------------------------------------------- /stupidArtnet/ArtnetUtils.py: -------------------------------------------------------------------------------- 1 | """Provides common functions byte objects.""" 2 | 3 | 4 | def shift_this(number, high_first=True): 5 | """Utility method: extracts MSB and LSB from number. 6 | 7 | Args: 8 | number - number to shift 9 | high_first - MSB or LSB first (true / false) 10 | 11 | Returns: 12 | (high, low) - tuple with shifted values 13 | 14 | """ 15 | low = (number & 0xFF) 16 | high = ((number >> 8) & 0xFF) 17 | if high_first: 18 | return((high, low)) 19 | return((low, high)) 20 | 21 | 22 | def clamp(number, min_val, max_val): 23 | """Utility method: sets number in defined range. 24 | 25 | Args: 26 | number - number to use 27 | range_min - lowest possible number 28 | range_max - highest possible number 29 | 30 | Returns: 31 | number - number in correct range 32 | """ 33 | return max(min_val, min(number, max_val)) 34 | 35 | 36 | def set_even(number): 37 | """Utility method: ensures number is even by adding. 38 | 39 | Args: 40 | number - number to make even 41 | 42 | Returns: 43 | number - even number 44 | """ 45 | if number % 2 != 0: 46 | number += 1 47 | return number 48 | 49 | 50 | def put_in_range(number, range_min, range_max, make_even=True): 51 | """Utility method: sets number in defined range. 52 | DEPRECATED: this will be removed from the library 53 | 54 | Args: 55 | number - number to use 56 | range_min - lowest possible number 57 | range_max - highest possible number 58 | make_even - should number be made even 59 | 60 | Returns: 61 | number - number in correct range 62 | 63 | """ 64 | number = clamp(number, range_min, range_max) 65 | if make_even: 66 | number = set_even(number) 67 | return number 68 | 69 | 70 | def make_address_mask(universe, sub=0, net=0, is_simplified=True): 71 | """Returns the address bytes for a given universe, subnet and net. 72 | 73 | Args: 74 | universe - Universe to listen 75 | sub - Subnet to listen 76 | net - Net to listen 77 | is_simplified - Whether to use nets and subnet or universe only, 78 | see User Guide page 5 (Universe Addressing) 79 | 80 | Returns: 81 | bytes - byte mask for given address 82 | 83 | """ 84 | address_mask = bytearray() 85 | 86 | if is_simplified: 87 | # Ensure data is in right range 88 | universe = clamp(universe, 0, 32767) 89 | 90 | # Make mask 91 | msb, lsb = shift_this(universe) # convert to MSB / LSB 92 | address_mask.append(lsb) 93 | address_mask.append(msb) 94 | else: 95 | # Ensure data is in right range 96 | universe = clamp(universe, 0, 15) 97 | sub = clamp(sub, 0, 15) 98 | net = clamp(net, 0, 127) 99 | 100 | # Make mask 101 | address_mask.append(sub << 4 | universe) 102 | address_mask.append(net & 0xFF) 103 | 104 | return address_mask 105 | -------------------------------------------------------------------------------- /stupidArtnet/StupidArtnet.py: -------------------------------------------------------------------------------- 1 | """(Very) Simple Implementation of Artnet. 2 | 3 | Python Version: 3.6 4 | Source: http://artisticlicence.com/WebSiteMaster/User%20Guides/art-net.pdf 5 | 6 | NOTES 7 | - For simplicity: NET and SUBNET not used by default but optional 8 | 9 | """ 10 | 11 | import socket 12 | import _thread 13 | from time import sleep 14 | from stupidArtnet.ArtnetUtils import shift_this, put_in_range 15 | 16 | 17 | class StupidArtnet(): 18 | """(Very) simple implementation of Artnet.""" 19 | 20 | def __init__(self, target_ip='127.0.0.1', universe=0, packet_size=512, fps=30, 21 | even_packet_size=True, broadcast=False, source_address=None, artsync=False, port=6454): 22 | """Initializes Art-Net Client. 23 | 24 | Args: 25 | targetIP - IP of receiving device 26 | universe - universe to listen 27 | packet_size - amount of channels to transmit 28 | fps - transmition rate 29 | even_packet_size - Some receivers enforce even packets 30 | broadcast - whether to broadcast in local sub 31 | artsync - if we want to synchronize buffer 32 | port - UDP port used to send Art-Net packets (default: 6454) 33 | 34 | Returns: 35 | None 36 | 37 | """ 38 | # Instance variables 39 | self.target_ip = target_ip 40 | self.sequence = 0 41 | self.physical = 0 42 | self.universe = universe 43 | self.subnet = 0 44 | self.if_sync = artsync 45 | self.net = 0 46 | self.packet_size = put_in_range(packet_size, 2, 512, even_packet_size) 47 | self.packet_header = bytearray() 48 | self.buffer = bytearray(self.packet_size) 49 | self.port = port 50 | # Use provided port or default 6454 51 | # By default, the server uses port 6454, no need to specify it. 52 | # If you need to change the Art-Net port, ensure the port is within the valid range for UDP ports (1024-65535). 53 | # Be sure that no other application is using the selected port on your network. 54 | 55 | self.make_even = even_packet_size 56 | 57 | self.is_simplified = True # simplify use of universe, net and subnet 58 | 59 | # UDP SOCKET 60 | self.socket_client = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) 61 | 62 | if broadcast: 63 | self.socket_client.setsockopt( 64 | socket.SOL_SOCKET, socket.SO_BROADCAST, 1) 65 | 66 | # Allow speciying the origin interface 67 | if source_address: 68 | self.socket_client.setsockopt( 69 | socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) 70 | self.socket_client.bind(source_address) 71 | 72 | # Timer 73 | self.fps = fps 74 | self.__clock = None 75 | 76 | self.make_artdmx_header() 77 | 78 | if self.if_sync: 79 | self.artsync_header = bytearray() 80 | self.make_artsync_header() 81 | 82 | 83 | def __del__(self): 84 | """Graceful shutdown.""" 85 | self.stop() 86 | self.close() 87 | 88 | 89 | def __str__(self): 90 | """Printable object state.""" 91 | state = "===================================\n" 92 | state += "Stupid Artnet initialized\n" 93 | state += f"Target IP: {self.target_ip} : {self.port} \n" 94 | state += f"Universe: {self.universe} \n" 95 | if not self.is_simplified: 96 | state += f"Subnet: {self.subnet} \n" 97 | state += f"Net: {self.net} \n" 98 | state += f"Packet Size: {self.packet_size} \n" 99 | state += "===================================" 100 | 101 | return state 102 | 103 | 104 | def make_artdmx_header(self): 105 | """Make packet header.""" 106 | # 0 - id (7 x bytes + Null) 107 | self.packet_header = bytearray() 108 | self.packet_header.extend(bytearray('Art-Net', 'utf8')) 109 | self.packet_header.append(0x0) 110 | # 8 - opcode (2 x 8 low byte first) 111 | self.packet_header.append(0x00) 112 | self.packet_header.append(0x50) # ArtDmx data packet 113 | # 10 - prototocol version (2 x 8 high byte first) 114 | self.packet_header.append(0x0) 115 | self.packet_header.append(14) 116 | # 12 - sequence (int 8), NULL for not implemented 117 | self.packet_header.append(self.sequence) 118 | # 13 - physical port (int 8) 119 | self.packet_header.append(0x00) 120 | # 14 - universe, (2 x 8 low byte first) 121 | if self.is_simplified: 122 | # not quite correct but good enough for most cases: 123 | # the whole net subnet is simplified 124 | # by transforming a single uint16 into its 8 bit parts 125 | # you will most likely not see any differences in small networks 126 | msb, lsb = shift_this(self.universe) # convert to MSB / LSB 127 | self.packet_header.append(lsb) 128 | self.packet_header.append(msb) 129 | # 14 - universe, subnet (2 x 4 bits each) 130 | # 15 - net (7 bit value) 131 | else: 132 | # as specified in Artnet 4 (remember to set the value manually after): 133 | # Bit 3 - 0 = Universe (1-16) 134 | # Bit 7 - 4 = Subnet (1-16) 135 | # Bit 14 - 8 = Net (1-128) 136 | # Bit 15 = 0 137 | # this means 16 * 16 * 128 = 32768 universes per port 138 | # a subnet is a group of 16 Universes 139 | # 16 subnets will make a net, there are 128 of them 140 | self.packet_header.append(self.subnet << 4 | self.universe) 141 | self.packet_header.append(self.net & 0xFF) 142 | # 16 - packet size (2 x 8 high byte first) 143 | msb, lsb = shift_this(self.packet_size) # convert to MSB / LSB 144 | self.packet_header.append(msb) 145 | self.packet_header.append(lsb) 146 | 147 | 148 | def make_artsync_header(self): 149 | """Make ArtSync header""" 150 | self.artsync_header = bytearray() # Initialize as empty bytearray 151 | # ID: Array of 8 characters, the final character is a null termination. 152 | self.artsync_header.extend(bytearray('Art-Net', 'utf8')) 153 | self.artsync_header.append(0x0) 154 | # OpCode: Defines the class of data within this UDP packet. Transmitted low byte first. 155 | self.artsync_header.append(0x00) 156 | self.artsync_header.append(0x52) 157 | # ProtVerHi and ProtVerLo: Art-Net protocol revision number. Current value =14. 158 | # Controllers should ignore communication with nodes using a protocol version lower than =14. 159 | self.artsync_header.append(0x0) 160 | self.artsync_header.append(14) 161 | # Aux1 and Aux2: Should be transmitted as zero. 162 | self.artsync_header.append(0x0) 163 | self.artsync_header.append(0x0) 164 | 165 | 166 | def send_artsync(self): 167 | """Send Artsync""" 168 | self.make_artsync_header() 169 | try: 170 | self.socket_client.sendto(self.artsync_header, (self.target_ip, self.port)) 171 | except socket.error as error: 172 | print(f"ERROR: Socket error with exception: {error}") 173 | 174 | 175 | def show(self): 176 | """Finally send data.""" 177 | packet = bytearray() 178 | packet.extend(self.packet_header) 179 | packet.extend(self.buffer) 180 | try: 181 | self.socket_client.sendto(packet, (self.target_ip, self.port)) 182 | if self.if_sync: # if we want to send artsync 183 | self.send_artsync() 184 | except socket.error as error: 185 | print(f"ERROR: Socket error with exception: {error}") 186 | finally: 187 | self.sequence = (self.sequence + 1) % 256 188 | 189 | 190 | def close(self): 191 | """Close UDP socket.""" 192 | self.socket_client.close() 193 | 194 | # THREADING # 195 | 196 | def start(self): 197 | """Starts thread clock.""" 198 | self.show() 199 | if not hasattr(self, "running"): 200 | self.running = True 201 | if self.running: 202 | sleep((1000.0 / self.fps) / 1000.0) 203 | _thread.start_new_thread(self.start, ()) 204 | 205 | 206 | def stop(self): 207 | """Set flag so thread will exit.""" 208 | self.running = False 209 | 210 | # SETTERS - HEADER # 211 | 212 | def set_universe(self, universe): 213 | """Setter for universe (0 - 15 / 256). 214 | 215 | Mind if protocol has been simplified 216 | """ 217 | # This is ugly, trying to keep interface easy 218 | # With simplified mode the universe will be split into two 219 | # values, (uni and sub) which is correct anyway. Net will always be 0 220 | if self.is_simplified: 221 | self.universe = put_in_range(universe, 0, 255, False) 222 | else: 223 | self.universe = put_in_range(universe, 0, 15, False) 224 | self.make_artdmx_header() 225 | 226 | 227 | def set_subnet(self, sub): 228 | """Setter for subnet address (0 - 15). 229 | 230 | Set simplify to false to use 231 | """ 232 | self.subnet = put_in_range(sub, 0, 15, False) 233 | self.make_artdmx_header() 234 | 235 | 236 | def set_net(self, net): 237 | """Setter for net address (0 - 127). 238 | 239 | Set simplify to false to use 240 | """ 241 | self.net = put_in_range(net, 0, 127, False) 242 | self.make_artdmx_header() 243 | 244 | 245 | def set_packet_size(self, packet_size): 246 | """Setter for packet size (2 - 512, even only).""" 247 | self.packet_size = put_in_range(packet_size, 2, 512, self.make_even) 248 | self.make_artdmx_header() 249 | 250 | # SETTERS - DATA # 251 | 252 | def clear(self): 253 | """Clear DMX buffer.""" 254 | self.buffer = bytearray(self.packet_size) 255 | 256 | 257 | def set(self, value): 258 | """Set buffer.""" 259 | if len(value) != self.packet_size: 260 | print("ERROR: packet does not match declared packet size") 261 | return 262 | self.buffer = value 263 | 264 | 265 | def set_16bit(self, address, value, high_first=False): 266 | """Set single 16bit value in DMX buffer.""" 267 | if address > self.packet_size: 268 | print("ERROR: Address given greater than defined packet size") 269 | return 270 | if address < 1 or address > 512 - 1: 271 | print("ERROR: Address out of range") 272 | return 273 | value = put_in_range(value, 0, 65535, False) 274 | 275 | # Check for endianess 276 | if high_first: 277 | self.buffer[address - 1] = (value >> 8) & 0xFF # high 278 | self.buffer[address] = (value) & 0xFF # low 279 | else: 280 | self.buffer[address - 1] = (value) & 0xFF # low 281 | self.buffer[address] = (value >> 8) & 0xFF # high 282 | 283 | 284 | def set_single_value(self, address, value): 285 | """Set single value in DMX buffer.""" 286 | if address > self.packet_size: 287 | print("ERROR: Address given greater than defined packet size") 288 | return 289 | if address < 1 or address > 512: 290 | print("ERROR: Address out of range") 291 | return 292 | self.buffer[address - 1] = put_in_range(value, 0, 255, False) 293 | 294 | 295 | def set_single_rem(self, address, value): 296 | """Set single value while blacking out others.""" 297 | if address > self.packet_size: 298 | print("ERROR: Address given greater than defined packet size") 299 | return 300 | if address < 1 or address > 512: 301 | print("ERROR: Address out of range") 302 | return 303 | self.clear() 304 | self.buffer[address - 1] = put_in_range(value, 0, 255, False) 305 | 306 | 307 | def set_rgb(self, address, red, green, blue): 308 | """Set RGB from start address.""" 309 | if address > self.packet_size: 310 | print("ERROR: Address given greater than defined packet size") 311 | return 312 | if address < 1 or address > 510: 313 | print("ERROR: Address out of range") 314 | return 315 | 316 | self.buffer[address - 1] = put_in_range(red, 0, 255, False) 317 | self.buffer[address] = put_in_range(green, 0, 255, False) 318 | self.buffer[address + 1] = put_in_range(blue, 0, 255, False) 319 | 320 | # AUX Function # 321 | 322 | def send(self, packet): 323 | """Set buffer and send straightaway. 324 | 325 | Args: 326 | array - integer array to send 327 | """ 328 | self.set(packet) 329 | self.show() 330 | 331 | 332 | def set_simplified(self, simplify): 333 | """Builds Header accordingly. 334 | 335 | True - Header sends 16 bit universe value (OK but incorrect) 336 | False - Headers sends Universe - Net and Subnet values as protocol 337 | It is recommended that you set these values with .set_net() and set_physical 338 | """ 339 | # avoid remaking header if there are no changes 340 | if simplify == self.is_simplified: 341 | return 342 | self.is_simplified = simplify 343 | self.make_artdmx_header() 344 | 345 | 346 | def see_header(self): 347 | """Show header values.""" 348 | print(self.packet_header) 349 | 350 | 351 | def see_buffer(self): 352 | """Show buffer values.""" 353 | print(self.buffer) 354 | 355 | 356 | def blackout(self): 357 | """Sends 0's all across.""" 358 | self.clear() 359 | self.show() 360 | 361 | 362 | def flash_all(self, delay=None): 363 | """Sends 255's all across.""" 364 | self.set([255] * self.packet_size) 365 | self.show() 366 | # Blackout after delay 367 | if delay: 368 | sleep(delay) 369 | self.blackout() 370 | 371 | 372 | if __name__ == '__main__': 373 | print("===================================") 374 | print("Namespace run") 375 | TARGET_IP = '127.0.0.1' # typically in 2.x or 10.x range 376 | UNIVERSE_TO_SEND = 15 # see docs 377 | PACKET_SIZE = 20 # it is not necessary to send whole universe 378 | 379 | a = StupidArtnet(TARGET_IP, UNIVERSE_TO_SEND, PACKET_SIZE, artsync=True) 380 | a.set_simplified(False) 381 | a.set_net(129) 382 | a.set_subnet(16) 383 | 384 | # Look at the object state 385 | print(a) 386 | 387 | a.set_single_value(13, 255) 388 | a.set_single_value(14, 100) 389 | a.set_single_value(15, 200) 390 | 391 | print("Sending values") 392 | a.show() 393 | a.see_buffer() 394 | a.flash_all() 395 | a.see_buffer() 396 | a.show() 397 | 398 | print("Values sent") 399 | 400 | # Cleanup when you are done 401 | del a 402 | -------------------------------------------------------------------------------- /stupidArtnet/StupidArtnetServer.py: -------------------------------------------------------------------------------- 1 | """(Very) Simple Implementation of Artnet. 2 | 3 | Python Version: 3.6 4 | Source: http://artisticlicence.com/WebSiteMaster/User%20Guides/art-net.pdf 5 | 6 | 7 | NOTES 8 | - For simplicity: NET and SUBNET not used by default but optional 9 | 10 | """ 11 | 12 | import socket 13 | import _thread 14 | from stupidArtnet.ArtnetUtils import make_address_mask 15 | 16 | 17 | class StupidArtnetServer(): 18 | """(Very) simple implementation of an Artnet Server.""" 19 | 20 | socket_server = None 21 | ARTDMX_HEADER = b'Art-Net\x00\x00P\x00\x0e' 22 | listeners = [] 23 | 24 | def __init__(self, port=6454): 25 | """Initializes Art-Net server.""" 26 | self.port = port # Use provided port or default 27 | # By default, the server uses port 6454, no need to specify it. 28 | # If you need to change the Art-Net port, ensure the port is within the valid range for UDP ports (1024-65535). 29 | # Be sure that no other application is using the selected port on your network. 30 | 31 | # server active flag 32 | self.listen = True 33 | 34 | self.server_thread = _thread.start_new_thread(self.__init_socket, ()) 35 | 36 | def __init_socket(self): 37 | """Initializes server socket.""" 38 | # Bind to UDP on the correct PORT 39 | self.socket_server = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) 40 | self.socket_server.setsockopt( 41 | socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) 42 | self.socket_server.bind(('', self.port)) # Listen on any valid IP 43 | 44 | while self.listen: 45 | 46 | data, unused_address = self.socket_server.recvfrom(1024) 47 | 48 | # only dealing with Art-Net DMX 49 | if self.validate_header(data): 50 | 51 | # check if this address is in any registered listener 52 | for listener in self.listeners: 53 | 54 | # is it the address we are listening to 55 | if listener['address_mask'] == data[14:16]: 56 | 57 | # check if the packet we've received is old 58 | new_seq = data[12] 59 | old_seq = listener['sequence'] 60 | # if there's a >50% packet loss it's not our problem 61 | if new_seq == 0x00 or new_seq > old_seq or old_seq - new_seq > 0x80: 62 | listener['sequence'] = new_seq 63 | 64 | listener['buffer'] = list(data)[18:] 65 | 66 | # check for registered callbacks 67 | callback = listener['callback'] 68 | if callback is not None: 69 | # choose the correct callback call based 70 | # on the number of the function's parameters 71 | try: 72 | from inspect import signature 73 | params = signature(callback).parameters 74 | params_len = len(params) 75 | except ImportError: 76 | params_len = 2 77 | 78 | if params_len == 1: 79 | callback(listener['buffer']) 80 | elif params_len == 2: 81 | addr_mask = listener['address_mask'] 82 | addr = int.from_bytes(addr_mask, 'little') 83 | callback(listener['buffer'], addr) 84 | 85 | def __del__(self): 86 | """Graceful shutdown.""" 87 | self.listeners.clear() 88 | self.close() 89 | 90 | def __str__(self): 91 | """Printable object state.""" 92 | state = "===================================\n" 93 | state += "Stupid Artnet Listening\n" 94 | return state 95 | 96 | def register_listener(self, universe=0, sub=0, net=0, 97 | is_simplified=True, callback_function=None): 98 | """Adds a listener to an Art-Net Universe. 99 | 100 | Args: 101 | universe - Universe to listen 102 | sub - Subnet to listen 103 | net - Net to listen 104 | is_simplified - Whether to use nets and subnet or universe only, 105 | see User Guide page 5 (Universe Addressing) 106 | callback_function - Function to call when new packet is received 107 | 108 | Returns: 109 | id - id of listener, used to delete listener if required 110 | """ 111 | listener_id = len(self.listeners) 112 | new_listener = { 113 | 'id': listener_id, 114 | 'simplified': is_simplified, 115 | 'address_mask': make_address_mask(universe, sub, net, is_simplified), 116 | 'callback': callback_function, 117 | 'buffer': [], 118 | 'sequence': 0 119 | } 120 | 121 | self.listeners.append(new_listener) 122 | 123 | return listener_id 124 | 125 | def delete_listener(self, listener_id): 126 | """Deletes a registered listener. 127 | 128 | Args: 129 | listener_id - Id of listener to delete 130 | 131 | Returns: 132 | None 133 | """ 134 | self.listeners = [ 135 | i for i in self.listeners if not i['id'] == listener_id] 136 | 137 | def delete_all_listener(self): 138 | """Deletes all registered listeners. 139 | 140 | Returns: 141 | None 142 | """ 143 | self.listeners = [] 144 | 145 | def see_buffer(self, listener_id): 146 | """Show buffer values.""" 147 | for listener in self.listeners: 148 | if listener.get('id') == listener_id: 149 | return listener.get('buffer') 150 | 151 | return "Listener not found" 152 | 153 | def get_buffer(self, listener_id): 154 | """Return buffer values.""" 155 | for listener in self.listeners: 156 | if listener.get('id') == listener_id: 157 | return listener.get('buffer') 158 | print("Buffer object not found") 159 | return [] 160 | 161 | def clear_buffer(self, listener_id): 162 | """Clear buffer in listener.""" 163 | for listener in self.listeners: 164 | if listener.get('id') == listener_id: 165 | listener['buffer'] = [] 166 | 167 | def set_callback(self, listener_id, callback_function): 168 | """Add / change callback to a given listener.""" 169 | for listener in self.listeners: 170 | if listener.get('id') == listener_id: 171 | listener['callback'] = callback_function 172 | 173 | def set_address_filter(self, listener_id, universe, sub=0, net=0, 174 | is_simplified=True): 175 | """Add / change filter to existing listener.""" 176 | # make mask bytes 177 | address_mask = make_address_mask( 178 | universe, sub, net, is_simplified) 179 | 180 | # find listener 181 | for listener in self.listeners: 182 | if listener.get('id') == listener_id: 183 | listener['simplified'] = is_simplified 184 | listener['address_mask'] = address_mask 185 | listener['buffer'] = [] 186 | 187 | def close(self): 188 | """Close UDP socket.""" 189 | self.listen = False # Set flag, so thread will exit 190 | 191 | @staticmethod 192 | def validate_header(header): 193 | """Validates packet header as Art-Net packet. 194 | 195 | - The packet header spells Art-Net 196 | - The definition is for DMX Artnet (OPCode 0x50) 197 | - The protocol version is 15 198 | 199 | Args: 200 | header - Packet header as bytearray 201 | 202 | Returns: 203 | boolean - comparison value 204 | 205 | """ 206 | return header[:12] == StupidArtnetServer.ARTDMX_HEADER 207 | 208 | 209 | def test_callback(data): 210 | """Test function, receives data from server callback.""" 211 | print('Received new data \n', data) 212 | 213 | 214 | if __name__ == '__main__': 215 | 216 | import time 217 | 218 | print("===================================") 219 | print("Namespace run") 220 | 221 | # Art-Net 4 definition specifies nets and subnets 222 | # Please see README and Art-Net user guide for details 223 | # Here we use the simplified default 224 | UNIVERSE_TO_LISTEN = 1 225 | 226 | # Initilize server, this starts a server in the Art-Net port 227 | a = StupidArtnetServer() 228 | 229 | # For every universe we would like to receive, 230 | # add a new listener with a optional callback 231 | # the return is an id for the listener 232 | u1_listener = a.register_listener( 233 | UNIVERSE_TO_LISTEN, callback_function=test_callback) 234 | 235 | # print object state 236 | print(a) 237 | 238 | # giving it some time for the demo 239 | time.sleep(3) 240 | 241 | # use the listener address to get data without a callback 242 | buffer = a.get_buffer(u1_listener) 243 | 244 | # Cleanup when you are done 245 | del a 246 | -------------------------------------------------------------------------------- /stupidArtnet/__init__.py: -------------------------------------------------------------------------------- 1 | """Facilitates library imports.""" 2 | from stupidArtnet.StupidArtnetServer import StupidArtnetServer 3 | from stupidArtnet.ArtnetUtils import shift_this, put_in_range, make_address_mask 4 | from .StupidArtnet import StupidArtnet 5 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cpvalente/stupidArtnet/fe89b95b98c4280ba717fa6220f388389454e986/tests/__init__.py -------------------------------------------------------------------------------- /tests/test_client.py: -------------------------------------------------------------------------------- 1 | import socket 2 | import unittest 3 | 4 | from stupidArtnet import StupidArtnet 5 | 6 | 7 | class Test(unittest.TestCase): 8 | """Test class for Artnet client.""" 9 | 10 | # Art-Net stuff 11 | header_size = 18 12 | 13 | def setUp(self): 14 | """Creates UDP Server and Art-Net Client.""" 15 | # Create dummy UDP Server 16 | self.sock = socket.socket( 17 | family=socket.AF_INET, type=socket.SOCK_DGRAM) 18 | self.sock.bind(('localhost', 6454)) 19 | self.sock.settimeout(2000) 20 | 21 | # Instanciate stupidArtnet 22 | self.stupid = StupidArtnet(packet_size=24) 23 | 24 | # define a packet to send 25 | data = list(range(25)) 26 | 27 | # send packet 28 | self.stupid.send(data) 29 | 30 | # confirm result 31 | self.received = self.sock.recv(512) 32 | 33 | def tearDown(self): 34 | """Destroy Objects.""" 35 | # destroy UDP Server 36 | self.sock.close() 37 | 38 | # destroy artnet instance 39 | del self.stupid 40 | 41 | def test_header(self): 42 | """Assert Art-Net header.""" 43 | # Art-Net header 44 | self.assertTrue(self.received.startswith(b'Art-Net')) 45 | 46 | def test_zero(self): 47 | """Check first data value, should be 0.""" 48 | self.assertEqual(self.received[self.header_size], 0) 49 | 50 | def test_twelve(self): 51 | """Check twelfth data value, should be 12.""" 52 | self.assertEqual(self.received[self.header_size] + 12, 12) 53 | 54 | def test_twentyfour(self): 55 | """Check twenty-fourth data value, should be 24.""" 56 | self.assertEqual(self.received[self.header_size] + 24, 24) 57 | 58 | 59 | if __name__ == '__main__': 60 | unittest.main() 61 | -------------------------------------------------------------------------------- /tests/test_server.py: -------------------------------------------------------------------------------- 1 | import time 2 | import socket 3 | import unittest 4 | 5 | from stupidArtnet import StupidArtnetServer 6 | 7 | 8 | class Test(unittest.TestCase): 9 | """Test class for Artnet server.""" 10 | 11 | artnet_header = b'Art-Net\x00\x00P\x00\x0e\x00\x00\x00\x00\x00\x08' 12 | dmx_packet = [1, 2, 3, 4, 5, 6, 7, 8] 13 | dmx_packet_bytes = b'\x01\x02\x03\x04\x05\x06\x07\x08' 14 | 15 | def setUp(self): 16 | """Creates UDP Client.""" 17 | # Create a dummy UDP Client 18 | self.sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) 19 | 20 | # Instanciate StupidArtnetServer 21 | self.stupid = StupidArtnetServer() 22 | 23 | print(self.stupid) 24 | 25 | # register listener on universe 0 26 | self.listener = self.stupid.register_listener() 27 | 28 | # make an artnet packet on universe 0 29 | artnet_data = bytearray() 30 | artnet_data.extend(self.artnet_header) 31 | artnet_data.extend(self.dmx_packet_bytes) 32 | 33 | time.sleep(0.5) 34 | 35 | # send artnet data 36 | self.sock.sendto(artnet_data, ('localhost', 6454)) 37 | 38 | time.sleep(0.2) 39 | 40 | # send random stuff 41 | self.sock.sendto(b'Hello world', ('localhost', 6454)) 42 | 43 | def tearDown(self): 44 | """Destroy Objects.""" 45 | # destroy UDP Server 46 | self.sock.close() 47 | 48 | # destroy artnet instance 49 | del self.stupid 50 | 51 | def test_buffer(self): 52 | """Assert that server received data and filtered correctly.""" 53 | buffer = self.stupid.get_buffer(self.listener) 54 | 55 | # Test with a artnet header 56 | self.assertEqual(buffer, self.dmx_packet) 57 | 58 | def test_header(self): 59 | """Assert Art-Net header.""" 60 | artdmx = b'Art-Net\x00\x00P\x00\x0e' 61 | typo = b'Art-Net\x00\x00\x00\x0e' 62 | 63 | # Test with a artnet header 64 | self.assertTrue(StupidArtnetServer.validate_header(artdmx)) 65 | 66 | # Test header with typo on OP code 67 | self.assertFalse(StupidArtnetServer.validate_header(typo)) 68 | 69 | 70 | if __name__ == '__main__': 71 | unittest.main() 72 | -------------------------------------------------------------------------------- /tests/test_utils.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | from stupidArtnet.ArtnetUtils import shift_this, put_in_range, make_address_mask 3 | 4 | 5 | class Test(unittest.TestCase): 6 | """Test class for Artnet Utilities.""" 7 | 8 | def test_shift(self): 9 | """Test shift_this utility.""" 10 | # send a 1 11 | self.assertEqual(shift_this(1), (0, 1)) 12 | 13 | # send a 17 14 | self.assertEqual(shift_this(17), (0, 17)) 15 | 16 | # send a 128 17 | self.assertEqual(shift_this(128), (0, 128)) 18 | 19 | # send a 512 20 | self.assertEqual(shift_this(512), (2, 0)) 21 | 22 | # send a 765 23 | self.assertEqual(shift_this(765), (2, 253)) 24 | 25 | # send a 1024 26 | self.assertEqual(shift_this(1024), (4, 0)) 27 | 28 | return True 29 | 30 | def test_range(self): 31 | """Test put_in_range utility.""" 32 | # send a 1 from 0, 512, even 33 | self.assertEqual(put_in_range(1, 0, 512), (2)) 34 | 35 | # send a 17 from 0, 18, even 36 | self.assertEqual(put_in_range(17, 0, 18), (18)) 37 | 38 | # send a 128 from 0, 127 39 | self.assertEqual(put_in_range(128, 0, 127, False), (127)) 40 | 41 | # send a 512 from 0, 512 42 | self.assertEqual(put_in_range(512, 0, 512), (512)) 43 | 44 | # send a 1024 from 0, 512 45 | self.assertEqual(put_in_range(1024, 0, 512), (512)) 46 | 47 | return True 48 | 49 | def test_mask(self): 50 | """Test mask and assert simplified mode.""" 51 | self.assertEqual(make_address_mask(8), b'\x08\x00') 52 | self.assertEqual(make_address_mask(8, 0, 0, False), b'\x08\x00') 53 | 54 | self.assertEqual(make_address_mask(15), b'\x0f\x00') 55 | self.assertEqual(make_address_mask(15, 0, 0, False), b'\x0f\x00') 56 | 57 | self.assertEqual(make_address_mask(16), b'\x10\x00') 58 | self.assertEqual(make_address_mask(0, 1, 0, False), b'\x10\x00') 59 | 60 | self.assertEqual(make_address_mask(17), b'\x11\x00') 61 | self.assertEqual(make_address_mask(1, 1, 0, False), b'\x11\x00') 62 | 63 | self.assertEqual(make_address_mask(18), b'\x12\x00') 64 | self.assertEqual(make_address_mask(2, 1, 0, False), b'\x12\x00') 65 | self.assertEqual(2 + 1 * 16, 18) 66 | 67 | self.assertEqual(make_address_mask(99), b'c\x00') 68 | self.assertEqual(make_address_mask(3, 6, 0, False), b'c\x00') 69 | self.assertEqual(3 + 6 * 16, 99) 70 | 71 | self.assertEqual(make_address_mask(255), b'\xff\x00') 72 | self.assertEqual(make_address_mask(15, 15, 0, False), b'\xff\x00') 73 | self.assertEqual(15 + 15 * 16, 255) 74 | 75 | self.assertEqual(make_address_mask(256), b'\x00\x01') 76 | self.assertEqual(make_address_mask(0, 0, 1, False), b'\x00\x01') 77 | 78 | self.assertEqual(make_address_mask(257), b'\x01\x01') 79 | self.assertEqual(make_address_mask(1, 0, 1, False), b'\x01\x01') 80 | 81 | # with nets, it becomes difficult 82 | # to use the straight universe number 83 | self.assertEqual(make_address_mask(25736), b'\x88d') 84 | self.assertEqual(make_address_mask(8, 8, 100, False), b'\x88d') 85 | 86 | # Test clamp min 87 | self.assertEqual(make_address_mask(0), b'\x00\x00') 88 | self.assertEqual(make_address_mask(-15), b'\x00\x00') 89 | 90 | # Test clamp max 91 | self.assertEqual(make_address_mask(32767), b'\xff\x7f') 92 | self.assertEqual(make_address_mask(15, 15, 256, False), b'\xff\x7f') 93 | self.assertEqual(make_address_mask(999999), b'\xff\x7f') 94 | self.assertEqual(make_address_mask(99, 99, 300, False), b'\xff\x7f') 95 | 96 | return True 97 | 98 | 99 | if __name__ == '__main__': 100 | unittest.main() 101 | --------------------------------------------------------------------------------