├── .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 | [](https://github.com/cpvalente/stupidArtnet/actions/workflows/publish-to-pypi.yml)
2 | [](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 |
--------------------------------------------------------------------------------