├── .gitignore ├── LICENSE ├── MANIFEST.in ├── README.md ├── dmx ├── __init__.py └── dmx.py ├── documentation ├── API.rst ├── Makefile ├── conf.py ├── history.rst ├── index.rst ├── installation.rst ├── make.bat ├── readme.rst └── usage.rst ├── pyproject.toml ├── setup.cfg ├── setup.py └── tests ├── __init__.py └── test_dmx.py /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | *.c 9 | 10 | # Distribution / packaging 11 | .Python 12 | env/ 13 | build/ 14 | develop-eggs/ 15 | dist/ 16 | downloads/ 17 | eggs/ 18 | .eggs/ 19 | lib/ 20 | lib64/ 21 | parts/ 22 | sdist/ 23 | var/ 24 | wheels/ 25 | *.egg-info/ 26 | .installed.cfg 27 | *.egg 28 | 29 | # PyInstaller 30 | # Usually these files are written by a python script from a template 31 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 32 | *.manifest 33 | *.spec 34 | 35 | # Installer logs 36 | pip-log.txt 37 | pip-delete-this-directory.txt 38 | 39 | # Unit test / coverage reports 40 | htmlcov/ 41 | .tox/ 42 | .coverage 43 | .coverage.* 44 | .cache 45 | nosetests.xml 46 | coverage.xml 47 | *.cover 48 | .hypothesis/ 49 | .pytest_cache/ 50 | 51 | # Translations 52 | *.mo 53 | *.pot 54 | 55 | # Django stuff: 56 | *.log 57 | local_settings.py 58 | 59 | # Flask stuff: 60 | instance/ 61 | .webassets-cache 62 | 63 | # Scrapy stuff: 64 | .scrapy 65 | 66 | # Sphinx documentation 67 | docs/_build/ 68 | 69 | # PyBuilder 70 | target/ 71 | 72 | # Jupyter Notebook 73 | .ipynb_checkpoints 74 | 75 | # pyenv 76 | .python-version 77 | 78 | # celery beat schedule file 79 | celerybeat-schedule 80 | 81 | # SageMath parsed files 82 | *.sage.py 83 | 84 | # dotenv 85 | .env 86 | 87 | # virtualenv 88 | .venv 89 | venv/ 90 | ENV/ 91 | 92 | # Spyder project settings 93 | .spyderproject 94 | .spyproject 95 | 96 | # Rope project settings 97 | .ropeproject 98 | 99 | # mkdocs documentation 100 | /site 101 | 102 | # mypy 103 | .mypy_cache/ 104 | 105 | # IDE settings 106 | .vscode/ 107 | 108 | # Pycharm 109 | .idea 110 | /python/ipt/ipt.c 111 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | BSD 3-Clause License 2 | 3 | Copyright (c) 2021, Rune Monzel 4 | All rights reserved. 5 | 6 | Redistribution and use in source and binary forms, with or without 7 | modification, are permitted provided that the following conditions are met: 8 | 9 | 1. Redistributions of source code must retain the above copyright notice, this 10 | list of conditions and the following disclaimer. 11 | 12 | 2. Redistributions in binary form must reproduce the above copyright notice, 13 | this list of conditions and the following disclaimer in the documentation 14 | and/or other materials provided with the distribution. 15 | 16 | 3. Neither the name of the copyright holder nor the names of its 17 | contributors may be used to endorse or promote products derived from 18 | this software without specific prior written permission. 19 | 20 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 21 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 22 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 23 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 24 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 25 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 26 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 27 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 28 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 29 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 30 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include dmx/__init__.py 2 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | USB-DMX512 Python Module 2 | ======================== 3 | 4 | This python module supports actual the following USB-DMX interfaces (FT232R chip based): 5 | - EUROLITE USB-DMX512 PRO Cable Interface [(Link)](https://www.steinigke.de/en/mpn51860122-eurolite-usb-dmx512-pro-cable-interface.html) 6 | - EUROLITE USB-DMX512 PRO Interface MK2 [(Link)](https://www.steinigke.de/en/mpn51860121-eurolite-usb-dmx512-pro-interface-mk2.html) 7 | 8 | 9 | Requirements 10 | ------------ 11 | - Python ≥ 3.6 12 | - numpy 13 | - pyserial 14 | 15 | Note: Tested on Windows 10, amd64, Python 3.8 \ 16 | Should also work on Linux, MacOS and on AARCH64 devices (ARM devices like Raspberry PI). 17 | 18 | 19 | Installation 20 | ------------ 21 | Make sure to have git, python and pip in your environment path or activate your python environment.\ 22 | To install enter in cmd/shell: 23 | 24 | git clone https://github.com/monzelr/dmx.git 25 | 26 | cd dmx 27 | 28 | pip install . 29 | 30 | Alternative with python: 31 | 32 | python setup.py install 33 | 34 | Example Code Snippets 35 | --------------------- 36 | If you want to dim 4 channels up and down: 37 | 38 | from dmx import DMX 39 | import time 40 | 41 | dmx = DMX(num_of_channels=4) 42 | dmx.set_data(1, 0) 43 | dmx.set_data(2, 0) 44 | dmx.set_data(3, 0) 45 | dmx.set_data(4, 0) 46 | 47 | while True: 48 | for i in range(0, 255, 5): 49 | dmx.set_data(1, i, auto_send=False) 50 | dmx.set_data(2, i, auto_send=False) 51 | dmx.set_data(3, i, auto_send=False) 52 | dmx.set_data(4, i) 53 | time.sleep(0.01) 54 | 55 | for i in range(255, 0, -5): 56 | dmx.set_data(1, i, auto_send=False) 57 | dmx.set_data(2, i, auto_send=False) 58 | dmx.set_data(3, i, auto_send=False) 59 | dmx.set_data(4, i) 60 | time.sleep(0.01) 61 | 62 | If you want to add your own adapter or multiple adapter by serial number: 63 | 64 | 65 | from dmx import DMX 66 | 67 | dmx = DMX() 68 | my_device_serial_number = dmx.use_device.serial_number 69 | del dmx 70 | 71 | my_device_sn = dmx.use_device.serial_number 72 | del dmx 73 | 74 | dmx2 = DMX(serial_number=my_device_sn) 75 | dmx2.set_data(1, 100) 76 | dmx2.send() 77 | time.sleep(1) 78 | del dmx2 79 | 80 | Technical notes 81 | --------------- 82 | to EUROLITE USB-DMX512 PRO Cable Interface / EUROLITE USB-DMX512 PRO Interface MK2 : 83 | 84 | - uses chip FTDI232R (like EUROLITE USB-DMX512 PRO Interface MK2) 85 | - the FTDI FT232R updates the DMX automatically - you do no need to refresh the DMX universe by yourself 86 | - if you only have 4 channels, set them at the DMX address start (1 to 4), thus sending updates to the FT232R chip is faster 87 | - 250000 baudrate for the FT232R is a must 88 | - needs 5 start bytes: [0x7E, 0x06, 0x01, 0x02, 0x00] 89 | 90 | - byte 1: signal start byte 0x7E 91 | - byte 2: TX DMX packet: 0x06 92 | - byte 3 & 4: LSB of DMX length (in this case 513 -> do not forget address 0): 0x01 and 0x02 93 | - byte 5: address 0 of DMX signal: 0x00 94 | - supports only label 6: TX DMX Packet 95 | - needs one end byte [0xE7] 96 | 97 | 98 | Building the documentation 99 | -------------------------- 100 | Go into the dmx root folder (where setup.py is) and type in the following command in the cmd/shell: 101 | 102 | python setup.py build_sphinx 103 | 104 | The documentation can than be found in dmx/build/sphinx/html. 105 | -------------------------------------------------------------------------------- /dmx/__init__.py: -------------------------------------------------------------------------------- 1 | """Top-level package""" 2 | 3 | from dmx.dmx import DMX 4 | from dmx.dmx import logger 5 | from dmx.dmx import Device 6 | from dmx.dmx import DEVICE_LIST 7 | from dmx.dmx import sleep_us 8 | 9 | __author__ = 'Rune Monzel' 10 | __email__ = 'runemonzel@googlemail.com' 11 | __version__ = '0.2.4' 12 | __all__ = ['DMX', 13 | 'logger', 14 | 'Device', 15 | 'DEVICE_LIST', 16 | 'sleep_us'] -------------------------------------------------------------------------------- /dmx/dmx.py: -------------------------------------------------------------------------------- 1 | """ 2 | file to control dmx devices 3 | 4 | Copyright 2021 Rune Monzel 5 | """ 6 | 7 | # internal python modules 8 | import logging 9 | import time 10 | from typing import Union 11 | 12 | # external python modules 13 | import serial 14 | import serial.tools.list_ports 15 | import numpy as np 16 | 17 | 18 | # Use 'dmx' Logger 19 | logger = logging.getLogger('dmx') 20 | logger.setLevel(logging.DEBUG) 21 | 22 | 23 | class Device(object): 24 | """ 25 | class to describe further RS-485 devices 26 | """ 27 | def __init__(self, vid=0, pid=0, serial_number=None): 28 | """ 29 | :param vid: vendor ID 30 | :param pid: product ID 31 | :param serial_number: serial number of the device / chip 32 | :return None: 33 | """ 34 | self.vid = vid 35 | self.pid = pid 36 | self.serial_number = serial_number 37 | 38 | 39 | DUMMY = Device(vid=0, pid=0, serial_number=None) 40 | 41 | """ 42 | Some notes to EUROLITE USB-DMX512 PRO Cable Interface / EUROLITE USB-DMX512 PRO Interface MK2 : 43 | - uses chip FTDI232R (like EUROLITE USB-DMX512 PRO Interface MK2) 44 | - the FTDI232R updates the DMX automatically! You do no need to refresh the DMX universe by yourself 45 | - if you have only want to update channel 1 to 4 you can just set the number of channels to 4, thus sending to the 46 | FTDI232R chip is faster 47 | - 250000 baudrate is a must 48 | - needs start bytes: [0x7E, 0x06, 0x01, 0x02, 0x00] 49 | 0x7E: signal start byte 50 | 0x06: label TX 51 | 0x01 and 0x02: LSB of DMX length (do not forget address 0) 52 | 0x00: address 0 of DMX signal, which is always 0 53 | - supports only label 6: TX DMX Packet 54 | - needs end byte [0xE7] 55 | - Example Code: 56 | dmx = DMX(num_of_channels=4) 57 | dmx.set_data(1, 0) 58 | dmx.set_data(2, 0) 59 | dmx.set_data(3, 0) 60 | dmx.set_data(4, 0) 61 | 62 | while True: 63 | for i in range(0, 255, 5): 64 | dmx.set_data(1, i, auto_send=False) 65 | dmx.set_data(2, i, auto_send=False) 66 | dmx.set_data(3, i, auto_send=False) 67 | dmx.set_data(4, i) 68 | time.sleep(0.01) 69 | 70 | for i in range(255, 0, -5): 71 | dmx.set_data(1, i, auto_send=False) 72 | dmx.set_data(2, i, auto_send=False) 73 | dmx.set_data(3, i, auto_send=False) 74 | dmx.set_data(4, i) 75 | time.sleep(0.01) 76 | """ 77 | EUROLITE_USB_DMX512_PRO_CABLE_INTERFACE = Device(vid=1027, pid=24577, serial_number=None) 78 | 79 | 80 | DEVICE_LIST = [DUMMY, 81 | EUROLITE_USB_DMX512_PRO_CABLE_INTERFACE] 82 | 83 | 84 | class DMX(object): 85 | """ 86 | DMX class which talks to RS-485 chip with the pyserial python package 87 | """ 88 | 89 | def __init__(self, num_of_channels: int = 512, serial_number: str = "") -> None: 90 | """ 91 | 92 | :param num_of_channels: integer between 1 and 512 93 | :param serial_number: serial number of the RS-485 chip as string. If you want to know the current serial number 94 | of your device, call my_dmx.device.serial_number 95 | :return None: 96 | """ 97 | 98 | # numpy array with num_of_channels length 99 | self.data = np.zeros([1], dtype=np.uint8) 100 | 101 | self.break_us = 88 # 88us < break condition < 1s -> not used in DMX implementation 102 | self.MAB_us = 8 # 8us < Mark-After-Break < 1s -> not used in DMX implementation 103 | 104 | # Search for RS-485 devices, for this look into DEVICE_LIST 105 | self.ser = None 106 | self.device = None 107 | for device in serial.tools.list_ports.comports(): 108 | for known_device in DEVICE_LIST: 109 | if device.vid == known_device.vid and device.pid == known_device.pid and serial_number == "": 110 | try: 111 | s = serial.Serial(device.device) 112 | s.close() 113 | except (OSError, serial.SerialException): 114 | pass 115 | else: 116 | self.device = device 117 | break 118 | 119 | elif device.vid == known_device.vid and device.pid == known_device.pid and \ 120 | serial_number == device.serial_number: 121 | try: 122 | s = serial.Serial(device.device) 123 | s.close() 124 | del s 125 | except (OSError, serial.SerialException) as error: 126 | raise error 127 | else: 128 | self.device = device 129 | logger.info("Found device with serial number: " + serial_number) 130 | break 131 | if self.device: 132 | logger.info("Found RS-485 interface: " + self.device.description) 133 | break 134 | 135 | if self.device is None: 136 | raise ConnectionError("Could not find the RS-485 interface.") 137 | 138 | if self.device.vid == EUROLITE_USB_DMX512_PRO_CABLE_INTERFACE.vid and \ 139 | self.device.pid == EUROLITE_USB_DMX512_PRO_CABLE_INTERFACE.pid: 140 | self.start_byte = np.array([0x7E, 0x06, 0x01, 0x02, 0x00], np.uint8) 141 | self.end_byte = np.array([0xE7], np.uint8) 142 | self.num_of_channels = num_of_channels 143 | self.ser = serial.Serial(self.device.device, 144 | baudrate=250000, 145 | parity=serial.PARITY_NONE, 146 | bytesize=serial.EIGHTBITS, 147 | stopbits=serial.STOPBITS_TWO 148 | ) 149 | else: 150 | self.start_byte = np.array([0x00], np.uint8) 151 | self.end_byte = np.array([], np.uint8) 152 | self.num_of_channels = num_of_channels 153 | self.ser = serial.Serial(self.device.device, 154 | baudrate=250000, 155 | parity=serial.PARITY_NONE, 156 | bytesize=serial.EIGHTBITS, 157 | stopbits=serial.STOPBITS_TWO 158 | ) 159 | # self.send() # make sure it has been send 160 | 161 | @property 162 | def num_of_channels(self) -> int: 163 | """ 164 | 165 | :return num_of_channels: number of DMX channels which shall be used in the universe, the less channels the faster! 166 | """ 167 | return self.__num_of_channels 168 | 169 | @num_of_channels.setter 170 | def num_of_channels(self, num_of_channels: int) -> None: 171 | """ 172 | sets the number of DMX channels 173 | 174 | :param num_of_channels: number of DMX channels 175 | :return None: 176 | """ 177 | if num_of_channels > 512: 178 | raise ValueError("Number of channels are maximal 512! Only channels 1 to 512 can be accessed. " + 179 | "Channel 0 is reserved as start channel.") 180 | if self.device.vid == EUROLITE_USB_DMX512_PRO_CABLE_INTERFACE.vid and \ 181 | self.device.pid == EUROLITE_USB_DMX512_PRO_CABLE_INTERFACE.pid: 182 | self.start_byte[2] = (num_of_channels+1) & 0xFF 183 | self.start_byte[3] = ((num_of_channels+1) >> 8) & 0xFF 184 | self.__num_of_channels = num_of_channels 185 | old_data = self.data # save old data 186 | self.data = np.zeros([self.__num_of_channels], dtype=np.uint8) # create new data 187 | # copy old data into new data 188 | for channel_id in range(min([len(old_data), len(self.data)])): 189 | self.data[channel_id] = old_data[channel_id] 190 | 191 | def is_connected(self) -> bool: 192 | """ 193 | checks if the DMX class has a connection to the device 194 | 195 | :return: 196 | """ 197 | connected = False 198 | devices = serial.tools.list_ports.comports() 199 | if self.device is not None: 200 | if self.device in devices: 201 | connected = True 202 | return connected 203 | 204 | def set_data(self, channel_id: int, data: int, auto_send: bool = True) -> None: 205 | """ 206 | 207 | :param channel_id: the channel ID as integer value between 1 and 511 208 | :param data: the data for the cannel ID as integer value between 0 and 255 209 | :param auto_send: if True, all DMX Data will be send out 210 | :return None: 211 | """ 212 | if channel_id < 1 or channel_id > 512: 213 | raise ValueError("Channel ID must between 1 and 512.") 214 | if data < 0 or data > 255: 215 | raise ValueError("Data ID must between 0 and 255.") 216 | if channel_id > self.__num_of_channels: 217 | raise ValueError("Channel ID was not reserved. Please set the num_of_channels first.") 218 | 219 | self.data[channel_id-1] = data 220 | 221 | if auto_send: 222 | self.send() 223 | 224 | def send(self) -> None: 225 | """ 226 | Sends data to RS-485 converter 227 | 228 | :return None: 229 | """ 230 | data = np.concatenate((self.start_byte, self.data, self.end_byte)).tobytes() 231 | self.ser.write(data) 232 | self.ser.flush() 233 | 234 | def __del__(self) -> None: 235 | """ 236 | make sure that all DMX channels are set to 0 due to security reasons 237 | if you do not want this behaviour, derive this class and override the __del__ function. 238 | 239 | :return None: 240 | """ 241 | if isinstance(self.ser, serial.Serial): 242 | if self.ser.is_open: 243 | if self.is_connected(): 244 | self.num_of_channels = 512 245 | self.data = np.zeros([self.num_of_channels], np.uint8) 246 | self.send() 247 | print("close serial port") 248 | self.ser.close() 249 | 250 | 251 | def sleep_us(sleep_in_us: int) -> None: 252 | """ 253 | an accurate sleep in microseconds 254 | 255 | Note: a function call in python needs up to 1 microseconds! This depends on your platform and your computer speed. 256 | Thus measure this function on your platform if you want to be accurate! 257 | 258 | Example code for measuring: 259 | ''' 260 | t = time.perf_counter_ns() 261 | sleep_us(1) 262 | b = time.perf_counter_ns() - t 263 | print("elapsed time: %3.3f us" % (b/1000)) 264 | ''' 265 | 266 | :param sleep_in_us: sleep time in microseconds 267 | :return None: 268 | """ 269 | start_time = time.perf_counter_ns() 270 | sleep_in_ns = sleep_in_us * 1000 271 | while (time.perf_counter_ns()-start_time) < sleep_in_ns: 272 | continue 273 | return 274 | 275 | 276 | if __name__ == "__main__": 277 | 278 | # create console handler with a higher log level 279 | ch = logging.StreamHandler() 280 | ch.setLevel(logging.DEBUG) 281 | # create formatter and add it to the handlers 282 | formatter = logging.Formatter('%(asctime)s.%(msecs)03d [%(levelname)s]: %(message)s', 283 | "%Y-%m-%d %H:%M:%S") 284 | ch.setFormatter(formatter) 285 | logger.addHandler(ch) 286 | 287 | dmx = DMX(num_of_channels=4) 288 | dmx.set_data(1, 0) 289 | dmx.set_data(2, 0) 290 | dmx.set_data(3, 0) 291 | dmx.set_data(4, 0) 292 | 293 | for t in range(5): 294 | for i in range(0, 255, 5): 295 | dmx.set_data(1, i, auto_send=False) 296 | dmx.set_data(2, i, auto_send=False) 297 | dmx.set_data(3, i, auto_send=False) 298 | dmx.set_data(4, i) 299 | time.sleep(0.01) 300 | 301 | for i in range(255, 1, -5): 302 | dmx.set_data(1, i, auto_send=False) 303 | dmx.set_data(2, i, auto_send=False) 304 | dmx.set_data(3, i, auto_send=False) 305 | dmx.set_data(4, i) 306 | time.sleep(0.01) 307 | 308 | my_device_sn = dmx.device.serial_number 309 | del dmx 310 | 311 | dmx2 = DMX(serial_number=my_device_sn) 312 | dmx2.set_data(1, 100) 313 | dmx2.send() 314 | time.sleep(1) 315 | del dmx2 316 | 317 | 318 | 319 | 320 | -------------------------------------------------------------------------------- /documentation/API.rst: -------------------------------------------------------------------------------- 1 | === 2 | API 3 | === 4 | 5 | 6 | .. automodule:: dmx 7 | 8 | .. autoclass:: logger 9 | .. autoclass:: Device 10 | .. autoclass:: DEVICE_LIST 11 | :members: 12 | :special-members: __init__ 13 | .. autoclass:: DMX 14 | :members: send, set_data, num_of_channels 15 | :special-members: __del__, __init__ 16 | .. autoclass:: sleep_us 17 | -------------------------------------------------------------------------------- /documentation/Makefile: -------------------------------------------------------------------------------- 1 | # Minimal makefile for Sphinx documentation 2 | # 3 | 4 | # You can set these variables from the command line. 5 | SPHINXOPTS = 6 | SPHINXBUILD = python -msphinx 7 | SPHINXPROJ = dmx 8 | SOURCEDIR = . 9 | BUILDDIR = _build 10 | 11 | # Put it first so that "make" without argument is like "make help". 12 | help: 13 | @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) 14 | 15 | .PHONY: help Makefile 16 | 17 | # Catch-all target: route all unknown targets to Sphinx using the new 18 | # "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). 19 | %: Makefile 20 | @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) 21 | -------------------------------------------------------------------------------- /documentation/conf.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # 3 | # dmx documentation build configuration file, created by 4 | # sphinx-quickstart on Fri Jun 9 13:47:02 2017. 5 | # 6 | # This file is execfile()d with the current directory set to its 7 | # containing dir. 8 | # 9 | # Note that not all possible configuration values are present in this 10 | # autogenerated file. 11 | # 12 | # All configuration values have a default; values that are commented out 13 | # serve to show the default. 14 | 15 | # If extensions (or modules to document with autodoc) are in another 16 | # directory, add these directories to sys.path here. If the directory is 17 | # relative to the documentation root, use os.path.abspath to make it 18 | # absolute, like shown here. 19 | # 20 | import os 21 | import sys 22 | sys.path.insert(0, os.path.abspath('..')) 23 | 24 | import dmx 25 | 26 | # -- General configuration --------------------------------------------- 27 | 28 | # If your documentation needs a minimal Sphinx version, state it here. 29 | # 30 | # needs_sphinx = '1.0' 31 | 32 | # Add any Sphinx extension module names here, as strings. They can be 33 | # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom ones. 34 | extensions = ['sphinx.ext.autodoc', 'sphinx.ext.viewcode', 'sphinx.ext.coverage'] 35 | 36 | # Add any paths that contain templates here, relative to this directory. 37 | templates_path = ['_templates'] 38 | 39 | # The suffix(es) of source filenames. 40 | # You can specify multiple suffix as a list of string: 41 | # 42 | # source_suffix = ['.rst', '.md'] 43 | source_suffix = '.rst' 44 | 45 | # The master toctree document. 46 | master_doc = 'index' 47 | 48 | # General information about the project. 49 | project = 'dmx' 50 | copyright = "2021, Rune Monzel" 51 | author = "Rune Monzel" 52 | 53 | # The version info for the project you're documenting, acts as replacement 54 | # for |version| and |release|, also used in various other places throughout 55 | # the built documents. 56 | # 57 | # The short X.Y version. 58 | version = dmx.__version__ 59 | # The full version, including alpha/beta/rc tags. 60 | release = dmx.__version__ 61 | 62 | # The language for content autogenerated by Sphinx. Refer to documentation 63 | # for a list of supported languages. 64 | # 65 | # This is also used if you do content translation via gettext catalogs. 66 | # Usually you set "language" from the command line for these cases. 67 | language = None 68 | 69 | # List of patterns, relative to source directory, that match files and 70 | # directories to ignore when looking for source files. 71 | # This patterns also effect to html_static_path and html_extra_path 72 | exclude_patterns = ['_build', 'Thumbs.db', '.DS_Store'] 73 | 74 | # The name of the Pygments (syntax highlighting) style to use. 75 | pygments_style = 'sphinx' 76 | 77 | # If true, `todo` and `todoList` produce output, else they produce nothing. 78 | todo_include_todos = False 79 | 80 | 81 | # -- Options for HTML output ------------------------------------------- 82 | 83 | # The theme to use for HTML and HTML Help pages. See the documentation for 84 | # a list of builtin themes. 85 | # 86 | html_theme = 'nature' 87 | 88 | # Theme options are theme-specific and customize the look and feel of a 89 | # theme further. For a list of options available for each theme, see the 90 | # documentation. 91 | # 92 | # html_theme_options = {} 93 | 94 | # Add any paths that contain custom static files (such as style sheets) here, 95 | # relative to this directory. They are copied after the builtin static files, 96 | # so a file named "default.css" will overwrite the builtin "default.css". 97 | html_static_path = ['_static'] 98 | 99 | 100 | # -- Options for HTMLHelp output --------------------------------------- 101 | 102 | # Output file base name for HTML help builder. 103 | htmlhelp_basename = 'dmxdoc' 104 | 105 | 106 | # -- Options for LaTeX output ------------------------------------------ 107 | 108 | latex_elements = { 109 | # The paper size ('letterpaper' or 'a4paper'). 110 | # 111 | # 'papersize': 'letterpaper', 112 | 113 | # The font size ('10pt', '11pt' or '12pt'). 114 | # 115 | # 'pointsize': '10pt', 116 | 117 | # Additional stuff for the LaTeX preamble. 118 | # 119 | # 'preamble': '', 120 | 121 | # Latex figure (float) alignment 122 | # 123 | # 'figure_align': 'htbp', 124 | } 125 | 126 | # Grouping the document tree into LaTeX files. List of tuples 127 | # (source start file, target name, title, author, documentclass 128 | # [howto, manual, or own class]). 129 | latex_documents = [ 130 | (master_doc, 'dmx.tex', 131 | 'dmx Documentation', 132 | 'Rune Monzel', 'manual'), 133 | ] 134 | 135 | 136 | # -- Options for manual page output ------------------------------------ 137 | 138 | # One entry per manual page. List of tuples 139 | # (source start file, name, description, authors, manual section). 140 | man_pages = [ 141 | (master_doc, 'dmx', 142 | 'dmx Documentation', 143 | [author], 1) 144 | ] 145 | 146 | 147 | # -- Options for Texinfo output ---------------------------------------- 148 | 149 | # Grouping the document tree into Texinfo files. List of tuples 150 | # (source start file, target name, title, author, 151 | # dir menu entry, description, category) 152 | texinfo_documents = [ 153 | (master_doc, 'dmx', 154 | 'dmx Documentation', 155 | author, 156 | 'dmx', 157 | 'One line description of project.', 158 | 'Miscellaneous'), 159 | ] 160 | 161 | 162 | 163 | -------------------------------------------------------------------------------- /documentation/history.rst: -------------------------------------------------------------------------------- 1 | ======= 2 | History 3 | ======= 4 | 5 | Version v0.1.0: 6 | --------------- 7 | - initial python package release of dmx -------------------------------------------------------------------------------- /documentation/index.rst: -------------------------------------------------------------------------------- 1 | Welcome to dmx's documentation! 2 | =============================== 3 | 4 | .. toctree:: 5 | :maxdepth: 2 6 | :caption: Contents: 7 | 8 | readme 9 | installation 10 | usage 11 | API 12 | history 13 | 14 | Indices and tables 15 | ================== 16 | * :ref:`genindex` 17 | * :ref:`modindex` 18 | * :ref:`search` 19 | -------------------------------------------------------------------------------- /documentation/installation.rst: -------------------------------------------------------------------------------- 1 | ============ 2 | Installation 3 | ============ 4 | 5 | 6 | From sources 7 | ------------ 8 | 9 | The sources for dmx can be found in the github `github repo`_. 10 | 11 | You should clone the private repository (ask for access): 12 | 13 | .. code-block:: console 14 | 15 | $ git clone git://gitlab.com/monzelr/dmx 16 | 17 | Best practice is just to link the source doe to your python environment via the develop command: 18 | 19 | .. code-block:: console 20 | 21 | $ cd dmx 22 | $ pip install dmx 23 | 24 | To uninstall the python package, type in this command: 25 | 26 | .. code-block:: console 27 | 28 | $ pip uninstall dmx 29 | 30 | Of course, you can also install the package with python: 31 | 32 | .. code-block:: console 33 | 34 | $ python setup.py install 35 | 36 | For deployment 37 | -------------- 38 | If you want to distribute the package, please build a python wheel which can be distributed: 39 | 40 | .. code-block:: console 41 | 42 | $ python setup.py build_ext 43 | $ python setup.py bdist_wheel 44 | 45 | The wheel contains compiled machine code which is not readable for humans. Thus it can be deployed savely. 46 | The wheel can be installed with the pip command. 47 | 48 | .. _github repo: https://github.com/monzelr/dmx 49 | -------------------------------------------------------------------------------- /documentation/make.bat: -------------------------------------------------------------------------------- 1 | @ECHO OFF 2 | 3 | pushd %~dp0 4 | 5 | REM Command file for Sphinx documentation 6 | set python=%userprofile%\python38_envs\littlelucid\Scripts\python.exe 7 | 8 | if "%SPHINXBUILD%" == "" ( 9 | set SPHINXBUILD=%python% -msphinx 10 | ) 11 | set SOURCEDIR=. 12 | set BUILDDIR=_build 13 | set SPHINXPROJ=sps 14 | 15 | if "%1" == "" goto help 16 | 17 | %SPHINXBUILD% >NUL 2>NUL 18 | if errorlevel 9009 ( 19 | echo. 20 | echo.The Sphinx module was not found. Make sure you have Sphinx installed, 21 | echo.then set the SPHINXBUILD environment variable to point to the full 22 | echo.path of the 'sphinx-build' executable. Alternatively you may add the 23 | echo.Sphinx directory to PATH. 24 | echo. 25 | echo.If you don't have Sphinx installed, grab it from 26 | echo.http://sphinx-doc.org/ 27 | exit /b 1 28 | ) 29 | 30 | %SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% 31 | goto end 32 | 33 | :help 34 | %SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% 35 | 36 | :end 37 | popd 38 | 39 | pause 40 | -------------------------------------------------------------------------------- /documentation/readme.rst: -------------------------------------------------------------------------------- 1 | .. include:: ../README.md 2 | -------------------------------------------------------------------------------- /documentation/usage.rst: -------------------------------------------------------------------------------- 1 | ===== 2 | Usage 3 | ===== 4 | 5 | Example Code Snippets 6 | --------------------- 7 | If you want to dim 4 channels up and down: 8 | 9 | .. code-block:: python 10 | 11 | from dmx import DMX 12 | import time 13 | 14 | dmx = DMX(num_of_channels=4) 15 | dmx.set_data(1, 0) 16 | dmx.set_data(2, 0) 17 | dmx.set_data(3, 0) 18 | dmx.set_data(4, 0) 19 | 20 | while True: 21 | for i in range(0, 255, 5): 22 | dmx.set_data(1, i, auto_send=False) 23 | dmx.set_data(2, i, auto_send=False) 24 | dmx.set_data(3, i, auto_send=False) 25 | dmx.set_data(4, i) 26 | time.sleep(0.01) 27 | 28 | for i in range(255, 0, -5): 29 | dmx.set_data(1, i, auto_send=False) 30 | dmx.set_data(2, i, auto_send=False) 31 | dmx.set_data(3, i, auto_send=False) 32 | dmx.set_data(4, i) 33 | time.sleep(0.01) 34 | 35 | 36 | If you want to add your own adapter or multiple adapter by serial number: 37 | 38 | .. code-block:: python 39 | 40 | from dmx import DMX 41 | 42 | dmx = DMX() 43 | my_device_serial_number = dmx.use_device.serial_number 44 | del dmx 45 | 46 | my_device_sn = dmx.use_device.serial_number 47 | del dmx 48 | 49 | dmx2 = DMX(serial_number=my_device_sn) 50 | dmx2.set_data(1, 100) 51 | dmx2.send() 52 | time.sleep(1) 53 | del dmx2 -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [build-system] 2 | requires = [ 3 | "setuptools>=42", 4 | "wheel", 5 | "sphinx" 6 | ] 7 | build-backend = "setuptools.build_meta" -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [metadata] 2 | # This includes the license file(s) in the wheel. 3 | # https://wheel.readthedocs.io/en/stable/user_guide.html#including-license-files-in-the-generated-wheel-file 4 | license_files = LICENSE -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | """The setup script.""" 4 | 5 | import sys 6 | import os 7 | import io 8 | import os 9 | import re 10 | 11 | from setuptools import setup 12 | 13 | def read(*names, **kwargs): 14 | """Python 2 and Python 3 compatible text file reading. 15 | Required for single-sourcing the version string. 16 | """ 17 | with io.open( 18 | os.path.join(os.path.dirname(__file__), *names), 19 | encoding=kwargs.get("encoding", "utf8") 20 | ) as fp: 21 | return fp.read() 22 | 23 | def find_version_author_email(*file_paths): 24 | """ 25 | Search the file for a specific string. 26 | file_path contain string path components. 27 | Reads the supplied Python module as text without importing it. 28 | """ 29 | _version = _author = _email = "" 30 | file = read(*file_paths) 31 | version_match = re.search(r"^__version__ = ['\"]([^'\"]*)['\"]", file, re.M) 32 | author_match = re.search(r"^__author__ = ['\"]([^'\"]*)['\"]", file, re.M) 33 | email_match = re.search(r"^__email__ = ['\"]([^'\"]*)['\"]", file, re.M) 34 | if version_match: 35 | _version = version_match.group(1) 36 | else: 37 | raise RuntimeError("Unable to find version string.") 38 | if author_match: 39 | _author = author_match.group(1) 40 | else: 41 | raise RuntimeError("Unable to find version string.") 42 | if email_match: 43 | _email = email_match.group(1) 44 | else: 45 | raise RuntimeError("Unable to find version string.") 46 | return _version, _author, _email 47 | 48 | version, author, author_email = find_version_author_email('dmx', '__init__.py') 49 | 50 | # this is only necessary when not using setuptools/distribute 51 | 52 | requirements = ["numpy>=1.13.0", 53 | "pyserial>=3.2"] 54 | 55 | test_requirements = ["numpy>=1.13.0", 56 | "pyserial>=3.2"] 57 | 58 | python_requires = '>=3.6' 59 | classifiers = [ 60 | 'Development Status :: 2 - Pre-Alpha', 61 | 'Intended Audience :: Developers', 62 | 'Natural Language :: English', 63 | 'License :: OSI Approved :: BSD License', 64 | 'Operating System :: POSIX', 65 | 'Operating System :: Microsoft :: Windows', 66 | 'Programming Language :: Python', 67 | 'Programming Language :: Python :: 3.6', 68 | 'Programming Language :: Python :: 3.7', 69 | 'Programming Language :: Python :: 3.8', 70 | 'Programming Language :: Python :: 3.9', 71 | 'Programming Language :: Python :: 3.10', 72 | ] 73 | description = "A python module to control one DMX Universe with a USB to RS-485 adapter." 74 | install_requires = requirements 75 | long_description = "" 76 | keywords = 'DMX, RS-485' 77 | name = 'dmx' 78 | test_suite = 'tests' 79 | url = 'https://gitlab.com/monzelr/dmx' 80 | zip_safe = False 81 | 82 | setup( 83 | author=author, 84 | author_email=author_email, 85 | python_requires=python_requires, 86 | classifiers=classifiers, 87 | description=description, 88 | install_requires=requirements, 89 | keywords=keywords, 90 | name=name, 91 | packages=['dmx'], 92 | test_suite=test_suite, 93 | tests_require=test_requirements, 94 | url=url, 95 | license="BSD 3-Clause License", 96 | version=version, 97 | zip_safe=zip_safe, 98 | ) 99 | 100 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- 1 | """Unit test package for dmx.""" 2 | -------------------------------------------------------------------------------- /tests/test_dmx.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | """Tests for `dmx` package.""" 4 | 5 | 6 | import unittest 7 | 8 | import time 9 | import os 10 | import numpy as np 11 | 12 | import dmx 13 | 14 | 15 | class TestDMX(unittest.TestCase): 16 | """Tests for `dmx` package.""" 17 | 18 | @classmethod 19 | def setUpClass(cls): 20 | """ 21 | setting up everything 22 | :return: 23 | """ 24 | 25 | 26 | def setUp(self): 27 | """Set up test fixtures, if any.""" 28 | 29 | @classmethod 30 | def tearDownClass(cls): 31 | """Tear down test fixtures, if any.""" 32 | cls.disconnect() 33 | 34 | def test_DMX_(self): 35 | """Test dmx with numpy array""" 36 | pass 37 | 38 | @staticmethod 39 | def disconnect(): 40 | pass 41 | --------------------------------------------------------------------------------