├── .bandit ├── .drone.yml ├── .flake8 ├── .github └── ISSUE_TEMPLATE │ ├── bug_report.md │ └── feature_request.md ├── .gitignore ├── .readthedocs.yaml ├── CHANGELOG ├── ExampleDeck.jpg ├── LICENSE ├── MANIFEST.in ├── README.md ├── VERSION ├── doc ├── Makefile ├── make.bat ├── requirements.txt └── source │ ├── conf.py │ ├── examples │ ├── animated.rst │ ├── basic.rst │ ├── deckinfo.rst │ ├── neo.rst │ ├── pedal.rst │ ├── plus.rst │ └── tiled.rst │ ├── index.rst │ ├── modules │ ├── devices.rst │ ├── discovery.rst │ ├── imagehelpers.rst │ └── transports.rst │ └── pages │ ├── backend_libusb_hidapi.rst │ ├── changelog.rst │ ├── installation.rst │ ├── license.rst │ └── source.rst ├── requirements.txt ├── setup.py ├── src ├── Assets │ ├── Apache License 2.0.txt │ ├── Attribution.txt │ ├── CC BY-SA 3.0 License.txt │ ├── CC0 License.txt │ ├── Elephant_Walking_animated.gif │ ├── Exit.png │ ├── Harold.jpg │ ├── Pressed.png │ ├── RGB_color_space_animated_view.gif │ ├── Released.png │ ├── Roboto-Regular.ttf │ └── Simple_CV_Joint_animated.gif ├── StreamDeck │ ├── DeviceManager.py │ ├── Devices │ │ ├── StreamDeck.py │ │ ├── StreamDeckMini.py │ │ ├── StreamDeckNeo.py │ │ ├── StreamDeckOriginal.py │ │ ├── StreamDeckOriginalV2.py │ │ ├── StreamDeckPedal.py │ │ ├── StreamDeckPlus.py │ │ ├── StreamDeckXL.py │ │ └── __init__.py │ ├── ImageHelpers │ │ ├── PILHelper.py │ │ └── __init__.py │ ├── ProductIDs.py │ ├── Transport │ │ ├── Dummy.py │ │ ├── LibUSBHIDAPI.py │ │ ├── Transport.py │ │ └── __init__.py │ └── __init__.py ├── example_animated.py ├── example_basic.py ├── example_deckinfo.py ├── example_neo.py ├── example_pedal.py ├── example_plus.py └── example_tileimage.py └── test └── test.py /.bandit: -------------------------------------------------------------------------------- 1 | [bandit] 2 | skips = B311 3 | -------------------------------------------------------------------------------- /.drone.yml: -------------------------------------------------------------------------------- 1 | kind: pipeline 2 | type: docker 3 | name: Build Tests 4 | 5 | platform: 6 | os: linux 7 | arch: amd64 8 | 9 | steps: 10 | - name: Flake8 11 | image: abcminiuser/docker-ci-python:latest 12 | pull: always 13 | commands: 14 | - flake8 --config .flake8 src/ 15 | 16 | - name: Bandit 17 | image: abcminiuser/docker-ci-python:latest 18 | pull: always 19 | commands: 20 | - bandit --ini .bandit -r src/ 21 | 22 | - name: MyPy 23 | image: abcminiuser/docker-ci-python:latest 24 | pull: always 25 | commands: 26 | - mypy --ignore-missing-imports src/ 27 | 28 | - name: Tests 29 | image: abcminiuser/docker-ci-python:latest 30 | pull: always 31 | commands: 32 | - apk add jpeg-dev zlib-dev 33 | - pip install Pillow 34 | - python test/test.py 35 | 36 | - name: Documentation 37 | image: abcminiuser/docker-ci-python:latest 38 | pull: always 39 | commands: 40 | - make -C doc html 41 | 42 | - name: Packaging 43 | image: abcminiuser/docker-ci-python:latest 44 | pull: always 45 | commands: 46 | - python -m build 47 | - twine check dist/* 48 | depends_on: 49 | - Flake8 50 | - Bandit 51 | - MyPy 52 | - Tests 53 | - Documentation 54 | -------------------------------------------------------------------------------- /.flake8: -------------------------------------------------------------------------------- 1 | [flake8] 2 | ignore = E501,E261,E722 3 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: bug 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | Please give a clear and concise description of what the bug is. 12 | 13 | **To Reproduce** 14 | Please list the steps to reproduce the behavior. 15 | 16 | **StreamDeck Information** 17 | List the model of StreamDeck(s) you are using here. 18 | 19 | **System Information** 20 | List details about your operating system and hardware setup here, including OS name and version, and the version of any library dependencies you are using. 21 | 22 | You should also include the version(s) of the library you are using - if not using a tagged release, list the commit hash you are testing here. 23 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: enhancement 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Is your feature request related to a problem? Please describe.** 11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 12 | 13 | **Describe the solution you'd like** 14 | A clear and concise description of what you want to happen. 15 | 16 | **Describe alternatives you've considered** 17 | A clear and concise description of any alternative solutions or features you've considered. 18 | 19 | **Additional context** 20 | Add any other context or screenshots about the feature request here. 21 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | __pycache__ 2 | .idea/ 3 | *.egg-info/ 4 | doc/build 5 | build/ 6 | dist/ 7 | -------------------------------------------------------------------------------- /.readthedocs.yaml: -------------------------------------------------------------------------------- 1 | # .readthedocs.yaml 2 | # Read the Docs configuration file 3 | # See https://docs.readthedocs.io/en/stable/config-file/v2.html for details 4 | # See for https://github.com/readthedocs/readthedocs.org/issues/6324 and https://github.com/readthedocs/readthedocs.org/issues/7554 for details 5 | 6 | version: 2 7 | 8 | build: 9 | os: ubuntu-22.04 10 | tools: 11 | python: "3.9" 12 | 13 | sphinx: 14 | configuration: doc/source/conf.py 15 | 16 | formats: all 17 | 18 | python: 19 | install: 20 | - requirements: doc/requirements.txt 21 | -------------------------------------------------------------------------------- /CHANGELOG: -------------------------------------------------------------------------------- 1 | Version 0.9.7: 2 | - Added type hints to public APIs. 3 | - Fixed leading whitespace in StreamDeck Plus serial number strings. 4 | 5 | Version 0.9.6: 6 | - Fixed StreamDeck+ returning 10 key states instead of the expected 8. 7 | - Fixed StreamDeck+ dial push event not being detected correctly. 8 | - Fixed old key states and dial states returned if requested from inside a key or dial callback function. 9 | - Added support for FreeBSD platforms. 10 | - Added support for the StreamDeck Neo. 11 | 12 | Version 0.9.5: 13 | - Added support for the StreamDeck Plus. 14 | 15 | Version 0.9.4: 16 | - Updated Windows HIDAPI backend to attempt to load from the local working directory. 17 | - Added detection for MacOS Homebrew installations of the libhidapi back-end library. 18 | 19 | Version 0.9.3: 20 | - Added support for a new sub-variant of the StreamDeck XL. 21 | 22 | Version 0.9.2: 23 | - Added support for a new sub-variant of the StreamDeck Mini. 24 | 25 | Version 0.9.1: 26 | - Transport errors now trigger a closing of the underlying StreamDeck device, so further API calls will throw correctly (and ``is_open()`` will return ``False``). 27 | - Updated animated example script to use separate cycle generators for each key, so the animations play at the correct rate regardless of key count. 28 | - Added support for the StreamDeck pedal. 29 | - Added new `is_visual()` function. 30 | 31 | Version 0.9.0: 32 | - Added new `set_poll_frequency()` function. 33 | - Added new `is_open()` function. 34 | - Fixed a possible internal thread join error when a deck object was closed. 35 | 36 | Version 0.8.5: 37 | - Add support for the new StreamDeck MK2. 38 | 39 | Version 0.8.4: 40 | - Updated animated example script to attempt to maintain a constant FPS, regardless of rendering time. 41 | - Fixed a race condition in the LibUSB HIDAPI transport backend that could cause crashes when a device was closed. 42 | 43 | Version 0.8.3: 44 | - Altered LibUSB transport workaround to only apply on Mac. 45 | - Fixed internal _extract_string() method to discard all data after the first NUL byte, fixing corrupt serial number strings being returned in some cases. 46 | - Set minimum Python version to 3.8, as some of the library uses newer syntax/core library features. 47 | 48 | Version 0.8.2: 49 | - Added new ``PILHelper.create_scaled_image()`` function to easily generate scaled/padded key images for a given deck. 50 | - Updated LibUSB transport backend so that device paths are returned as UTF-8 strings, not raw bytes. 51 | - Updated version/serial number string extraction from StreamDecks so that invalid characters are substituted, rather than raising a ``UnicodeDecodeError`` error. 52 | - Added LibUSB transport workaround for a bug on Mac platforms when using older versions of the library. 53 | 54 | Version 0.8.1: 55 | - Fixed memory leak in LibUSB HIDAPI transport backend. 56 | 57 | Version 0.8.0: 58 | - Fix random crashes in LibUSB HIDAPI transport backend on Windows, as the API is not thread safe. 59 | - Added support for atomic updates of StreamDeck instances via the Python ``with`` scope syntax. 60 | 61 | Version 0.7.3: 62 | - Fix crash in new LibUSB HIDAPI transport backend on systems with multiple connected StreamDeck devices. 63 | - Fix crash in new LibUSB HIDAPI transport backend when ``connected()`` was called on a StreamDeck instance. 64 | 65 | Version 0.7.2: 66 | - Documentation restructuring to move installation out of the readme and into the library documentation. 67 | 68 | Version 0.7.1: 69 | - Cleaned up new LibUSB HIDAPI transport backend, so that it only searches for OS-specific library files. 70 | - Fixed minor typo in the libUSB HIDAPI transport backend probe failure message. 71 | 72 | Version 0.7.0: 73 | - Removed old HID and HIDAPI backends, added new ``ctypes`` based LibUSB-HIDAPI backend replacement. 74 | 75 | Version 0.6.3: 76 | - Added support for the new V2 hardware revision of the StreamDeck Original. 77 | 78 | Version 0.6.2: 79 | - Fixed broken StreamDeck XL communications on Linux. 80 | - Added blacklist for the ``libhidapi-hidraw`` system library which breaks StreamDeck Original communications. 81 | 82 | Version 0.6.1: 83 | - Fixed broken HIDAPI backend probing. 84 | - Fixed double-open of HID backend devices causing connection issues on some platforms. 85 | 86 | Version 0.6.0: 87 | - Added support for the ``HID`` Python package. This new HID backend is strongly recommended over the old HIDAPI backend. 88 | - Added auto-probing of installed backends, if no specific transport is supplied when constructing a DeviceManager instance. 89 | 90 | Version 0.5.1: 91 | - Fixed StreamDeck XL reporting swapped rows/columns count. 92 | - Fixed StreamDeck XL failing to report correct serial number and firmware version. 93 | 94 | Version 0.5.0: 95 | - Fixed StreamDeck devices occasionally showing partial old frames on initial connection. 96 | - Removed support for RAW pixel images, StreamDeck Mini and Original take BMP images. 97 | - Removed ``width`` and ``height`` information from Deck key image dict, now returned as ``size`` tuple entry. 98 | 99 | Version 0.4.0: 100 | - Added StreamDeck XL support. 101 | 102 | Version 0.3.2: 103 | - Fixed StreamDeck Mini key images not updating under some circumstances. 104 | 105 | Version 0.3.1: 106 | - Added animated image example script. 107 | 108 | Version 0.3: 109 | - Remapped StreamDeck key indexes so that key 0 is located on the physical 110 | top-left of all supported devices. 111 | 112 | Version 0.2.4: 113 | - Added new ``StreamDeck.get_serial_number()`` function. 114 | - Added new ``StreamDeck.get_firmware_version()`` function. 115 | 116 | Version 0.2.3: 117 | - Added new ``StreamDeck.ImageHelpers modules`` for easier key image generation. 118 | -------------------------------------------------------------------------------- /ExampleDeck.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/abcminiuser/python-elgato-streamdeck/34dbf63a79d50834d554e955c8df3644eb0530be/ExampleDeck.jpg -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Permission to use, copy, modify, and distribute this software 2 | and its documentation for any purpose is hereby granted without 3 | fee, provided that the above copyright notice appear in all 4 | copies and that both that the copyright notice and this 5 | permission notice and warranty disclaimer appear in supporting 6 | documentation, and that the name of the author not be used in 7 | advertising or publicity pertaining to distribution of the 8 | software without specific, written prior permission. 9 | 10 | The author disclaims all warranties with regard to this 11 | software, including all implied warranties of merchantability 12 | and fitness. In no event shall the author be liable for any 13 | special, indirect or consequential damages or any damages 14 | whatsoever resulting from loss of use, data or profits, whether 15 | in an action of contract, negligence or other tortious action, 16 | arising out of or in connection with the use or performance of 17 | this software. 18 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | graft src/StreamDeck 2 | graft src/Assets 3 | include src/example*.py 4 | include *.md 5 | include *.jpg 6 | include VERSION 7 | include CHANGELOG 8 | include LICENSE 9 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Python Elgato Stream Deck Library 2 | 3 | ![Example Deck](ExampleDeck.jpg) 4 | 5 | This is an open source Python 3 library to control an 6 | [Elgato Stream Deck](https://www.elgato.com/en/gaming/stream-deck) directly, 7 | without the official software. This can allow you to create your own custom 8 | front-ends, such as a custom control front-end for home automation software. 9 | 10 | _________________ 11 | 12 | [PyPi Project Entry](https://pypi.org/project/streamdeck/) - [Online Documentation](https://python-elgato-streamdeck.readthedocs.io) - [Source Code](https://github.com/abcminiuser/python-elgato-streamdeck) 13 | 14 | 15 | ## Project Status: 16 | 17 | Working - you can enumerate devices, set the brightness of the panel(s), set 18 | the images shown on each button, and read the current button states. 19 | 20 | Currently the following StreamDeck products are supported in multiple hardware 21 | variants: 22 | 23 | * StreamDeck Mini 24 | * StreamDeck Neo 25 | * StreamDeck Original 26 | * StreamDeck Pedal 27 | * StreamDeck Plus 28 | * StreamDeck XL 29 | 30 | ## Package Installation: 31 | 32 | Install the library via pip: 33 | 34 | ``` 35 | pip install streamdeck 36 | ``` 37 | 38 | Alternatively, manually clone the project repository: 39 | 40 | ``` 41 | git clone https://github.com/abcminiuser/python-elgato-streamdeck.git 42 | ``` 43 | 44 | For detailed installation instructions, refer to the prebuilt 45 | [online documentation](https://python-elgato-streamdeck.readthedocs.io), or 46 | build the documentation yourself locally by running `make html` from the `docs` 47 | directory. 48 | 49 | 50 | ## Credits: 51 | 52 | I've used the reverse engineering notes from 53 | [this GitHub](https://github.com/alvancamp/node-elgato-stream-deck/blob/master/NOTES.md) 54 | repository to implement this library. Thanks Alex Van Camp! 55 | 56 | Thank you to the following contributors, large and small, for helping with the 57 | development and maintenance of this library: 58 | 59 | - [admiral0](https://github.com/admiral0) 60 | - [Aetherdyne](https://github.com/Aetherdyne) 61 | - [benedikt-bartscher](https://github.com/benedikt-bartscher) 62 | - [brimston3](https://github.com/brimston3) 63 | - [BS-Tek](https://github.com/BS-Tek) 64 | - [Core447](https://github.com/Core447) 65 | - [dirkk0](https://github.com/dirkk0) 66 | - [dodgyrabbit](https://github.com/dodgyrabbit) 67 | - [dubstech](https://github.com/dubstech) 68 | - [Giraut](https://github.com/Giraut) 69 | - [impala454](https://github.com/impala454) 70 | - [iPhoneAddict](https://github.com/iPhoneAddict) 71 | - [itsusony](https://github.com/itsusony) 72 | - [jakobbuis](https://github.com/jakobbuis) 73 | - [jmudge14](https://github.com/jmudge14) 74 | - [Kalle-Wirsch](https://github.com/Kalle-Wirsch) 75 | - [karstlok](https://github.com/karstlok) 76 | - [Lewiscowles1986](https://github.com/Lewiscowles1986) 77 | - [m-weigand](https://github.com/m-weigand) 78 | - [mathben](https://github.com/mathben) 79 | - [matrixinius](https://github.com/matrixinius) 80 | - [phillco](https://github.com/phillco) 81 | - [pointshader](https://github.com/pointshader) 82 | - [shanna](https://github.com/shanna) 83 | - [spidererrol](https://github.com/Spidererrol) 84 | - [spyoungtech](https://github.com/spyoungtech) 85 | - [Subsentient](https://github.com/Subsentient) 86 | - [swedishmike](https://github.com/swedishmike) 87 | - [TheSchmidt](https://github.com/TheSchmidt) 88 | - [theslimshaney](https://github.com/theslimshaney) 89 | - [tjemg](https://github.com/tjemg) 90 | - [VladFlorinIlie](https://github.com/VladFlorinIlie) 91 | 92 | If you've contributed in some manner, but I've accidentally missed you in the 93 | list above, please let me know. 94 | 95 | 96 | ## License: 97 | 98 | Released under the [MIT license](LICENSE). 99 | -------------------------------------------------------------------------------- /VERSION: -------------------------------------------------------------------------------- 1 | 0.9.7 2 | -------------------------------------------------------------------------------- /doc/Makefile: -------------------------------------------------------------------------------- 1 | # Minimal makefile for Sphinx documentation 2 | # 3 | 4 | # You can set these variables from the command line. 5 | SPHINXOPTS = 6 | SPHINXBUILD = sphinx-build 7 | SPHINXPROJ = python-elgato-streamdeck 8 | SOURCEDIR = source 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) -------------------------------------------------------------------------------- /doc/make.bat: -------------------------------------------------------------------------------- 1 | @ECHO OFF 2 | 3 | pushd %~dp0 4 | 5 | REM Command file for Sphinx documentation 6 | 7 | if "%SPHINXBUILD%" == "" ( 8 | set SPHINXBUILD=sphinx-build 9 | ) 10 | set SOURCEDIR=source 11 | set BUILDDIR=build 12 | set SPHINXPROJ=python-elgato-streamdeck 13 | 14 | if "%1" == "" goto help 15 | 16 | %SPHINXBUILD% >NUL 2>NUL 17 | if errorlevel 9009 ( 18 | echo. 19 | echo.The 'sphinx-build' command was not found. Make sure you have Sphinx 20 | echo.installed, then set the SPHINXBUILD environment variable to point 21 | echo.to the full path of the 'sphinx-build' executable. Alternatively you 22 | echo.may add the Sphinx directory to PATH. 23 | echo. 24 | echo.If you don't have Sphinx installed, grab it from 25 | echo.http://sphinx-doc.org/ 26 | exit /b 1 27 | ) 28 | 29 | %SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% 30 | goto end 31 | 32 | :help 33 | %SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% 34 | 35 | :end 36 | popd 37 | -------------------------------------------------------------------------------- /doc/requirements.txt: -------------------------------------------------------------------------------- 1 | Sphinx>=8.1.3 2 | sphinx-rtd-theme>=3.0.2 3 | -------------------------------------------------------------------------------- /doc/source/conf.py: -------------------------------------------------------------------------------- 1 | # Configuration file for the Sphinx documentation builder. 2 | # 3 | # This file only contains a selection of the most common options. For a full 4 | # list see the documentation: 5 | # https://www.sphinx-doc.org/en/master/usage/configuration.html 6 | 7 | # -- Path setup -------------------------------------------------------------- 8 | 9 | # If extensions (or modules to document with autodoc) are in another directory, 10 | # add these directories to sys.path here. If the directory is relative to the 11 | # documentation root, use os.path.abspath to make it absolute, like shown here. 12 | # 13 | import os 14 | import sys 15 | 16 | import sphinx_rtd_theme 17 | 18 | ROOT_PATH = os.path.join(os.path.dirname(__file__), "..", "..") 19 | 20 | with open(os.path.join(ROOT_PATH, "VERSION"), 'r') as f: 21 | package_version = f.read().strip() 22 | 23 | sys.path.insert(0, os.path.abspath(os.path.join(ROOT_PATH, 'src'))) 24 | 25 | 26 | # -- General configuration ------------------------------------------------ 27 | 28 | # If your documentation needs a minimal Sphinx version, state it here. 29 | # 30 | needs_sphinx = '3.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 34 | # ones. 35 | extensions = ['sphinx.ext.autodoc', 'sphinx_rtd_theme'] 36 | 37 | # Add any paths that contain templates here, relative to this directory. 38 | templates_path = ['_templates'] 39 | 40 | # The suffix(es) of source filenames. 41 | # You can specify multiple suffix as a list of string: 42 | # 43 | source_suffix = ['.rst'] 44 | 45 | # The master toctree document. 46 | master_doc = 'index' 47 | 48 | # General information about the project. 49 | project = 'python-elgato-streamdeck' 50 | copyright = '2023, Dean Camera' 51 | author = 'Dean Camera' 52 | 53 | # The version info for the project you're documenting, acts as replacement for 54 | # |version| and |release|, also used in various other places throughout the 55 | # built documents. 56 | # 57 | # The short X.Y version. 58 | version = package_version 59 | # The full version, including alpha/beta/rc tags. 60 | release = package_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 = 'en' 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 = [] 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 | autoclass_content = 'both' 81 | 82 | # -- Options for HTML output ---------------------------------------------- 83 | 84 | # The theme to use for HTML and HTML Help pages. See the documentation for 85 | # a list of builtin themes. 86 | # 87 | html_theme = 'sphinx_rtd_theme' 88 | 89 | # Theme options are theme-specific and customize the look and feel of a theme 90 | # further. For a list of options available for each theme, see the 91 | # documentation. 92 | # 93 | # html_theme_options = {} 94 | 95 | # Add any paths that contain custom static files (such as style sheets) here, 96 | # relative to this directory. They are copied after the builtin static files, 97 | # so a file named "default.css" will overwrite the built-in "default.css". 98 | html_static_path = [] 99 | 100 | # Custom sidebar templates, must be a dictionary that maps document names 101 | # to template names. 102 | # 103 | # This is required for the alabaster theme 104 | # refs: http://alabaster.readthedocs.io/en/latest/installation.html#sidebars 105 | html_sidebars = { 106 | '**': [ 107 | 'relations.html', # needs 'show_related': True theme option to display 108 | 'searchbox.html', 109 | ] 110 | } 111 | -------------------------------------------------------------------------------- /doc/source/examples/animated.rst: -------------------------------------------------------------------------------- 1 | ******************************* 2 | Example Script: Animated Images 3 | ******************************* 4 | 5 | The following is a complete example script to connect to attached StreamDeck 6 | devices, and display animated graphics on the keys. 7 | 8 | .. literalinclude:: ../../../src/example_animated.py 9 | :language: python 10 | -------------------------------------------------------------------------------- /doc/source/examples/basic.rst: -------------------------------------------------------------------------------- 1 | *************************** 2 | Example Script: Basic Usage 3 | *************************** 4 | 5 | The following is a complete example script to connect to attached StreamDeck 6 | devices, display custom image/text graphics on the buttons and respond to press 7 | events. 8 | 9 | .. literalinclude:: ../../../src/example_basic.py 10 | :language: python 11 | -------------------------------------------------------------------------------- /doc/source/examples/deckinfo.rst: -------------------------------------------------------------------------------- 1 | ********************************** 2 | Example Script: Device Information 3 | ********************************** 4 | 5 | The following is a complete example script to enumerate any available StreamDeck 6 | devices and print out all information on the device's location and image format. 7 | 8 | .. literalinclude:: ../../../src/example_deckinfo.py 9 | :language: python 10 | -------------------------------------------------------------------------------- /doc/source/examples/neo.rst: -------------------------------------------------------------------------------- 1 | ******************* 2 | Example Script: Neo 3 | ******************* 4 | 5 | The following is an example script to connect to the Stream Deck Neo, and print 6 | whenever its buttons are pushed and released. 7 | 8 | .. literalinclude:: ../../../src/example_neo.py 9 | :language: python 10 | -------------------------------------------------------------------------------- /doc/source/examples/pedal.rst: -------------------------------------------------------------------------------- 1 | ********************* 2 | Example Script: Pedal 3 | ********************* 4 | 5 | The following is an example script to connect to the Stream Deck Pedal, and 6 | print whenever its buttons are pushed and released. 7 | 8 | .. literalinclude:: ../../../src/example_pedal.py 9 | :language: python 10 | -------------------------------------------------------------------------------- /doc/source/examples/plus.rst: -------------------------------------------------------------------------------- 1 | **************************** 2 | Example Script: StreamDeck + 3 | **************************** 4 | 5 | The following is a complete example script to display an image on the 6 | touchscreen of the Stream Deck +, and react to dial events. 7 | 8 | .. literalinclude:: ../../../src/example_plus.py 9 | :language: python 10 | -------------------------------------------------------------------------------- /doc/source/examples/tiled.rst: -------------------------------------------------------------------------------- 1 | *************************** 2 | Example Script: Tiled Image 3 | *************************** 4 | 5 | The following is a complete example script to display a larger image across a 6 | StreamDeck, by sectioning up the image into key-sized tiles, and displaying 7 | them individually onto each key. 8 | 9 | .. literalinclude:: ../../../src/example_tileimage.py 10 | :language: python 11 | -------------------------------------------------------------------------------- /doc/source/index.rst: -------------------------------------------------------------------------------- 1 | ##### 2 | About 3 | ##### 4 | 5 | .. image:: ../../ExampleDeck.jpg 6 | :alt: Example StreamDeck Device 7 | 8 | This is an open source Python 3 library to control an `Elgato Stream Deck 9 | `_ directly, without the official 10 | software. This can allow you to create your own custom front-ends, such as a 11 | custom control front-end for home automation software. 12 | 13 | ##### 14 | Index 15 | ##### 16 | 17 | .. toctree:: 18 | :caption: Installation and Setup 19 | 20 | pages/installation.rst 21 | 22 | 23 | .. toctree:: 24 | :caption: Module Documentation 25 | :numbered: 26 | 27 | modules/discovery.rst 28 | modules/devices.rst 29 | modules/transports.rst 30 | modules/imagehelpers.rst 31 | 32 | 33 | .. toctree:: 34 | :caption: Library Examples 35 | :numbered: 36 | 37 | examples/deckinfo.rst 38 | examples/basic.rst 39 | examples/pedal.rst 40 | examples/plus.rst 41 | examples/neo.rst 42 | examples/tiled.rst 43 | examples/animated.rst 44 | 45 | 46 | .. toctree:: 47 | :caption: About 48 | 49 | pages/source.rst 50 | pages/changelog.rst 51 | pages/license.rst 52 | 53 | 54 | ################## 55 | Indices and tables 56 | ################## 57 | 58 | * :ref:`genindex` 59 | * :ref:`modindex` 60 | -------------------------------------------------------------------------------- /doc/source/modules/devices.rst: -------------------------------------------------------------------------------- 1 | *************************** 2 | Modules: StreamDeck Devices 3 | *************************** 4 | 5 | ========================== 6 | StreamDeck (Abstract Base) 7 | ========================== 8 | 9 | .. automodule:: StreamDeck.Devices.StreamDeck 10 | :members: 11 | :show-inheritance: 12 | 13 | 14 | =============== 15 | StreamDeck Mini 16 | =============== 17 | 18 | .. automodule:: StreamDeck.Devices.StreamDeckMini 19 | :members: 20 | :show-inheritance: 21 | 22 | 23 | =============== 24 | StreamDeck Neo 25 | =============== 26 | 27 | .. automodule:: StreamDeck.Devices.StreamDeckNeo 28 | :members: 29 | :show-inheritance: 30 | 31 | 32 | =================== 33 | StreamDeck Original 34 | =================== 35 | 36 | .. automodule:: StreamDeck.Devices.StreamDeckOriginal 37 | :members: 38 | :show-inheritance: 39 | 40 | 41 | ================ 42 | StreamDeck Pedal 43 | ================ 44 | 45 | .. automodule:: StreamDeck.Devices.StreamDeckPedal 46 | :members: 47 | :show-inheritance: 48 | 49 | 50 | ================ 51 | StreamDeck Plus 52 | ================ 53 | 54 | .. automodule:: StreamDeck.Devices.StreamDeckPlus 55 | :members: 56 | :show-inheritance: 57 | 58 | 59 | ============= 60 | StreamDeck XL 61 | ============= 62 | 63 | .. automodule:: StreamDeck.Devices.StreamDeckXL 64 | :members: 65 | :show-inheritance: 66 | -------------------------------------------------------------------------------- /doc/source/modules/discovery.rst: -------------------------------------------------------------------------------- 1 | ************************* 2 | Modules: Device Discovery 3 | ************************* 4 | 5 | ============== 6 | Device Manager 7 | ============== 8 | 9 | .. automodule:: StreamDeck.DeviceManager 10 | :members: 11 | :show-inheritance: 12 | -------------------------------------------------------------------------------- /doc/source/modules/imagehelpers.rst: -------------------------------------------------------------------------------- 1 | ********************** 2 | Modules: Image Helpers 3 | ********************** 4 | 5 | ================ 6 | PIL Image Helper 7 | ================ 8 | 9 | .. automodule:: StreamDeck.ImageHelpers.PILHelper 10 | :members: 11 | :show-inheritance: 12 | -------------------------------------------------------------------------------- /doc/source/modules/transports.rst: -------------------------------------------------------------------------------- 1 | ********************************* 2 | Modules: Communication Transports 3 | ********************************* 4 | 5 | ========================= 6 | Transport (Abstract Base) 7 | ========================= 8 | 9 | .. automodule:: StreamDeck.Transport.Transport 10 | :members: 11 | :show-inheritance: 12 | 13 | 14 | ================================= 15 | 'LibUSB HIDAPI' Library Transport 16 | ================================= 17 | 18 | .. automodule:: StreamDeck.Transport.LibUSBHIDAPI 19 | :members: 20 | :show-inheritance: 21 | -------------------------------------------------------------------------------- /doc/source/pages/backend_libusb_hidapi.rst: -------------------------------------------------------------------------------- 1 | ----------------------------- 2 | Default LibUSB HIDAPI Backend 3 | ----------------------------- 4 | 5 | This is the default and recommended backend - a cross platform library for 6 | communicating with HID devices. Most systems will have this as a system package 7 | available for easy installation. 8 | 9 | 10 | ^^^^^^^ 11 | Windows 12 | ^^^^^^^ 13 | 14 | Windows systems requires additional manual installation of a DLL in order to 15 | function. The latest source for the ``hidapi.dll`` DLL is the `releases page of 16 | the libUSB GitHub project `_. 17 | 18 | Place the DLL into a folder that has been added to your system ``%PATH%`` 19 | directory list (typically this includes the ``C:\Windows\System32`` folder but 20 | adding a new path would be recommended instead of modifying your Windows 21 | directory). 22 | 23 | Ensure you use the correct DLL version for your installed Python version; i.e. 24 | if you are using 32-bit Python, install the 32-bit ``hidapi.dll``. 25 | 26 | ^^^^^^^^^^^^^^ 27 | MacOS (Darwin) 28 | ^^^^^^^^^^^^^^ 29 | 30 | On MacOS systems, you can choose to either compile the `HIDAPI project 31 | `_ yourself, or install it via one of the 32 | multiple third party package managers (e.g. ``brew install hidapi``, when using 33 | Homebrew). 34 | 35 | 36 | ^^^^^^^^^^^^^^^^^^^^^ 37 | Linux (Ubuntu/Debian) 38 | ^^^^^^^^^^^^^^^^^^^^^ 39 | 40 | On Linux, the ``libhidapi-libusb0`` package is required can can be installed via 41 | the system's package manager. 42 | 43 | The following script has been verified working on a Raspberry Pi (Models 2B and 44 | 4B) running a stock Debian Buster image, to install all the required 45 | dependencies needed by this project on a fresh system:: 46 | 47 | # Ensure system is up to date, upgrade all out of date packages 48 | sudo apt update && sudo apt dist-upgrade -y 49 | 50 | # Install the pip Python package manager 51 | sudo apt install -y python3-pip python3-setuptools 52 | 53 | # Install system packages needed for the default LibUSB HIDAPI backend 54 | sudo apt install -y libudev-dev libusb-1.0-0-dev libhidapi-libusb0 55 | 56 | # Install system packages needed for the Python Pillow package installation 57 | sudo apt install -y libjpeg-dev zlib1g-dev libopenjp2-7 libtiff5 58 | 59 | # Install python library dependencies 60 | pip3 install wheel 61 | pip3 install pillow 62 | 63 | # Add udev rule to allow all users non-root access to Elgato StreamDeck devices: 64 | sudo tee /etc/udev/rules.d/10-streamdeck.rules << EOF 65 | SUBSYSTEMS=="usb", ATTRS{idVendor}=="0fd9", GROUP="users", TAG+="uaccess" 66 | EOF 67 | 68 | # Reload udev rules to ensure the new permissions take effect 69 | sudo udevadm control --reload-rules 70 | 71 | # Install the latest version of the StreamDeck library via pip 72 | pip3 install streamdeck 73 | 74 | Note that after adding the ``udev`` rules, you will need to remove and 75 | re-attach any existing StreamDeck devices to ensure they adopt the new 76 | permissions. This should allow you to access StreamDeck devices *without* 77 | needing root permissions. 78 | -------------------------------------------------------------------------------- /doc/source/pages/changelog.rst: -------------------------------------------------------------------------------- 1 | ********* 2 | Changelog 3 | ********* 4 | 5 | .. include:: ../../../CHANGELOG 6 | -------------------------------------------------------------------------------- /doc/source/pages/installation.rst: -------------------------------------------------------------------------------- 1 | ******************** 2 | Library Installation 3 | ******************** 4 | 5 | To install this library via the `pip` package manager, simply run 6 | ``pip install streamdeck`` from a terminal. 7 | 8 | The included examples require the PIL fork `pillow`, although it can be 9 | swapped out if desired by the user application for any other image manipulation 10 | library. This can also be installed with `pip` via ``pip install pillow``. 11 | 12 | ================= 13 | HID Backend Setup 14 | ================= 15 | 16 | The library core is structured so that it can use one of (potentially) several 17 | alternative HID backend libraries for the actual low level device 18 | communications. You will need to install the dependencies appropriate to your 19 | chosen backend for the library to work correctly. 20 | 21 | **Backends:** 22 | 23 | .. toctree:: 24 | :glob: 25 | :maxdepth: 1 26 | 27 | backend_libusb_hidapi.rst 28 | -------------------------------------------------------------------------------- /doc/source/pages/license.rst: -------------------------------------------------------------------------------- 1 | ******* 2 | License 3 | ******* 4 | 5 | Released under the MIT license below. 6 | 7 | .. literalinclude:: ../../../LICENSE 8 | -------------------------------------------------------------------------------- /doc/source/pages/source.rst: -------------------------------------------------------------------------------- 1 | *********** 2 | Source Code 3 | *********** 4 | 5 | Source code is `available on Github `_. Bug reports, patches, suggestions and other contributions welcome. 6 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | Pillow>=9.0.0 2 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | import setuptools 2 | 3 | with open("VERSION", 'r') as f: 4 | version = f.read().strip() 5 | 6 | with open("README.md", 'r') as f: 7 | long_description = f.read() 8 | 9 | setuptools.setup( 10 | name='streamdeck', 11 | version=version, 12 | description='Library to control Elgato StreamDeck devices.', 13 | author='Dean Camera', 14 | author_email='dean@fourwalledcubicle.com', 15 | url='https://github.com/abcminiuser/python-elgato-streamdeck', 16 | package_dir={'': 'src'}, 17 | packages=setuptools.find_packages(where='src'), 18 | install_requires=[], 19 | license="MIT", 20 | long_description=long_description, 21 | long_description_content_type="text/markdown", 22 | include_package_data=True, 23 | python_requires='>=3.9', 24 | ) 25 | -------------------------------------------------------------------------------- /src/Assets/Apache License 2.0.txt: -------------------------------------------------------------------------------- 1 | Apache License, Version 2.0 Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ 2 | 3 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 4 | 5 | 1. Definitions. 6 | 7 | "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. 8 | 9 | "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. 10 | 11 | "Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. 12 | 13 | "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. 14 | 15 | "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. 16 | 17 | "Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. 18 | 19 | "Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). 20 | 21 | "Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. 22 | 23 | "Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." 24 | 25 | "Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. 26 | 27 | 2. Grant of Copyright License. 28 | 29 | Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. 30 | 31 | 3. Grant of Patent License. 32 | 33 | Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. 34 | 35 | 4. Redistribution. 36 | 37 | You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: 38 | 39 | You must give any other recipients of the Work or Derivative Works a copy of this License; and You must cause any modified files to carry prominent notices stating that You changed the files; and You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. 40 | 41 | 5. Submission of Contributions. 42 | 43 | Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. 44 | 45 | 6. Trademarks. 46 | 47 | This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. 48 | 49 | 7. Disclaimer of Warranty. 50 | 51 | Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. 52 | 53 | 8. Limitation of Liability. 54 | 55 | In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. 56 | 57 | 9. Accepting Warranty or Additional Liability. 58 | 59 | While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. 60 | 61 | END OF TERMS AND CONDITIONS 62 | 63 | APPENDIX: How to apply the Apache License to your work 64 | 65 | To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "[]" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives. 66 | 67 | Copyright [yyyy] [name of copyright owner] 68 | 69 | Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at 70 | 71 | http://www.apache.org/licenses/LICENSE-2.0 72 | 73 | Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. 74 | -------------------------------------------------------------------------------- /src/Assets/Attribution.txt: -------------------------------------------------------------------------------- 1 | List of assets and their associated licenses: 2 | 3 | 4 | - Elephant_Walking_animated.gif 5 | 6 | Source/Author: Wikimedia Commons 7 | License: Public domain 8 | URL: https://commons.wikimedia.org/wiki/File:Elephant_walking.jpg 9 | 10 | - Exit.png 11 | 12 | Source/Author: Gradient Icons by Abhishek Pipalva 13 | License: CC BY-SA 3.0 14 | URL: https://www.iconfinder.com/abhishekpipalva 15 | 16 | - Pressed.png 17 | 18 | Source/Author: Emoji Icons by bukeicon 19 | License: CC BY-SA 3.0 20 | URL: https://www.iconfinder.com/iconsets/emoji-18 21 | 22 | - Harold.jpg 23 | 24 | Source/Author: Dean Camera 25 | Licence: CC0 (Public Domain) 26 | URL: https://www.fourwalledcubicle.com 27 | 28 | - Released.png 29 | 30 | Source/Author: Emoji Icons by bukeicon 31 | License: CC BY-SA 3.0 32 | URL: https://www.iconfinder.com/iconsets/emoji-18 33 | 34 | - RGB_color_space_animated_view.gif 35 | 36 | Source/Author: Wikimedia Commons 37 | License: CC0 (Public domain) 38 | URL: https://commons.wikimedia.org/wiki/File:RGB_color_space_animated_view.gif 39 | 40 | - Roboto-Regular.ttf 41 | 42 | Source/Author: Roboto Font by Google 43 | Licence: Apache License 2.0 44 | URL: https://fonts.google.com/specimen/Roboto 45 | 46 | - Simple_CV_Joint_animated.gif 47 | 48 | Source/Author: Wikimedia Commons 49 | License: Public domain 50 | URL: https://commons.wikimedia.org/wiki/File:Simple_CV_Joint_animated.gif 51 | -------------------------------------------------------------------------------- /src/Assets/CC BY-SA 3.0 License.txt: -------------------------------------------------------------------------------- 1 | CREATIVE COMMONS CORPORATION IS NOT A LAW FIRM AND DOES NOT PROVIDE LEGAL SERVICES. DISTRIBUTION OF THIS LICENSE DOES NOT CREATE AN ATTORNEY-CLIENT RELATIONSHIP. CREATIVE COMMONS PROVIDES THIS INFORMATION ON AN "AS-IS" BASIS. CREATIVE COMMONS MAKES NO WARRANTIES REGARDING THE INFORMATION PROVIDED, AND DISCLAIMS LIABILITY FOR DAMAGES RESULTING FROM ITS USE. 2 | 3 | License 4 | 5 | THE WORK (AS DEFINED BELOW) IS PROVIDED UNDER THE TERMS OF THIS CREATIVE COMMONS PUBLIC LICENSE ("CCPL" OR "LICENSE"). THE WORK IS PROTECTED BY COPYRIGHT AND/OR OTHER APPLICABLE LAW. ANY USE OF THE WORK OTHER THAN AS AUTHORIZED UNDER THIS LICENSE OR COPYRIGHT LAW IS PROHIBITED. 6 | 7 | BY EXERCISING ANY RIGHTS TO THE WORK PROVIDED HERE, YOU ACCEPT AND AGREE TO BE BOUND BY THE TERMS OF THIS LICENSE. TO THE EXTENT THIS LICENSE MAY BE CONSIDERED TO BE A CONTRACT, THE LICENSOR GRANTS YOU THE RIGHTS CONTAINED HERE IN CONSIDERATION OF YOUR ACCEPTANCE OF SUCH TERMS AND CONDITIONS. 8 | 9 | 1. Definitions 10 | 11 | "Adaptation" means a work based upon the Work, or upon the Work and other pre-existing works, such as a translation, adaptation, derivative work, arrangement of music or other alterations of a literary or artistic work, or phonogram or performance and includes cinematographic adaptations or any other form in which the Work may be recast, transformed, or adapted including in any form recognizably derived from the original, except that a work that constitutes a Collection will not be considered an Adaptation for the purpose of this License. For the avoidance of doubt, where the Work is a musical work, performance or phonogram, the synchronization of the Work in timed-relation with a moving image ("synching") will be considered an Adaptation for the purpose of this License. 12 | "Collection" means a collection of literary or artistic works, such as encyclopedias and anthologies, or performances, phonograms or broadcasts, or other works or subject matter other than works listed in Section 1(f) below, which, by reason of the selection and arrangement of their contents, constitute intellectual creations, in which the Work is included in its entirety in unmodified form along with one or more other contributions, each constituting separate and independent works in themselves, which together are assembled into a collective whole. A work that constitutes a Collection will not be considered an Adaptation (as defined below) for the purposes of this License. 13 | "Creative Commons Compatible License" means a license that is listed at https://creativecommons.org/compatiblelicenses that has been approved by Creative Commons as being essentially equivalent to this License, including, at a minimum, because that license: (i) contains terms that have the same purpose, meaning and effect as the License Elements of this License; and, (ii) explicitly permits the relicensing of adaptations of works made available under that license under this License or a Creative Commons jurisdiction license with the same License Elements as this License. 14 | "Distribute" means to make available to the public the original and copies of the Work or Adaptation, as appropriate, through sale or other transfer of ownership. 15 | "License Elements" means the following high-level license attributes as selected by Licensor and indicated in the title of this License: Attribution, ShareAlike. 16 | "Licensor" means the individual, individuals, entity or entities that offer(s) the Work under the terms of this License. 17 | "Original Author" means, in the case of a literary or artistic work, the individual, individuals, entity or entities who created the Work or if no individual or entity can be identified, the publisher; and in addition (i) in the case of a performance the actors, singers, musicians, dancers, and other persons who act, sing, deliver, declaim, play in, interpret or otherwise perform literary or artistic works or expressions of folklore; (ii) in the case of a phonogram the producer being the person or legal entity who first fixes the sounds of a performance or other sounds; and, (iii) in the case of broadcasts, the organization that transmits the broadcast. 18 | "Work" means the literary and/or artistic work offered under the terms of this License including without limitation any production in the literary, scientific and artistic domain, whatever may be the mode or form of its expression including digital form, such as a book, pamphlet and other writing; a lecture, address, sermon or other work of the same nature; a dramatic or dramatico-musical work; a choreographic work or entertainment in dumb show; a musical composition with or without words; a cinematographic work to which are assimilated works expressed by a process analogous to cinematography; a work of drawing, painting, architecture, sculpture, engraving or lithography; a photographic work to which are assimilated works expressed by a process analogous to photography; a work of applied art; an illustration, map, plan, sketch or three-dimensional work relative to geography, topography, architecture or science; a performance; a broadcast; a phonogram; a compilation of data to the extent it is protected as a copyrightable work; or a work performed by a variety or circus performer to the extent it is not otherwise considered a literary or artistic work. 19 | "You" means an individual or entity exercising rights under this License who has not previously violated the terms of this License with respect to the Work, or who has received express permission from the Licensor to exercise rights under this License despite a previous violation. 20 | "Publicly Perform" means to perform public recitations of the Work and to communicate to the public those public recitations, by any means or process, including by wire or wireless means or public digital performances; to make available to the public Works in such a way that members of the public may access these Works from a place and at a place individually chosen by them; to perform the Work to the public by any means or process and the communication to the public of the performances of the Work, including by public digital performance; to broadcast and rebroadcast the Work by any means including signs, sounds or images. 21 | "Reproduce" means to make copies of the Work by any means including without limitation by sound or visual recordings and the right of fixation and reproducing fixations of the Work, including storage of a protected performance or phonogram in digital form or other electronic medium. 22 | 23 | 2. Fair Dealing Rights. Nothing in this License is intended to reduce, limit, or restrict any uses free from copyright or rights arising from limitations or exceptions that are provided for in connection with the copyright protection under copyright law or other applicable laws. 24 | 25 | 3. License Grant. Subject to the terms and conditions of this License, Licensor hereby grants You a worldwide, royalty-free, non-exclusive, perpetual (for the duration of the applicable copyright) license to exercise the rights in the Work as stated below: 26 | 27 | to Reproduce the Work, to incorporate the Work into one or more Collections, and to Reproduce the Work as incorporated in the Collections; 28 | to create and Reproduce Adaptations provided that any such Adaptation, including any translation in any medium, takes reasonable steps to clearly label, demarcate or otherwise identify that changes were made to the original Work. For example, a translation could be marked "The original work was translated from English to Spanish," or a modification could indicate "The original work has been modified."; 29 | to Distribute and Publicly Perform the Work including as incorporated in Collections; and, 30 | to Distribute and Publicly Perform Adaptations. 31 | 32 | For the avoidance of doubt: 33 | Non-waivable Compulsory License Schemes. In those jurisdictions in which the right to collect royalties through any statutory or compulsory licensing scheme cannot be waived, the Licensor reserves the exclusive right to collect such royalties for any exercise by You of the rights granted under this License; 34 | Waivable Compulsory License Schemes. In those jurisdictions in which the right to collect royalties through any statutory or compulsory licensing scheme can be waived, the Licensor waives the exclusive right to collect such royalties for any exercise by You of the rights granted under this License; and, 35 | Voluntary License Schemes. The Licensor waives the right to collect royalties, whether individually or, in the event that the Licensor is a member of a collecting society that administers voluntary licensing schemes, via that society, from any exercise by You of the rights granted under this License. 36 | 37 | The above rights may be exercised in all media and formats whether now known or hereafter devised. The above rights include the right to make such modifications as are technically necessary to exercise the rights in other media and formats. Subject to Section 8(f), all rights not expressly granted by Licensor are hereby reserved. 38 | 39 | 4. Restrictions. The license granted in Section 3 above is expressly made subject to and limited by the following restrictions: 40 | 41 | You may Distribute or Publicly Perform the Work only under the terms of this License. You must include a copy of, or the Uniform Resource Identifier (URI) for, this License with every copy of the Work You Distribute or Publicly Perform. You may not offer or impose any terms on the Work that restrict the terms of this License or the ability of the recipient of the Work to exercise the rights granted to that recipient under the terms of the License. You may not sublicense the Work. You must keep intact all notices that refer to this License and to the disclaimer of warranties with every copy of the Work You Distribute or Publicly Perform. When You Distribute or Publicly Perform the Work, You may not impose any effective technological measures on the Work that restrict the ability of a recipient of the Work from You to exercise the rights granted to that recipient under the terms of the License. This Section 4(a) applies to the Work as incorporated in a Collection, but this does not require the Collection apart from the Work itself to be made subject to the terms of this License. If You create a Collection, upon notice from any Licensor You must, to the extent practicable, remove from the Collection any credit as required by Section 4(c), as requested. If You create an Adaptation, upon notice from any Licensor You must, to the extent practicable, remove from the Adaptation any credit as required by Section 4(c), as requested. 42 | You may Distribute or Publicly Perform an Adaptation only under the terms of: (i) this License; (ii) a later version of this License with the same License Elements as this License; (iii) a Creative Commons jurisdiction license (either this or a later license version) that contains the same License Elements as this License (e.g., Attribution-ShareAlike 3.0 US)); (iv) a Creative Commons Compatible License. If you license the Adaptation under one of the licenses mentioned in (iv), you must comply with the terms of that license. If you license the Adaptation under the terms of any of the licenses mentioned in (i), (ii) or (iii) (the "Applicable License"), you must comply with the terms of the Applicable License generally and the following provisions: (I) You must include a copy of, or the URI for, the Applicable License with every copy of each Adaptation You Distribute or Publicly Perform; (II) You may not offer or impose any terms on the Adaptation that restrict the terms of the Applicable License or the ability of the recipient of the Adaptation to exercise the rights granted to that recipient under the terms of the Applicable License; (III) You must keep intact all notices that refer to the Applicable License and to the disclaimer of warranties with every copy of the Work as included in the Adaptation You Distribute or Publicly Perform; (IV) when You Distribute or Publicly Perform the Adaptation, You may not impose any effective technological measures on the Adaptation that restrict the ability of a recipient of the Adaptation from You to exercise the rights granted to that recipient under the terms of the Applicable License. This Section 4(b) applies to the Adaptation as incorporated in a Collection, but this does not require the Collection apart from the Adaptation itself to be made subject to the terms of the Applicable License. 43 | If You Distribute, or Publicly Perform the Work or any Adaptations or Collections, You must, unless a request has been made pursuant to Section 4(a), keep intact all copyright notices for the Work and provide, reasonable to the medium or means You are utilizing: (i) the name of the Original Author (or pseudonym, if applicable) if supplied, and/or if the Original Author and/or Licensor designate another party or parties (e.g., a sponsor institute, publishing entity, journal) for attribution ("Attribution Parties") in Licensor's copyright notice, terms of service or by other reasonable means, the name of such party or parties; (ii) the title of the Work if supplied; (iii) to the extent reasonably practicable, the URI, if any, that Licensor specifies to be associated with the Work, unless such URI does not refer to the copyright notice or licensing information for the Work; and (iv) , consistent with Ssection 3(b), in the case of an Adaptation, a credit identifying the use of the Work in the Adaptation (e.g., "French translation of the Work by Original Author," or "Screenplay based on original Work by Original Author"). The credit required by this Section 4(c) may be implemented in any reasonable manner; provided, however, that in the case of a Adaptation or Collection, at a minimum such credit will appear, if a credit for all contributing authors of the Adaptation or Collection appears, then as part of these credits and in a manner at least as prominent as the credits for the other contributing authors. For the avoidance of doubt, You may only use the credit required by this Section for the purpose of attribution in the manner set out above and, by exercising Your rights under this License, You may not implicitly or explicitly assert or imply any connection with, sponsorship or endorsement by the Original Author, Licensor and/or Attribution Parties, as appropriate, of You or Your use of the Work, without the separate, express prior written permission of the Original Author, Licensor and/or Attribution Parties. 44 | Except as otherwise agreed in writing by the Licensor or as may be otherwise permitted by applicable law, if You Reproduce, Distribute or Publicly Perform the Work either by itself or as part of any Adaptations or Collections, You must not distort, mutilate, modify or take other derogatory action in relation to the Work which would be prejudicial to the Original Author's honor or reputation. Licensor agrees that in those jurisdictions (e.g. Japan), in which any exercise of the right granted in Section 3(b) of this License (the right to make Adaptations) would be deemed to be a distortion, mutilation, modification or other derogatory action prejudicial to the Original Author's honor and reputation, the Licensor will waive or not assert, as appropriate, this Section, to the fullest extent permitted by the applicable national law, to enable You to reasonably exercise Your right under Section 3(b) of this License (right to make Adaptations) but not otherwise. 45 | 46 | 5. Representations, Warranties and Disclaimer 47 | 48 | UNLESS OTHERWISE MUTUALLY AGREED TO BY THE PARTIES IN WRITING, LICENSOR OFFERS THE WORK AS-IS AND MAKES NO REPRESENTATIONS OR WARRANTIES OF ANY KIND CONCERNING THE WORK, EXPRESS, IMPLIED, STATUTORY OR OTHERWISE, INCLUDING, WITHOUT LIMITATION, WARRANTIES OF TITLE, MERCHANTIBILITY, FITNESS FOR A PARTICULAR PURPOSE, NONINFRINGEMENT, OR THE ABSENCE OF LATENT OR OTHER DEFECTS, ACCURACY, OR THE PRESENCE OF ABSENCE OF ERRORS, WHETHER OR NOT DISCOVERABLE. SOME JURISDICTIONS DO NOT ALLOW THE EXCLUSION OF IMPLIED WARRANTIES, SO SUCH EXCLUSION MAY NOT APPLY TO YOU. 49 | 50 | 6. Limitation on Liability. EXCEPT TO THE EXTENT REQUIRED BY APPLICABLE LAW, IN NO EVENT WILL LICENSOR BE LIABLE TO YOU ON ANY LEGAL THEORY FOR ANY SPECIAL, INCIDENTAL, CONSEQUENTIAL, PUNITIVE OR EXEMPLARY DAMAGES ARISING OUT OF THIS LICENSE OR THE USE OF THE WORK, EVEN IF LICENSOR HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. 51 | 52 | 7. Termination 53 | 54 | This License and the rights granted hereunder will terminate automatically upon any breach by You of the terms of this License. Individuals or entities who have received Adaptations or Collections from You under this License, however, will not have their licenses terminated provided such individuals or entities remain in full compliance with those licenses. Sections 1, 2, 5, 6, 7, and 8 will survive any termination of this License. 55 | Subject to the above terms and conditions, the license granted here is perpetual (for the duration of the applicable copyright in the Work). Notwithstanding the above, Licensor reserves the right to release the Work under different license terms or to stop distributing the Work at any time; provided, however that any such election will not serve to withdraw this License (or any other license that has been, or is required to be, granted under the terms of this License), and this License will continue in full force and effect unless terminated as stated above. 56 | 57 | 8. Miscellaneous 58 | 59 | Each time You Distribute or Publicly Perform the Work or a Collection, the Licensor offers to the recipient a license to the Work on the same terms and conditions as the license granted to You under this License. 60 | Each time You Distribute or Publicly Perform an Adaptation, Licensor offers to the recipient a license to the original Work on the same terms and conditions as the license granted to You under this License. 61 | If any provision of this License is invalid or unenforceable under applicable law, it shall not affect the validity or enforceability of the remainder of the terms of this License, and without further action by the parties to this agreement, such provision shall be reformed to the minimum extent necessary to make such provision valid and enforceable. 62 | No term or provision of this License shall be deemed waived and no breach consented to unless such waiver or consent shall be in writing and signed by the party to be charged with such waiver or consent. 63 | This License constitutes the entire agreement between the parties with respect to the Work licensed here. There are no understandings, agreements or representations with respect to the Work not specified here. Licensor shall not be bound by any additional provisions that may appear in any communication from You. This License may not be modified without the mutual written agreement of the Licensor and You. 64 | The rights granted under, and the subject matter referenced, in this License were drafted utilizing the terminology of the Berne Convention for the Protection of Literary and Artistic Works (as amended on September 28, 1979), the Rome Convention of 1961, the WIPO Copyright Treaty of 1996, the WIPO Performances and Phonograms Treaty of 1996 and the Universal Copyright Convention (as revised on July 24, 1971). These rights and subject matter take effect in the relevant jurisdiction in which the License terms are sought to be enforced according to the corresponding provisions of the implementation of those treaty provisions in the applicable national law. If the standard suite of rights granted under applicable copyright law includes additional rights not granted under this License, such additional rights are deemed to be included in the License; this License is not intended to restrict the license of any rights under applicable law. 65 | 66 | Creative Commons Notice 67 | 68 | Creative Commons is not a party to this License, and makes no warranty whatsoever in connection with the Work. Creative Commons will not be liable to You or any party on any legal theory for any damages whatsoever, including without limitation any general, special, incidental or consequential damages arising in connection to this license. Notwithstanding the foregoing two (2) sentences, if Creative Commons has expressly identified itself as the Licensor hereunder, it shall have all rights and obligations of Licensor. 69 | 70 | Except for the limited purpose of indicating to the public that the Work is licensed under the CCPL, Creative Commons does not authorize the use by either party of the trademark "Creative Commons" or any related trademark or logo of Creative Commons without the prior written consent of Creative Commons. Any permitted use will be in compliance with Creative Commons' then-current trademark usage guidelines, as may be published on its website or otherwise made available upon request from time to time. For the avoidance of doubt, this trademark restriction does not form part of the License. 71 | 72 | Creative Commons may be contacted at https://creativecommons.org/. 73 | -------------------------------------------------------------------------------- /src/Assets/CC0 License.txt: -------------------------------------------------------------------------------- 1 | Creative Commons Legal Code 2 | 3 | CC0 1.0 Universal 4 | 5 | CREATIVE COMMONS CORPORATION IS NOT A LAW FIRM AND DOES NOT PROVIDE 6 | LEGAL SERVICES. DISTRIBUTION OF THIS DOCUMENT DOES NOT CREATE AN 7 | ATTORNEY-CLIENT RELATIONSHIP. CREATIVE COMMONS PROVIDES THIS 8 | INFORMATION ON AN "AS-IS" BASIS. CREATIVE COMMONS MAKES NO WARRANTIES 9 | REGARDING THE USE OF THIS DOCUMENT OR THE INFORMATION OR WORKS 10 | PROVIDED HEREUNDER, AND DISCLAIMS LIABILITY FOR DAMAGES RESULTING FROM 11 | THE USE OF THIS DOCUMENT OR THE INFORMATION OR WORKS PROVIDED 12 | HEREUNDER. 13 | 14 | Statement of Purpose 15 | 16 | The laws of most jurisdictions throughout the world automatically confer 17 | exclusive Copyright and Related Rights (defined below) upon the creator 18 | and subsequent owner(s) (each and all, an "owner") of an original work of 19 | authorship and/or a database (each, a "Work"). 20 | 21 | Certain owners wish to permanently relinquish those rights to a Work for 22 | the purpose of contributing to a commons of creative, cultural and 23 | scientific works ("Commons") that the public can reliably and without fear 24 | of later claims of infringement build upon, modify, incorporate in other 25 | works, reuse and redistribute as freely as possible in any form whatsoever 26 | and for any purposes, including without limitation commercial purposes. 27 | These owners may contribute to the Commons to promote the ideal of a free 28 | culture and the further production of creative, cultural and scientific 29 | works, or to gain reputation or greater distribution for their Work in 30 | part through the use and efforts of others. 31 | 32 | For these and/or other purposes and motivations, and without any 33 | expectation of additional consideration or compensation, the person 34 | associating CC0 with a Work (the "Affirmer"), to the extent that he or she 35 | is an owner of Copyright and Related Rights in the Work, voluntarily 36 | elects to apply CC0 to the Work and publicly distribute the Work under its 37 | terms, with knowledge of his or her Copyright and Related Rights in the 38 | Work and the meaning and intended legal effect of CC0 on those rights. 39 | 40 | 1. Copyright and Related Rights. A Work made available under CC0 may be 41 | protected by copyright and related or neighboring rights ("Copyright and 42 | Related Rights"). Copyright and Related Rights include, but are not 43 | limited to, the following: 44 | 45 | i. the right to reproduce, adapt, distribute, perform, display, 46 | communicate, and translate a Work; 47 | ii. moral rights retained by the original author(s) and/or performer(s); 48 | iii. publicity and privacy rights pertaining to a person's image or 49 | likeness depicted in a Work; 50 | iv. rights protecting against unfair competition in regards to a Work, 51 | subject to the limitations in paragraph 4(a), below; 52 | v. rights protecting the extraction, dissemination, use and reuse of data 53 | in a Work; 54 | vi. database rights (such as those arising under Directive 96/9/EC of the 55 | European Parliament and of the Council of 11 March 1996 on the legal 56 | protection of databases, and under any national implementation 57 | thereof, including any amended or successor version of such 58 | directive); and 59 | vii. other similar, equivalent or corresponding rights throughout the 60 | world based on applicable law or treaty, and any national 61 | implementations thereof. 62 | 63 | 2. Waiver. To the greatest extent permitted by, but not in contravention 64 | of, applicable law, Affirmer hereby overtly, fully, permanently, 65 | irrevocably and unconditionally waives, abandons, and surrenders all of 66 | Affirmer's Copyright and Related Rights and associated claims and causes 67 | of action, whether now known or unknown (including existing as well as 68 | future claims and causes of action), in the Work (i) in all territories 69 | worldwide, (ii) for the maximum duration provided by applicable law or 70 | treaty (including future time extensions), (iii) in any current or future 71 | medium and for any number of copies, and (iv) for any purpose whatsoever, 72 | including without limitation commercial, advertising or promotional 73 | purposes (the "Waiver"). Affirmer makes the Waiver for the benefit of each 74 | member of the public at large and to the detriment of Affirmer's heirs and 75 | successors, fully intending that such Waiver shall not be subject to 76 | revocation, rescission, cancellation, termination, or any other legal or 77 | equitable action to disrupt the quiet enjoyment of the Work by the public 78 | as contemplated by Affirmer's express Statement of Purpose. 79 | 80 | 3. Public License Fallback. Should any part of the Waiver for any reason 81 | be judged legally invalid or ineffective under applicable law, then the 82 | Waiver shall be preserved to the maximum extent permitted taking into 83 | account Affirmer's express Statement of Purpose. In addition, to the 84 | extent the Waiver is so judged Affirmer hereby grants to each affected 85 | person a royalty-free, non transferable, non sublicensable, non exclusive, 86 | irrevocable and unconditional license to exercise Affirmer's Copyright and 87 | Related Rights in the Work (i) in all territories worldwide, (ii) for the 88 | maximum duration provided by applicable law or treaty (including future 89 | time extensions), (iii) in any current or future medium and for any number 90 | of copies, and (iv) for any purpose whatsoever, including without 91 | limitation commercial, advertising or promotional purposes (the 92 | "License"). The License shall be deemed effective as of the date CC0 was 93 | applied by Affirmer to the Work. Should any part of the License for any 94 | reason be judged legally invalid or ineffective under applicable law, such 95 | partial invalidity or ineffectiveness shall not invalidate the remainder 96 | of the License, and in such case Affirmer hereby affirms that he or she 97 | will not (i) exercise any of his or her remaining Copyright and Related 98 | Rights in the Work or (ii) assert any associated claims and causes of 99 | action with respect to the Work, in either case contrary to Affirmer's 100 | express Statement of Purpose. 101 | 102 | 4. Limitations and Disclaimers. 103 | 104 | a. No trademark or patent rights held by Affirmer are waived, abandoned, 105 | surrendered, licensed or otherwise affected by this document. 106 | b. Affirmer offers the Work as-is and makes no representations or 107 | warranties of any kind concerning the Work, express, implied, 108 | statutory or otherwise, including without limitation warranties of 109 | title, merchantability, fitness for a particular purpose, non 110 | infringement, or the absence of latent or other defects, accuracy, or 111 | the present or absence of errors, whether or not discoverable, all to 112 | the greatest extent permissible under applicable law. 113 | c. Affirmer disclaims responsibility for clearing rights of other persons 114 | that may apply to the Work or any use thereof, including without 115 | limitation any person's Copyright and Related Rights in the Work. 116 | Further, Affirmer disclaims responsibility for obtaining any necessary 117 | consents, permissions or other rights required for any use of the 118 | Work. 119 | d. Affirmer understands and acknowledges that Creative Commons is not a 120 | party to this document and has no duty or obligation with respect to 121 | this CC0 or use of the Work. 122 | -------------------------------------------------------------------------------- /src/Assets/Elephant_Walking_animated.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/abcminiuser/python-elgato-streamdeck/34dbf63a79d50834d554e955c8df3644eb0530be/src/Assets/Elephant_Walking_animated.gif -------------------------------------------------------------------------------- /src/Assets/Exit.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/abcminiuser/python-elgato-streamdeck/34dbf63a79d50834d554e955c8df3644eb0530be/src/Assets/Exit.png -------------------------------------------------------------------------------- /src/Assets/Harold.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/abcminiuser/python-elgato-streamdeck/34dbf63a79d50834d554e955c8df3644eb0530be/src/Assets/Harold.jpg -------------------------------------------------------------------------------- /src/Assets/Pressed.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/abcminiuser/python-elgato-streamdeck/34dbf63a79d50834d554e955c8df3644eb0530be/src/Assets/Pressed.png -------------------------------------------------------------------------------- /src/Assets/RGB_color_space_animated_view.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/abcminiuser/python-elgato-streamdeck/34dbf63a79d50834d554e955c8df3644eb0530be/src/Assets/RGB_color_space_animated_view.gif -------------------------------------------------------------------------------- /src/Assets/Released.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/abcminiuser/python-elgato-streamdeck/34dbf63a79d50834d554e955c8df3644eb0530be/src/Assets/Released.png -------------------------------------------------------------------------------- /src/Assets/Roboto-Regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/abcminiuser/python-elgato-streamdeck/34dbf63a79d50834d554e955c8df3644eb0530be/src/Assets/Roboto-Regular.ttf -------------------------------------------------------------------------------- /src/Assets/Simple_CV_Joint_animated.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/abcminiuser/python-elgato-streamdeck/34dbf63a79d50834d554e955c8df3644eb0530be/src/Assets/Simple_CV_Joint_animated.gif -------------------------------------------------------------------------------- /src/StreamDeck/DeviceManager.py: -------------------------------------------------------------------------------- 1 | # Python Stream Deck Library 2 | # Released under the MIT license 3 | # 4 | # dean [at] fourwalledcubicle [dot] com 5 | # www.fourwalledcubicle.com 6 | # 7 | 8 | from .Devices.StreamDeck import StreamDeck 9 | from .Devices.StreamDeckMini import StreamDeckMini 10 | from .Devices.StreamDeckNeo import StreamDeckNeo 11 | from .Devices.StreamDeckOriginal import StreamDeckOriginal 12 | from .Devices.StreamDeckOriginalV2 import StreamDeckOriginalV2 13 | from .Devices.StreamDeckXL import StreamDeckXL 14 | from .Devices.StreamDeckPedal import StreamDeckPedal 15 | from .Devices.StreamDeckPlus import StreamDeckPlus 16 | from .Transport import Transport 17 | from .Transport.Dummy import Dummy 18 | from .Transport.LibUSBHIDAPI import LibUSBHIDAPI 19 | from .ProductIDs import USBVendorIDs, USBProductIDs 20 | 21 | 22 | class ProbeError(Exception): 23 | """ 24 | Exception thrown when attempting to probe for attached StreamDeck devices, 25 | but no suitable valid transport was found. 26 | """ 27 | 28 | pass 29 | 30 | 31 | class DeviceManager: 32 | """ 33 | Central device manager, to enumerate any attached StreamDeck devices. An 34 | instance of this class must be created in order to detect and use any 35 | StreamDeck devices. 36 | """ 37 | 38 | USB_VID_ELGATO = 0x0fd9 39 | USB_PID_STREAMDECK_ORIGINAL = 0x0060 40 | USB_PID_STREAMDECK_ORIGINAL_V2 = 0x006d 41 | USB_PID_STREAMDECK_MINI = 0x0063 42 | USB_PID_STREAMDECK_NEO = 0x009a 43 | USB_PID_STREAMDECK_XL = 0x006c 44 | USB_PID_STREAMDECK_MK2 = 0x0080 45 | USB_PID_STREAMDECK_PEDAL = 0x0086 46 | USB_PID_STREAMDECK_PLUS = 0x0084 47 | 48 | @staticmethod 49 | def _get_transport(transport: str | None): 50 | """ 51 | Creates a new HID transport instance from the given transport back-end 52 | name. If no specific transport is supplied, an attempt to find an 53 | installed backend will be made. 54 | 55 | :param str transport: Name of a supported HID transport back-end to use, None to autoprobe. 56 | 57 | :rtype: Transport.* instance 58 | :return: Instance of a HID Transport class 59 | """ 60 | 61 | transports = { 62 | "dummy": Dummy, 63 | "libusb": LibUSBHIDAPI, 64 | } 65 | 66 | if transport: 67 | transport_class = transports.get(transport) 68 | 69 | if transport_class is None: 70 | raise ProbeError("Unknown HID transport backend \"{}\".".format(transport)) 71 | 72 | try: 73 | transport_class.probe() 74 | return transport_class() 75 | except Exception as transport_error: 76 | raise ProbeError("Probe failed on HID backend \"{}\".".format(transport), transport_error) 77 | else: 78 | probe_errors = {} 79 | 80 | for transport_name, transport_class in transports.items(): 81 | if transport_name == "dummy": 82 | continue 83 | 84 | try: 85 | transport_class.probe() 86 | return transport_class() 87 | except Exception as transport_error: 88 | probe_errors[transport_name] = transport_error 89 | 90 | raise ProbeError("Probe failed to find any functional HID backend.", probe_errors) 91 | 92 | def __init__(self, transport: str | None = None): 93 | """ 94 | Creates a new StreamDeck DeviceManager, used to detect attached StreamDeck devices. 95 | 96 | :param str transport: name of the the specific HID transport back-end to use, None to auto-probe. 97 | """ 98 | self.transport: Transport.Transport = self._get_transport(transport) 99 | 100 | def enumerate(self) -> list[StreamDeck]: 101 | """ 102 | Detect attached StreamDeck devices. 103 | 104 | :rtype: list(StreamDeck) 105 | :return: list of :class:`StreamDeck` instances, one for each detected device. 106 | """ 107 | 108 | products = [ 109 | (USBVendorIDs.USB_VID_ELGATO, USBProductIDs.USB_PID_STREAMDECK_ORIGINAL, StreamDeckOriginal), 110 | (USBVendorIDs.USB_VID_ELGATO, USBProductIDs.USB_PID_STREAMDECK_ORIGINAL_V2, StreamDeckOriginalV2), 111 | (USBVendorIDs.USB_VID_ELGATO, USBProductIDs.USB_PID_STREAMDECK_MINI, StreamDeckMini), 112 | (USBVendorIDs.USB_VID_ELGATO, USBProductIDs.USB_PID_STREAMDECK_NEO, StreamDeckNeo), 113 | (USBVendorIDs.USB_VID_ELGATO, USBProductIDs.USB_PID_STREAMDECK_XL, StreamDeckXL), 114 | (USBVendorIDs.USB_VID_ELGATO, USBProductIDs.USB_PID_STREAMDECK_MK2, StreamDeckOriginalV2), 115 | (USBVendorIDs.USB_VID_ELGATO, USBProductIDs.USB_PID_STREAMDECK_PEDAL, StreamDeckPedal), 116 | (USBVendorIDs.USB_VID_ELGATO, USBProductIDs.USB_PID_STREAMDECK_MINI_MK2, StreamDeckMini), 117 | (USBVendorIDs.USB_VID_ELGATO, USBProductIDs.USB_PID_STREAMDECK_XL_V2, StreamDeckXL), 118 | (USBVendorIDs.USB_VID_ELGATO, USBProductIDs.USB_PID_STREAMDECK_PLUS, StreamDeckPlus), 119 | ] 120 | 121 | streamdecks = list() 122 | 123 | for vid, pid, class_type in products: 124 | found_devices = self.transport.enumerate(vid=vid, pid=pid) 125 | streamdecks.extend([class_type(d) for d in found_devices]) 126 | 127 | return streamdecks 128 | -------------------------------------------------------------------------------- /src/StreamDeck/Devices/StreamDeck.py: -------------------------------------------------------------------------------- 1 | # Python Stream Deck Library 2 | # Released under the MIT license 3 | # 4 | # dean [at] fourwalledcubicle [dot] com 5 | # www.fourwalledcubicle.com 6 | # 7 | 8 | import threading 9 | import time 10 | from abc import ABC, abstractmethod 11 | from collections.abc import Callable 12 | from enum import Enum 13 | from typing import Any, Iterable, TypeVar 14 | 15 | from ..Transport.Transport import Transport, TransportError 16 | 17 | 18 | class TouchscreenEventType(Enum): 19 | """ 20 | Type of event that has occurred for a Touchscreen. 21 | """ 22 | 23 | SHORT = 1 24 | LONG = 2 25 | DRAG = 3 26 | 27 | 28 | class DialEventType(Enum): 29 | """ 30 | Type of event that has occurred for a Dial. 31 | """ 32 | TURN = 1 33 | PUSH = 2 34 | 35 | 36 | class ControlType(Enum): 37 | """ 38 | Type of control. This is used when returning internal events from a 39 | StreamDeck subclass. 40 | 41 | :meta private: 42 | """ 43 | KEY = 1 44 | DIAL = 2 45 | TOUCHSCREEN = 3 46 | 47 | 48 | class StreamDeck(ABC): 49 | """ 50 | Represents a physically attached StreamDeck device. 51 | """ 52 | 53 | KEY_COUNT = 0 54 | KEY_COLS = 0 55 | KEY_ROWS = 0 56 | 57 | TOUCH_KEY_COUNT = 0 58 | 59 | KEY_PIXEL_WIDTH = 0 60 | KEY_PIXEL_HEIGHT = 0 61 | KEY_IMAGE_FORMAT = "" 62 | KEY_FLIP = (False, False) 63 | KEY_ROTATION = 0 64 | 65 | TOUCHSCREEN_PIXEL_WIDTH = 0 66 | TOUCHSCREEN_PIXEL_HEIGHT = 0 67 | TOUCHSCREEN_IMAGE_FORMAT = "" 68 | TOUCHSCREEN_FLIP = (False, False) 69 | TOUCHSCREEN_ROTATION = 0 70 | 71 | SCREEN_PIXEL_WIDTH = 0 72 | SCREEN_PIXEL_HEIGHT = 0 73 | SCREEN_IMAGE_FORMAT = "" 74 | SCREEN_FLIP = (False, False) 75 | SCREEN_ROTATION = 0 76 | 77 | DIAL_COUNT = 0 78 | 79 | DECK_TYPE = "" 80 | DECK_VISUAL = False 81 | DECK_TOUCH = False 82 | 83 | _Self = TypeVar('_Self', bound='StreamDeck') 84 | KeyCallback = Callable[[_Self, int, bool], None] | None 85 | DialCallback = Callable[[_Self, int, DialEventType, bool], None] | None 86 | TouchScreenCallback = Callable[[_Self, TouchscreenEventType, Any], None] | None 87 | 88 | def __init__(self, device: Transport.Device): 89 | self.device: Transport.Device = device 90 | self.last_key_states: list[bool] = [False] * (self.KEY_COUNT + self.TOUCH_KEY_COUNT) 91 | self.last_dial_states: list[bool] = [False] * self.DIAL_COUNT 92 | self.read_thread: threading.Thread | None = None 93 | self.run_read_thread: bool = False 94 | self.read_poll_hz: int = 20 95 | 96 | self.key_callback: StreamDeck.KeyCallback = None 97 | self.dial_callback: StreamDeck.DialCallback = None 98 | self.touchscreen_callback: StreamDeck.TouchScreenCallback = None 99 | 100 | self.update_lock: threading.RLock = threading.RLock() 101 | 102 | def __del__(self): 103 | """ 104 | Delete handler for the StreamDeck, automatically closing the transport 105 | if it is currently open and terminating the transport reader thread. 106 | """ 107 | try: 108 | self._setup_reader(None) 109 | except (TransportError, ValueError): 110 | pass 111 | 112 | try: 113 | self.device.close() 114 | except (TransportError): 115 | pass 116 | 117 | def __enter__(self): 118 | """ 119 | Enter handler for the StreamDeck, taking the exclusive update lock on 120 | the deck. This can be used in a `with` statement to ensure that only one 121 | thread is currently updating the deck, even if it is doing multiple 122 | operations (e.g. setting the image on multiple keys). 123 | """ 124 | self.update_lock.acquire() 125 | 126 | def __exit__(self, type, value, traceback): 127 | """ 128 | Exit handler for the StreamDeck, releasing the exclusive update lock on 129 | the deck. 130 | """ 131 | self.update_lock.release() 132 | 133 | @abstractmethod 134 | def _read_control_states(self) -> None: 135 | """ 136 | Reads the raw key states from an attached StreamDeck. 137 | 138 | :return: dictionary containing states for all controls 139 | """ 140 | pass 141 | 142 | @abstractmethod 143 | def _reset_key_stream(self) -> None: 144 | """ 145 | Sends a blank key report to the StreamDeck, resetting the key image 146 | streamer in the device. This prevents previously started partial key 147 | writes that were not completed from corrupting images sent from this 148 | application. 149 | """ 150 | pass 151 | 152 | def _extract_string(self, data: Iterable[int]) -> str: 153 | """ 154 | Extracts out a human-readable string from a collection of raw bytes, 155 | removing any trailing whitespace or data after and before the first NUL 156 | byte. 157 | """ 158 | 159 | return str(bytes(data), 'ascii', 'replace').partition('\0')[0].strip() 160 | 161 | def _read(self) -> None: 162 | """ 163 | Read handler for the underlying transport, listening for control state 164 | changes on the underlying device, caching the new states and firing off 165 | any registered callbacks. 166 | """ 167 | while self.run_read_thread: 168 | try: 169 | control_states = self._read_control_states() 170 | if control_states is None: 171 | time.sleep(1.0 / self.read_poll_hz) 172 | continue 173 | 174 | if ControlType.KEY in control_states: 175 | for k, (old, new) in enumerate(zip(self.last_key_states, control_states[ControlType.KEY])): 176 | if old == new: 177 | continue 178 | 179 | self.last_key_states[k] = new 180 | 181 | if self.key_callback is not None: 182 | self.key_callback(self, k, new) 183 | 184 | elif ControlType.DIAL in control_states: 185 | if DialEventType.PUSH in control_states[ControlType.DIAL]: 186 | for k, (old, new) in enumerate(zip(self.last_dial_states, control_states[ControlType.DIAL][DialEventType.PUSH])): 187 | if old == new: 188 | continue 189 | 190 | self.last_dial_states[k] = new 191 | 192 | if self.dial_callback is not None: 193 | self.dial_callback(self, k, DialEventType.PUSH, new) 194 | 195 | if DialEventType.TURN in control_states[ControlType.DIAL]: 196 | for k, amount in enumerate(control_states[ControlType.DIAL][DialEventType.TURN]): 197 | if amount == 0: 198 | continue 199 | 200 | if self.dial_callback is not None: 201 | self.dial_callback(self, k, DialEventType.TURN, amount) 202 | 203 | elif ControlType.TOUCHSCREEN in control_states: 204 | if self.touchscreen_callback is not None: 205 | self.touchscreen_callback(self, *control_states[ControlType.TOUCHSCREEN]) 206 | 207 | except TransportError: 208 | self.run_read_thread = False 209 | self.close() 210 | 211 | def _setup_reader(self, callback: Callable) -> None: 212 | """ 213 | Sets up the internal transport reader thread with the given callback, 214 | for asynchronous processing of HID events from the device. If the thread 215 | already exists, it is terminated and restarted with the new callback 216 | function. 217 | 218 | :param function callback: Callback to run on the reader thread. 219 | """ 220 | if self.read_thread is not None: 221 | self.run_read_thread = False 222 | 223 | try: 224 | self.read_thread.join() 225 | except RuntimeError: 226 | pass 227 | 228 | if callback is not None: 229 | self.run_read_thread = True 230 | self.read_thread = threading.Thread(target=callback) 231 | self.read_thread.daemon = True 232 | self.read_thread.start() 233 | 234 | def open(self) -> None: 235 | """ 236 | Opens the device for input/output. This must be called prior to setting 237 | or retrieving any device state. 238 | 239 | .. seealso:: See :func:`~StreamDeck.close` for the corresponding close method. 240 | """ 241 | self.device.open() 242 | 243 | self._reset_key_stream() 244 | self._setup_reader(self._read) 245 | 246 | def close(self) -> None: 247 | """ 248 | Closes the device for input/output. 249 | 250 | .. seealso:: See :func:`~StreamDeck.open` for the corresponding open method. 251 | """ 252 | self.device.close() 253 | 254 | def is_open(self) -> bool: 255 | """ 256 | Indicates if the StreamDeck device is currently open and ready for use. 257 | 258 | :rtype: bool 259 | :return: `True` if the deck is open, `False` otherwise. 260 | """ 261 | return self.device.is_open() 262 | 263 | def connected(self) -> bool: 264 | """ 265 | Indicates if the physical StreamDeck device this instance is attached to 266 | is still connected to the host. 267 | 268 | :rtype: bool 269 | :return: `True` if the deck is still connected, `False` otherwise. 270 | """ 271 | return self.device.connected() 272 | 273 | def vendor_id(self) -> int: 274 | """ 275 | Retrieves the vendor ID attached StreamDeck. This can be used 276 | to determine the exact type of attached StreamDeck. 277 | 278 | :rtype: int 279 | :return: Vendor ID of the attached device. 280 | """ 281 | return self.device.vendor_id() 282 | 283 | def product_id(self) -> int: 284 | """ 285 | Retrieves the product ID attached StreamDeck. This can be used 286 | to determine the exact type of attached StreamDeck. 287 | 288 | :rtype: int 289 | :return: Product ID of the attached device. 290 | """ 291 | return self.device.product_id() 292 | 293 | def id(self) -> str: 294 | """ 295 | Retrieves the physical ID of the attached StreamDeck. This can be used 296 | to differentiate one StreamDeck from another. 297 | 298 | :rtype: str 299 | :return: Identifier for the attached device. 300 | """ 301 | return self.device.path() 302 | 303 | def key_count(self) -> int: 304 | """ 305 | Retrieves number of physical buttons on the attached StreamDeck device. 306 | 307 | :rtype: int 308 | :return: Number of physical buttons. 309 | """ 310 | return self.KEY_COUNT 311 | 312 | def touch_key_count(self) -> int: 313 | """ 314 | Retrieves number of touch buttons on the attached StreamDeck device. 315 | 316 | :rtype: int 317 | :return: Number of touch buttons. 318 | """ 319 | return self.TOUCH_KEY_COUNT 320 | 321 | def dial_count(self) -> int: 322 | """ 323 | Retrieves number of physical dials on the attached StreamDeck device. 324 | 325 | :rtype: int 326 | :return: Number of physical dials 327 | """ 328 | return self.DIAL_COUNT 329 | 330 | def deck_type(self) -> str: 331 | """ 332 | Retrieves the model of Stream Deck. 333 | 334 | :rtype: str 335 | :return: String containing the model name of the StreamDeck device. 336 | """ 337 | return self.DECK_TYPE 338 | 339 | def is_visual(self) -> bool: 340 | """ 341 | Returns whether the Stream Deck has a visual display output. 342 | 343 | :rtype: bool 344 | :return: `True` if the deck has a screen, `False` otherwise. 345 | """ 346 | return self.DECK_VISUAL 347 | 348 | def is_touch(self) -> bool: 349 | """ 350 | Returns whether the Stream Deck can receive touch events 351 | 352 | :rtype: bool 353 | :return: `True` if the deck can receive touch events, `False` otherwise 354 | """ 355 | return self.DECK_TOUCH 356 | 357 | def key_layout(self) -> tuple[int, int]: 358 | """ 359 | Retrieves the physical button layout on the attached StreamDeck device. 360 | 361 | :rtype: (int, int) 362 | :return (rows, columns): Number of button rows and columns. 363 | """ 364 | return self.KEY_ROWS, self.KEY_COLS 365 | 366 | def key_image_format(self): 367 | """ 368 | Retrieves the image format accepted by the attached StreamDeck device. 369 | Images should be given in this format when setting an image on a button. 370 | 371 | .. seealso:: See :func:`~StreamDeck.set_key_image` method to update the 372 | image displayed on a StreamDeck button. 373 | 374 | :rtype: dict() 375 | :return: Dictionary describing the various image parameters 376 | (size, image format, image mirroring and rotation). 377 | """ 378 | return { 379 | 'size': (self.KEY_PIXEL_WIDTH, self.KEY_PIXEL_HEIGHT), 380 | 'format': self.KEY_IMAGE_FORMAT, 381 | 'flip': self.KEY_FLIP, 382 | 'rotation': self.KEY_ROTATION, 383 | } 384 | 385 | def touchscreen_image_format(self): 386 | """ 387 | Retrieves the image format accepted by the touchscreen of the Stream 388 | Deck. Images should be given in this format when drawing on 389 | touchscreen. 390 | 391 | .. seealso:: See :func:`~StreamDeck.set_touchscreen_image` method to 392 | draw an image on the StreamDeck touchscreen. 393 | 394 | :rtype: dict() 395 | :return: Dictionary describing the various image parameters 396 | (size, image format). 397 | """ 398 | return { 399 | 'size': (self.TOUCHSCREEN_PIXEL_WIDTH, self.TOUCHSCREEN_PIXEL_HEIGHT), 400 | 'format': self.TOUCHSCREEN_IMAGE_FORMAT, 401 | 'flip': self.TOUCHSCREEN_FLIP, 402 | 'rotation': self.TOUCHSCREEN_ROTATION, 403 | } 404 | 405 | def screen_image_format(self): 406 | """ 407 | Retrieves the image format accepted by the screen of the Stream 408 | Deck. Images should be given in this format when drawing on 409 | screen. 410 | 411 | .. seealso:: See :func:`~StreamDeck.set_screen_image` method to 412 | draw an image on the StreamDeck screen. 413 | 414 | :rtype: dict() 415 | :return: Dictionary describing the various image parameters 416 | (size, image format). 417 | """ 418 | return { 419 | 'size': (self.SCREEN_PIXEL_WIDTH, self.SCREEN_PIXEL_HEIGHT), 420 | 'format': self.SCREEN_IMAGE_FORMAT, 421 | 'flip': self.SCREEN_FLIP, 422 | 'rotation': self.SCREEN_ROTATION, 423 | } 424 | 425 | def set_poll_frequency(self, hz: int) -> None: 426 | """ 427 | Sets the frequency of the button polling reader thread, determining how 428 | often the StreamDeck will be polled for button changes. 429 | 430 | A higher frequency will result in a higher CPU usage, but a lower 431 | latency between a physical button press and a event from the library. 432 | 433 | :param int hz: Reader thread frequency, in Hz (1-1000). 434 | """ 435 | self.read_poll_hz = min(max(hz, 1), 1000) 436 | 437 | def set_key_callback(self, callback: KeyCallback) -> None: 438 | """ 439 | Sets the callback function called each time a button on the StreamDeck 440 | changes state (either pressed, or released). 441 | 442 | .. note:: This callback will be fired from an internal reader thread. 443 | Ensure that the given callback function is thread-safe. 444 | 445 | .. note:: Only one callback can be registered at one time. 446 | 447 | .. seealso:: See :func:`~StreamDeck.set_key_callback_async` method for 448 | a version compatible with Python 3 `asyncio` asynchronous 449 | functions. 450 | 451 | :param function callback: Callback function to fire each time a button 452 | state changes. 453 | """ 454 | self.key_callback = callback 455 | 456 | def set_key_callback_async(self, async_callback: KeyCallback, loop=None): 457 | """ 458 | Sets the asynchronous callback function called each time a button on the 459 | StreamDeck changes state (either pressed, or released). The given 460 | callback should be compatible with Python 3's `asyncio` routines. 461 | 462 | .. note:: The asynchronous callback will be fired in a thread-safe 463 | manner. 464 | 465 | .. note:: This will override the callback (if any) set by 466 | :func:`~StreamDeck.set_key_callback`. 467 | 468 | :param function async_callback: Asynchronous callback function to fire 469 | each time a button state changes. 470 | :param asyncio.loop loop: Asyncio loop to dispatch the callback into 471 | """ 472 | import asyncio 473 | 474 | loop = loop or asyncio.get_event_loop() 475 | 476 | def callback(*args): 477 | asyncio.run_coroutine_threadsafe(async_callback(*args), loop) 478 | 479 | self.set_key_callback(callback) 480 | 481 | def set_dial_callback(self, callback: DialCallback) -> None: 482 | """ 483 | Sets the callback function called each time there is an interaction 484 | with a dial on the StreamDeck. 485 | 486 | .. note:: This callback will be fired from an internal reader thread. 487 | Ensure that the given callback function is thread-safe. 488 | 489 | .. note:: Only one callback can be registered at one time. 490 | 491 | .. seealso:: See :func:`~StreamDeck.set_dial_callback_async` method 492 | for a version compatible with Python 3 `asyncio` 493 | asynchronous functions. 494 | 495 | :param function callback: Callback function to fire each time a button 496 | state changes. 497 | """ 498 | self.dial_callback = callback 499 | 500 | def set_dial_callback_async(self, async_callback: DialCallback, loop=None) -> None: 501 | """ 502 | Sets the asynchronous callback function called each time there is an 503 | interaction with a dial on the StreamDeck. The given callback should 504 | be compatible with Python 3's `asyncio` routines. 505 | 506 | .. note:: The asynchronous callback will be fired in a thread-safe 507 | manner. 508 | 509 | .. note:: This will override the callback (if any) set by 510 | :func:`~StreamDeck.set_dial_callback`. 511 | 512 | :param function async_callback: Asynchronous callback function to fire 513 | each time a button state changes. 514 | :param asyncio.loop loop: Asyncio loop to dispatch the callback into 515 | """ 516 | import asyncio 517 | 518 | loop = loop or asyncio.get_event_loop() 519 | 520 | def callback(*args): 521 | asyncio.run_coroutine_threadsafe(async_callback(*args), loop) 522 | 523 | self.set_dial_callback(callback) 524 | 525 | def set_touchscreen_callback(self, callback: TouchScreenCallback) -> None: 526 | """ 527 | Sets the callback function called each time there is an interaction 528 | with a touchscreen on the StreamDeck. 529 | 530 | .. note:: This callback will be fired from an internal reader thread. 531 | Ensure that the given callback function is thread-safe. 532 | 533 | .. note:: Only one callback can be registered at one time. 534 | 535 | .. seealso:: See :func:`~StreamDeck.set_touchscreen_callback_async` 536 | method for a version compatible with Python 3 `asyncio` 537 | asynchronous functions. 538 | 539 | :param function callback: Callback function to fire each time a button 540 | state changes. 541 | """ 542 | self.touchscreen_callback = callback 543 | 544 | def set_touchscreen_callback_async(self, async_callback: TouchScreenCallback, loop=None) -> None: 545 | """ 546 | Sets the asynchronous callback function called each time there is an 547 | interaction with the touchscreen on the StreamDeck. The given callback 548 | should be compatible with Python 3's `asyncio` routines. 549 | 550 | .. note:: The asynchronous callback will be fired in a thread-safe 551 | manner. 552 | 553 | .. note:: This will override the callback (if any) set by 554 | :func:`~StreamDeck.set_touchscreen_callback`. 555 | 556 | :param function async_callback: Asynchronous callback function to fire 557 | each time a button state changes. 558 | :param asyncio.loop loop: Asyncio loop to dispatch the callback into 559 | """ 560 | import asyncio 561 | 562 | loop = loop or asyncio.get_event_loop() 563 | 564 | def callback(*args): 565 | asyncio.run_coroutine_threadsafe(async_callback(*args), loop) 566 | 567 | self.set_touchscreen_callback(callback) 568 | 569 | def key_states(self) -> list[bool]: 570 | """ 571 | Retrieves the current states of the buttons on the StreamDeck. 572 | 573 | :rtype: list(bool) 574 | :return: List describing the current states of each of the buttons on 575 | the device (`True` if the button is being pressed, `False` 576 | otherwise). 577 | """ 578 | return self.last_key_states 579 | 580 | def dial_states(self) -> list[bool]: 581 | """ 582 | Retrieves the current states of the dials (pressed or not) on the 583 | Stream Deck 584 | 585 | :rtype: list(bool) 586 | :return: List describing the current states of each of the dials on 587 | the device (`True` if the dial is being pressed, `False` 588 | otherwise). 589 | """ 590 | return self.last_dial_states 591 | 592 | @abstractmethod 593 | def reset(self) -> None: 594 | """ 595 | Resets the StreamDeck, clearing all button images and showing the 596 | standby image. 597 | """ 598 | pass 599 | 600 | @abstractmethod 601 | def set_brightness(self, percent: int | float) -> None: 602 | """ 603 | Sets the global screen brightness of the StreamDeck, across all the 604 | physical buttons. 605 | 606 | :param int/float percent: brightness percent, from [0-100] as an `int`, 607 | or normalized to [0.0-1.0] as a `float`. 608 | """ 609 | pass 610 | 611 | @abstractmethod 612 | def get_serial_number(self) -> str: 613 | """ 614 | Gets the serial number of the attached StreamDeck. 615 | 616 | :rtype: str 617 | :return: String containing the serial number of the attached device. 618 | """ 619 | pass 620 | 621 | @abstractmethod 622 | def get_firmware_version(self) -> str: 623 | """ 624 | Gets the firmware version of the attached StreamDeck. 625 | 626 | :rtype: str 627 | :return: String containing the firmware version of the attached device. 628 | """ 629 | pass 630 | 631 | @abstractmethod 632 | def set_key_image(self, key: int, image: bytes) -> None: 633 | """ 634 | Sets the image of a button on the StreamDeck to the given image. The 635 | image being set should be in the correct format for the device, as an 636 | enumerable collection of bytes. 637 | 638 | .. seealso:: See :func:`~StreamDeck.key_image_format` method for 639 | information on the image format accepted by the device. 640 | 641 | :param int key: Index of the button whose image is to be updated. 642 | :param enumerable image: Raw data of the image to set on the button. 643 | If `None`, the key will be cleared to a black 644 | color. 645 | """ 646 | pass 647 | 648 | @abstractmethod 649 | def set_touchscreen_image(self, image: bytes, x_pos: int = 0, y_pos: int = 0, width: int = 0, height: int = 0): 650 | """ 651 | Draws an image on the touchscreen in a certain position. The image 652 | should be in the correct format for the devices, as an enumerable 653 | collection of bytes. 654 | 655 | .. seealso:: See :func:`~StreamDeck.touchscreen_image_format` method for 656 | information on the image format accepted by the device. 657 | 658 | :param enumerable image: Raw data of the image to set on the button. 659 | If `None`, the touchscreen will be cleared. 660 | :param int x_pos: Position on x axis of the image to draw 661 | :param int y_pos: Position on y axis of the image to draw 662 | :param int width: width of the image 663 | :param int height: height of the image 664 | 665 | """ 666 | pass 667 | 668 | @abstractmethod 669 | def set_key_color(self, key: int, r: int, g: int, b: int) -> None: 670 | """ 671 | Sets the color of the touch buttons. These buttons are indexed 672 | in order after the standard keys. 673 | 674 | :param int key: Index of the button 675 | :param int r: Red value 676 | :param int g: Green value 677 | :param int b: Blue value 678 | 679 | """ 680 | pass 681 | 682 | @abstractmethod 683 | def set_screen_image(self, image: bytes) -> None: 684 | """ 685 | Draws an image on the touchless screen of the StreamDeck. 686 | 687 | .. seealso:: See :func:`~StreamDeck.screen_image_format` method for 688 | information on the image format accepted by the device. 689 | 690 | :param enumerable image: Raw data of the image to set on the button. 691 | If `None`, the screen will be cleared. 692 | """ 693 | pass 694 | -------------------------------------------------------------------------------- /src/StreamDeck/Devices/StreamDeckMini.py: -------------------------------------------------------------------------------- 1 | # Python Stream Deck Library 2 | # Released under the MIT license 3 | # 4 | # dean [at] fourwalledcubicle [dot] com 5 | # www.fourwalledcubicle.com 6 | # 7 | 8 | from .StreamDeck import StreamDeck, ControlType 9 | from ..ProductIDs import USBProductIDs 10 | 11 | 12 | class StreamDeckMini(StreamDeck): 13 | """ 14 | Represents a physically attached StreamDeck Mini device. 15 | """ 16 | 17 | KEY_COUNT = 6 18 | KEY_COLS = 3 19 | KEY_ROWS = 2 20 | 21 | KEY_PIXEL_WIDTH = 80 22 | KEY_PIXEL_HEIGHT = 80 23 | KEY_IMAGE_FORMAT = "BMP" 24 | KEY_FLIP = (False, True) 25 | KEY_ROTATION = 90 26 | 27 | DECK_TYPE = "Stream Deck Mini" 28 | DECK_VISUAL = True 29 | 30 | IMAGE_REPORT_LENGTH = 1024 31 | IMAGE_REPORT_HEADER_LENGTH = 16 32 | IMAGE_REPORT_PAYLOAD_LENGTH = IMAGE_REPORT_LENGTH - IMAGE_REPORT_HEADER_LENGTH 33 | 34 | # 80 x 80 black BMP 35 | BLANK_KEY_IMAGE = [ 36 | 0x42, 0x4d, 0xf6, 0x3c, 0x00, 0x00, 0x00, 0x00, 37 | 0x00, 0x00, 0x36, 0x00, 0x00, 0x00, 0x28, 0x00, 38 | 0x00, 0x00, 0x48, 0x00, 0x00, 0x00, 0x48, 0x00, 39 | 0x00, 0x00, 0x01, 0x00, 0x18, 0x00, 0x00, 0x00, 40 | 0x00, 0x00, 0xc0, 0x3c, 0x00, 0x00, 0xc4, 0x0e, 41 | 0x00, 0x00, 0xc4, 0x0e, 0x00, 0x00, 0x00, 0x00, 42 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 43 | ] + [0] * (KEY_PIXEL_WIDTH * KEY_PIXEL_HEIGHT * 3) 44 | 45 | def _read_control_states(self): 46 | states = self.device.read(1 + self.KEY_COUNT) 47 | if states is None: 48 | return None 49 | 50 | states = states[1:] 51 | return { 52 | ControlType.KEY: [bool(s) for s in states], 53 | } 54 | 55 | def _reset_key_stream(self): 56 | payload = bytearray(self.IMAGE_REPORT_LENGTH) 57 | payload[0] = 0x02 58 | self.device.write(payload) 59 | 60 | def reset(self): 61 | payload = bytearray(17) 62 | payload[0:2] = [0x0B, 0x63] 63 | self.device.write_feature(payload) 64 | 65 | def set_brightness(self, percent): 66 | if isinstance(percent, float): 67 | percent = int(100.0 * percent) 68 | 69 | percent = min(max(percent, 0), 100) 70 | 71 | payload = bytearray(17) 72 | payload[0:6] = [0x05, 0x55, 0xaa, 0xd1, 0x01, percent] 73 | self.device.write_feature(payload) 74 | 75 | def get_serial_number(self): 76 | report_read_length = 17 if self.device.product_id() == USBProductIDs.USB_PID_STREAMDECK_MINI else 32 77 | serial = self.device.read_feature(0x03, report_read_length) 78 | return self._extract_string(serial[5:]) 79 | 80 | def get_firmware_version(self): 81 | version = self.device.read_feature(0x04, 17) 82 | return self._extract_string(version[5:]) 83 | 84 | def set_key_image(self, key, image): 85 | if min(max(key, 0), self.KEY_COUNT) != key: 86 | raise IndexError("Invalid key index {}.".format(key)) 87 | 88 | image = bytes(image or self.BLANK_KEY_IMAGE) 89 | 90 | page_number = 0 91 | bytes_remaining = len(image) 92 | while bytes_remaining > 0: 93 | this_length = min(bytes_remaining, self.IMAGE_REPORT_PAYLOAD_LENGTH) 94 | bytes_sent = page_number * self.IMAGE_REPORT_PAYLOAD_LENGTH 95 | 96 | header = [ 97 | 0x02, 98 | 0x01, 99 | page_number, 100 | 0, 101 | 1 if this_length == bytes_remaining else 0, 102 | key + 1, 103 | 0, 104 | 0, 105 | 0, 106 | 0, 107 | 0, 108 | 0, 109 | 0, 110 | 0, 111 | 0, 112 | 0, 113 | ] 114 | 115 | payload = bytes(header) + image[bytes_sent:bytes_sent + this_length] 116 | padding = bytearray(self.IMAGE_REPORT_LENGTH - len(payload)) 117 | self.device.write(payload + padding) 118 | 119 | bytes_remaining = bytes_remaining - this_length 120 | page_number = page_number + 1 121 | 122 | def set_touchscreen_image(self, image, x_pos=0, y_pos=0, width=0, height=0): 123 | pass 124 | 125 | def set_key_color(self, key, r, g, b): 126 | pass 127 | 128 | def set_screen_image(self, image): 129 | pass 130 | -------------------------------------------------------------------------------- /src/StreamDeck/Devices/StreamDeckNeo.py: -------------------------------------------------------------------------------- 1 | # Python Stream Deck Library 2 | # Released under the MIT license 3 | # 4 | # dean [at] fourwalledcubicle [dot] com 5 | # www.fourwalledcubicle.com 6 | # 7 | 8 | from .StreamDeck import StreamDeck, ControlType 9 | 10 | 11 | class StreamDeckNeo(StreamDeck): 12 | """ 13 | Represents a physically attached StreamDeck Neo device. 14 | """ 15 | 16 | KEY_COUNT = 8 17 | KEY_COLS = 4 18 | KEY_ROWS = 2 19 | 20 | TOUCH_KEY_COUNT = 2 21 | 22 | KEY_PIXEL_WIDTH = 96 23 | KEY_PIXEL_HEIGHT = 96 24 | KEY_IMAGE_FORMAT = "JPEG" 25 | KEY_FLIP = (True, True) 26 | KEY_ROTATION = 0 27 | 28 | DECK_TYPE = "Stream Deck Neo" 29 | DECK_VISUAL = True 30 | 31 | SCREEN_PIXEL_WIDTH = 248 32 | SCREEN_PIXEL_HEIGHT = 58 33 | SCREEN_IMAGE_FORMAT = "JPEG" 34 | SCREEN_FLIP = (True, True) 35 | SCREEN_ROTATION = 0 36 | 37 | IMAGE_REPORT_LENGTH = 1024 38 | IMAGE_REPORT_HEADER_LENGTH = 8 39 | IMAGE_REPORT_PAYLOAD_LENGTH = IMAGE_REPORT_LENGTH - IMAGE_REPORT_HEADER_LENGTH 40 | 41 | # 96 x 96 black JPEG 42 | BLANK_KEY_IMAGE = [ 43 | 0xff, 0xd8, 0xff, 0xe0, 0x00, 0x10, 0x4a, 0x46, 0x49, 0x46, 0x00, 0x01, 0x01, 0x00, 0x00, 0x01, 0x00, 0x01, 0x00, 44 | 0x00, 0xff, 0xdb, 0x00, 0x43, 0x00, 0x08, 0x06, 0x06, 0x07, 0x06, 0x05, 0x08, 0x07, 0x07, 0x07, 0x09, 0x09, 0x08, 45 | 0x0a, 0x0c, 0x14, 0x0d, 0x0c, 0x0b, 0x0b, 0x0c, 0x19, 0x12, 0x13, 0x0f, 0x14, 0x1d, 0x1a, 0x1f, 0x1e, 0x1d, 0x1a, 46 | 0x1c, 0x1c, 0x20, 0x24, 0x2e, 0x27, 0x20, 0x22, 0x2c, 0x23, 0x1c, 0x1c, 0x28, 0x37, 0x29, 0x2c, 0x30, 0x31, 0x34, 47 | 0x34, 0x34, 0x1f, 0x27, 0x39, 0x3d, 0x38, 0x32, 0x3c, 0x2e, 0x33, 0x34, 0x32, 0xff, 0xdb, 0x00, 0x43, 0x01, 0x09, 48 | 0x09, 0x09, 0x0c, 0x0b, 0x0c, 0x18, 0x0d, 0x0d, 0x18, 0x32, 0x21, 0x1c, 0x21, 0x32, 0x32, 0x32, 0x32, 0x32, 0x32, 49 | 0x32, 0x32, 0x32, 0x32, 0x32, 0x32, 0x32, 0x32, 0x32, 0x32, 0x32, 0x32, 0x32, 0x32, 0x32, 0x32, 0x32, 0x32, 0x32, 50 | 0x32, 0x32, 0x32, 0x32, 0x32, 0x32, 0x32, 0x32, 0x32, 0x32, 0x32, 0x32, 0x32, 0x32, 0x32, 0x32, 0x32, 0x32, 0x32, 51 | 0x32, 0x32, 0x32, 0x32, 0x32, 0x32, 0xff, 0xc0, 0x00, 0x11, 0x08, 0x00, 0x60, 0x00, 0x60, 0x03, 0x01, 0x22, 0x00, 52 | 0x02, 0x11, 0x01, 0x03, 0x11, 0x01, 0xff, 0xc4, 0x00, 0x1f, 0x00, 0x00, 0x01, 0x05, 0x01, 0x01, 0x01, 0x01, 0x01, 53 | 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 54 | 0x0b, 0xff, 0xc4, 0x00, 0xb5, 0x10, 0x00, 0x02, 0x01, 0x03, 0x03, 0x02, 0x04, 0x03, 0x05, 0x05, 0x04, 0x04, 0x00, 55 | 0x00, 0x01, 0x7d, 0x01, 0x02, 0x03, 0x00, 0x04, 0x11, 0x05, 0x12, 0x21, 0x31, 0x41, 0x06, 0x13, 0x51, 0x61, 0x07, 56 | 0x22, 0x71, 0x14, 0x32, 0x81, 0x91, 0xa1, 0x08, 0x23, 0x42, 0xb1, 0xc1, 0x15, 0x52, 0xd1, 0xf0, 0x24, 0x33, 0x62, 57 | 0x72, 0x82, 0x09, 0x0a, 0x16, 0x17, 0x18, 0x19, 0x1a, 0x25, 0x26, 0x27, 0x28, 0x29, 0x2a, 0x34, 0x35, 0x36, 0x37, 58 | 0x38, 0x39, 0x3a, 0x43, 0x44, 0x45, 0x46, 0x47, 0x48, 0x49, 0x4a, 0x53, 0x54, 0x55, 0x56, 0x57, 0x58, 0x59, 0x5a, 59 | 0x63, 0x64, 0x65, 0x66, 0x67, 0x68, 0x69, 0x6a, 0x73, 0x74, 0x75, 0x76, 0x77, 0x78, 0x79, 0x7a, 0x83, 0x84, 0x85, 60 | 0x86, 0x87, 0x88, 0x89, 0x8a, 0x92, 0x93, 0x94, 0x95, 0x96, 0x97, 0x98, 0x99, 0x9a, 0xa2, 0xa3, 0xa4, 0xa5, 0xa6, 61 | 0xa7, 0xa8, 0xa9, 0xaa, 0xb2, 0xb3, 0xb4, 0xb5, 0xb6, 0xb7, 0xb8, 0xb9, 0xba, 0xc2, 0xc3, 0xc4, 0xc5, 0xc6, 0xc7, 62 | 0xc8, 0xc9, 0xca, 0xd2, 0xd3, 0xd4, 0xd5, 0xd6, 0xd7, 0xd8, 0xd9, 0xda, 0xe1, 0xe2, 0xe3, 0xe4, 0xe5, 0xe6, 0xe7, 63 | 0xe8, 0xe9, 0xea, 0xf1, 0xf2, 0xf3, 0xf4, 0xf5, 0xf6, 0xf7, 0xf8, 0xf9, 0xfa, 0xff, 0xc4, 0x00, 0x1f, 0x01, 0x00, 64 | 0x03, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x02, 0x03, 65 | 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0xff, 0xc4, 0x00, 0xb5, 0x11, 0x00, 0x02, 0x01, 0x02, 0x04, 0x04, 66 | 0x03, 0x04, 0x07, 0x05, 0x04, 0x04, 0x00, 0x01, 0x02, 0x77, 0x00, 0x01, 0x02, 0x03, 0x11, 0x04, 0x05, 0x21, 0x31, 67 | 0x06, 0x12, 0x41, 0x51, 0x07, 0x61, 0x71, 0x13, 0x22, 0x32, 0x81, 0x08, 0x14, 0x42, 0x91, 0xa1, 0xb1, 0xc1, 0x09, 68 | 0x23, 0x33, 0x52, 0xf0, 0x15, 0x62, 0x72, 0xd1, 0x0a, 0x16, 0x24, 0x34, 0xe1, 0x25, 0xf1, 0x17, 0x18, 0x19, 0x1a, 69 | 0x26, 0x27, 0x28, 0x29, 0x2a, 0x35, 0x36, 0x37, 0x38, 0x39, 0x3a, 0x43, 0x44, 0x45, 0x46, 0x47, 0x48, 0x49, 0x4a, 70 | 0x53, 0x54, 0x55, 0x56, 0x57, 0x58, 0x59, 0x5a, 0x63, 0x64, 0x65, 0x66, 0x67, 0x68, 0x69, 0x6a, 0x73, 0x74, 0x75, 71 | 0x76, 0x77, 0x78, 0x79, 0x7a, 0x82, 0x83, 0x84, 0x85, 0x86, 0x87, 0x88, 0x89, 0x8a, 0x92, 0x93, 0x94, 0x95, 0x96, 72 | 0x97, 0x98, 0x99, 0x9a, 0xa2, 0xa3, 0xa4, 0xa5, 0xa6, 0xa7, 0xa8, 0xa9, 0xaa, 0xb2, 0xb3, 0xb4, 0xb5, 0xb6, 0xb7, 73 | 0xb8, 0xb9, 0xba, 0xc2, 0xc3, 0xc4, 0xc5, 0xc6, 0xc7, 0xc8, 0xc9, 0xca, 0xd2, 0xd3, 0xd4, 0xd5, 0xd6, 0xd7, 0xd8, 74 | 0xd9, 0xda, 0xe2, 0xe3, 0xe4, 0xe5, 0xe6, 0xe7, 0xe8, 0xe9, 0xea, 0xf2, 0xf3, 0xf4, 0xf5, 0xf6, 0xf7, 0xf8, 0xf9, 75 | 0xfa, 0xff, 0xda, 0x00, 0x0c, 0x03, 0x01, 0x00, 0x02, 0x11, 0x03, 0x11, 0x00, 0x3f, 0x00, 0xf9, 0xfe, 0x8a, 0x28, 76 | 0xa0, 0x02, 0x8a, 0x28, 0xa0, 0x02, 0x8a, 0x28, 0xa0, 0x02, 0x8a, 0x28, 0xa0, 0x02, 0x8a, 0x28, 0xa0, 0x02, 0x8a, 77 | 0x28, 0xa0, 0x02, 0x8a, 0x28, 0xa0, 0x02, 0x8a, 0x28, 0xa0, 0x02, 0x8a, 0x28, 0xa0, 0x02, 0x8a, 0x28, 0xa0, 0x02, 78 | 0x8a, 0x28, 0xa0, 0x02, 0x8a, 0x28, 0xa0, 0x02, 0x8a, 0x28, 0xa0, 0x02, 0x8a, 0x28, 0xa0, 0x02, 0x8a, 0x28, 0xa0, 79 | 0x02, 0x8a, 0x28, 0xa0, 0x02, 0x8a, 0x28, 0xa0, 0x02, 0x8a, 0x28, 0xa0, 0x02, 0x8a, 0x28, 0xa0, 0x02, 0x8a, 0x28, 80 | 0xa0, 0x02, 0x8a, 0x28, 0xa0, 0x02, 0x8a, 0x28, 0xa0, 0x02, 0x8a, 0x28, 0xa0, 0x02, 0x8a, 0x28, 0xa0, 0x02, 0x8a, 81 | 0x28, 0xa0, 0x02, 0x8a, 0x28, 0xa0, 0x02, 0x8a, 0x28, 0xa0, 0x02, 0x8a, 0x28, 0xa0, 0x02, 0x8a, 0x28, 0xa0, 0x02, 82 | 0x8a, 0x28, 0xa0, 0x02, 0x8a, 0x28, 0xa0, 0x02, 0x8a, 0x28, 0xa0, 0x02, 0x8a, 0x28, 0xa0, 0x02, 0x8a, 0x28, 0xa0, 83 | 0x02, 0x8a, 0x28, 0xa0, 0x02, 0x8a, 0x28, 0xa0, 0x0f, 0xff, 0xd9 84 | ] 85 | 86 | # 248 x 58 black JPEG 87 | BLANK_SCREEN_IMAGE = [ 88 | 0xff, 0xd8, 0xff, 0xe0, 0x00, 0x10, 0x4a, 0x46, 0x49, 0x46, 0x00, 0x01, 0x01, 0x00, 0x00, 0x01, 0x00, 0x01, 0x00, 89 | 0x00, 0xff, 0xdb, 0x00, 0x43, 0x00, 0x08, 0x06, 0x06, 0x07, 0x06, 0x05, 0x08, 0x07, 0x07, 0x07, 0x09, 0x09, 0x08, 90 | 0x0a, 0x0c, 0x14, 0x0d, 0x0c, 0x0b, 0x0b, 0x0c, 0x19, 0x12, 0x13, 0x0f, 0x14, 0x1d, 0x1a, 0x1f, 0x1e, 0x1d, 0x1a, 91 | 0x1c, 0x1c, 0x20, 0x24, 0x2e, 0x27, 0x20, 0x22, 0x2c, 0x23, 0x1c, 0x1c, 0x28, 0x37, 0x29, 0x2c, 0x30, 0x31, 0x34, 92 | 0x34, 0x34, 0x1f, 0x27, 0x39, 0x3d, 0x38, 0x32, 0x3c, 0x2e, 0x33, 0x34, 0x32, 0xff, 0xc0, 0x00, 0x0b, 0x08, 0x00, 93 | 0x3a, 0x00, 0xf8, 0x01, 0x01, 0x11, 0x00, 0xff, 0xc4, 0x00, 0x15, 0x00, 0x01, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 94 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x08, 0xff, 0xc4, 0x00, 0x14, 0x10, 0x01, 0x00, 0x00, 95 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xff, 0xda, 0x00, 0x08, 0x01, 96 | 0x01, 0x00, 0x00, 0x3f, 0x00, 0x9f, 0xc0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 97 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 98 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 99 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x7f, 0xff, 0xd9 100 | ] 101 | 102 | def _read_control_states(self): 103 | states = self.device.read(4 + self.KEY_COUNT + self.TOUCH_KEY_COUNT) 104 | if states is None: 105 | return None 106 | 107 | states = states[4:] 108 | return { 109 | ControlType.KEY: [bool(s) for s in states] 110 | } 111 | 112 | def _reset_key_stream(self): 113 | payload = bytearray(self.IMAGE_REPORT_LENGTH) 114 | payload[0] = 0x02 115 | self.device.write(payload) 116 | 117 | def reset(self): 118 | payload = bytearray(32) 119 | payload[0:2] = [0x03, 0x02] 120 | self.device.write_feature(payload) 121 | 122 | def set_brightness(self, percent): 123 | if isinstance(percent, float): 124 | percent = int(100.0 * percent) 125 | 126 | percent = min(max(percent, 0), 100) 127 | 128 | payload = bytearray(32) 129 | payload[0:2] = [0x03, 0x08, percent] 130 | self.device.write_feature(payload) 131 | 132 | def get_serial_number(self): 133 | serial = self.device.read_feature(0x06, 32) 134 | return self._extract_string(serial[2:]) 135 | 136 | def get_firmware_version(self): 137 | version = self.device.read_feature(0x05, 32) 138 | return self._extract_string(version[6:]) 139 | 140 | def set_key_image(self, key, image): 141 | if min(max(key, 0), self.KEY_COUNT - 1) != key: 142 | raise IndexError("Invalid key index {}.".format(key)) 143 | 144 | image = bytes(image or self.BLANK_KEY_IMAGE) 145 | 146 | page_number = 0 147 | bytes_remaining = len(image) 148 | while bytes_remaining > 0: 149 | this_length = min(bytes_remaining, self.IMAGE_REPORT_PAYLOAD_LENGTH) 150 | bytes_sent = page_number * self.IMAGE_REPORT_PAYLOAD_LENGTH 151 | 152 | header = [ 153 | 0x02, 154 | 0x07, 155 | key, 156 | 1 if this_length == bytes_remaining else 0, 157 | this_length & 0xFF, 158 | this_length >> 8, 159 | page_number & 0xFF, 160 | page_number >> 8 161 | ] 162 | 163 | payload = bytes(header) + image[bytes_sent:bytes_sent + this_length] 164 | padding = bytearray(self.IMAGE_REPORT_LENGTH - len(payload)) 165 | self.device.write(payload + padding) 166 | 167 | bytes_remaining = bytes_remaining - this_length 168 | page_number = page_number + 1 169 | 170 | def set_key_color(self, key, r, g, b): 171 | if min(max(key, 0), self.KEY_COUNT + self.TOUCH_KEY_COUNT - 1) != key: 172 | raise IndexError("Invalid touch key index {}.".format(key)) 173 | 174 | if r > 255 or r < 0 or g > 255 or g < 0 or b > 255 or b < 0: 175 | raise ValueError("Invalid color") 176 | 177 | payload = bytearray(32) 178 | payload[0:6] = [0x03, 0x06, key, r, g, b] 179 | self.device.write_feature(payload) 180 | 181 | def set_screen_image(self, image): 182 | if not image: 183 | image = self.BLANK_SCREEN_IMAGE 184 | 185 | image = bytes(image) 186 | 187 | page_number = 0 188 | bytes_remaining = len(image) 189 | while bytes_remaining > 0: 190 | this_length = min(bytes_remaining, self.IMAGE_REPORT_PAYLOAD_LENGTH) 191 | bytes_sent = page_number * self.IMAGE_REPORT_PAYLOAD_LENGTH 192 | 193 | header = [ 194 | 0x02, # 0 195 | 0x0b, # 1 196 | 0x00, # 2 197 | 0x01 if this_length == bytes_remaining else 0x00, # 3 is the last report? 198 | this_length & 0xff, # 5 bytecount high byte 199 | (this_length >> 8) & 0xff, # 4 bytecount high byte 200 | page_number & 0xff, # 7 pagenumber low byte 201 | (page_number >> 8) & 0xff # 6 pagenumber high byte 202 | ] 203 | 204 | payload = bytes(header) + image[bytes_sent:bytes_sent + this_length] 205 | padding = bytearray(self.IMAGE_REPORT_LENGTH - len(payload)) 206 | self.device.write(payload + padding) 207 | 208 | bytes_remaining = bytes_remaining - this_length 209 | page_number = page_number + 1 210 | 211 | def set_touchscreen_image(self, image, x_pos=0, y_pos=0, width=0, height=0): 212 | pass 213 | -------------------------------------------------------------------------------- /src/StreamDeck/Devices/StreamDeckOriginal.py: -------------------------------------------------------------------------------- 1 | # Python Stream Deck Library 2 | # Released under the MIT license 3 | # 4 | # dean [at] fourwalledcubicle [dot] com 5 | # www.fourwalledcubicle.com 6 | # 7 | 8 | from .StreamDeck import StreamDeck, ControlType 9 | 10 | 11 | class StreamDeckOriginal(StreamDeck): 12 | """ 13 | Represents a physically attached StreamDeck Original device. 14 | """ 15 | 16 | KEY_COUNT = 15 17 | KEY_COLS = 5 18 | KEY_ROWS = 3 19 | 20 | KEY_PIXEL_WIDTH = 72 21 | KEY_PIXEL_HEIGHT = 72 22 | KEY_IMAGE_FORMAT = "BMP" 23 | KEY_FLIP = (True, True) 24 | KEY_ROTATION = 0 25 | 26 | DECK_TYPE = "Stream Deck Original" 27 | DECK_VISUAL = True 28 | 29 | IMAGE_REPORT_LENGTH = 8191 30 | IMAGE_REPORT_HEADER_LENGTH = 16 31 | 32 | # 72 x 72 black BMP 33 | BLANK_KEY_IMAGE = [ 34 | 0x42, 0x4d, 0xf6, 0x3c, 0x00, 0x00, 0x00, 0x00, 35 | 0x00, 0x00, 0x36, 0x00, 0x00, 0x00, 0x28, 0x00, 36 | 0x00, 0x00, 0x48, 0x00, 0x00, 0x00, 0x48, 0x00, 37 | 0x00, 0x00, 0x01, 0x00, 0x18, 0x00, 0x00, 0x00, 38 | 0x00, 0x00, 0xc0, 0x3c, 0x00, 0x00, 0xc4, 0x0e, 39 | 0x00, 0x00, 0xc4, 0x0e, 0x00, 0x00, 0x00, 0x00, 40 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 41 | ] + [0] * (KEY_PIXEL_WIDTH * KEY_PIXEL_HEIGHT * 3) 42 | 43 | def _convert_key_id_origin(self, key): 44 | key_col = key % self.KEY_COLS 45 | return (key - key_col) + ((self.KEY_COLS - 1) - key_col) 46 | 47 | def _read_control_states(self): 48 | states = self.device.read(1 + self.KEY_COUNT) 49 | if states is None: 50 | return None 51 | 52 | states = states[1:] 53 | return { 54 | ControlType.KEY: [bool(states[s]) for s in map(self._convert_key_id_origin, range(self.KEY_COUNT))] 55 | } 56 | 57 | def _reset_key_stream(self): 58 | payload = bytearray(self.IMAGE_REPORT_LENGTH) 59 | payload[0] = 0x02 60 | self.device.write(payload) 61 | 62 | def reset(self): 63 | payload = bytearray(17) 64 | payload[0:2] = [0x0B, 0x63] 65 | self.device.write_feature(payload) 66 | 67 | def set_brightness(self, percent): 68 | if isinstance(percent, float): 69 | percent = int(100.0 * percent) 70 | 71 | percent = min(max(percent, 0), 100) 72 | 73 | payload = bytearray(17) 74 | payload[0:6] = [0x05, 0x55, 0xaa, 0xd1, 0x01, percent] 75 | self.device.write_feature(payload) 76 | 77 | def get_serial_number(self): 78 | serial = self.device.read_feature(0x03, 17) 79 | return self._extract_string(serial[5:]) 80 | 81 | def get_firmware_version(self): 82 | version = self.device.read_feature(0x04, 17) 83 | return self._extract_string(version[5:]) 84 | 85 | def set_key_image(self, key, image): 86 | if min(max(key, 0), self.KEY_COUNT) != key: 87 | raise IndexError("Invalid key index {}.".format(key)) 88 | 89 | image = bytes(image or self.BLANK_KEY_IMAGE) 90 | image_report_payload_length = len(image) // 2 91 | 92 | key = self._convert_key_id_origin(key) 93 | 94 | page_number = 0 95 | bytes_remaining = len(image) 96 | while bytes_remaining > 0: 97 | this_length = min(bytes_remaining, image_report_payload_length) 98 | bytes_sent = page_number * image_report_payload_length 99 | 100 | header = [ 101 | 0x02, 102 | 0x01, 103 | page_number + 1, 104 | 0, 105 | 1 if this_length == bytes_remaining else 0, 106 | key + 1, 107 | 0, 108 | 0, 109 | 0, 110 | 0, 111 | 0, 112 | 0, 113 | 0, 114 | 0, 115 | 0, 116 | 0, 117 | ] 118 | 119 | payload = bytes(header) + image[bytes_sent:bytes_sent + this_length] 120 | padding = bytearray(self.IMAGE_REPORT_LENGTH - len(payload)) 121 | self.device.write(payload + padding) 122 | 123 | bytes_remaining = bytes_remaining - this_length 124 | page_number = page_number + 1 125 | 126 | def set_touchscreen_image(self, image, x_pos=0, y_pos=0, width=0, height=0): 127 | pass 128 | 129 | def set_key_color(self, key, r, g, b): 130 | pass 131 | 132 | def set_screen_image(self, image): 133 | pass 134 | -------------------------------------------------------------------------------- /src/StreamDeck/Devices/StreamDeckOriginalV2.py: -------------------------------------------------------------------------------- 1 | # Python Stream Deck Library 2 | # Released under the MIT license 3 | # 4 | # dean [at] fourwalledcubicle [dot] com 5 | # www.fourwalledcubicle.com 6 | # 7 | 8 | from .StreamDeck import StreamDeck, ControlType 9 | 10 | 11 | class StreamDeckOriginalV2(StreamDeck): 12 | """ 13 | Represents a physically attached StreamDeck Original (V2) device. 14 | """ 15 | 16 | KEY_COUNT = 15 17 | KEY_COLS = 5 18 | KEY_ROWS = 3 19 | 20 | KEY_PIXEL_WIDTH = 72 21 | KEY_PIXEL_HEIGHT = 72 22 | KEY_IMAGE_FORMAT = "JPEG" 23 | KEY_FLIP = (True, True) 24 | KEY_ROTATION = 0 25 | 26 | DECK_TYPE = "Stream Deck Original" 27 | DECK_VISUAL = True 28 | 29 | IMAGE_REPORT_LENGTH = 1024 30 | IMAGE_REPORT_HEADER_LENGTH = 8 31 | IMAGE_REPORT_PAYLOAD_LENGTH = IMAGE_REPORT_LENGTH - IMAGE_REPORT_HEADER_LENGTH 32 | 33 | # 72 x 72 black JPEG 34 | BLANK_KEY_IMAGE = [ 35 | 0xff, 0xd8, 0xff, 0xe0, 0x00, 0x10, 0x4a, 0x46, 0x49, 0x46, 0x00, 0x01, 0x01, 0x00, 0x00, 0x01, 0x00, 0x01, 0x00, 36 | 0x00, 0xff, 0xdb, 0x00, 0x43, 0x00, 0x08, 0x06, 0x06, 0x07, 0x06, 0x05, 0x08, 0x07, 0x07, 0x07, 0x09, 0x09, 0x08, 37 | 0x0a, 0x0c, 0x14, 0x0d, 0x0c, 0x0b, 0x0b, 0x0c, 0x19, 0x12, 0x13, 0x0f, 0x14, 0x1d, 0x1a, 0x1f, 0x1e, 0x1d, 0x1a, 38 | 0x1c, 0x1c, 0x20, 0x24, 0x2e, 0x27, 0x20, 0x22, 0x2c, 0x23, 0x1c, 0x1c, 0x28, 0x37, 0x29, 0x2c, 0x30, 0x31, 0x34, 39 | 0x34, 0x34, 0x1f, 0x27, 0x39, 0x3d, 0x38, 0x32, 0x3c, 0x2e, 0x33, 0x34, 0x32, 0xff, 0xdb, 0x00, 0x43, 0x01, 0x09, 40 | 0x09, 0x09, 0x0c, 0x0b, 0x0c, 0x18, 0x0d, 0x0d, 0x18, 0x32, 0x21, 0x1c, 0x21, 0x32, 0x32, 0x32, 0x32, 0x32, 0x32, 41 | 0x32, 0x32, 0x32, 0x32, 0x32, 0x32, 0x32, 0x32, 0x32, 0x32, 0x32, 0x32, 0x32, 0x32, 0x32, 0x32, 0x32, 0x32, 0x32, 42 | 0x32, 0x32, 0x32, 0x32, 0x32, 0x32, 0x32, 0x32, 0x32, 0x32, 0x32, 0x32, 0x32, 0x32, 0x32, 0x32, 0x32, 0x32, 0x32, 43 | 0x32, 0x32, 0x32, 0x32, 0x32, 0x32, 0xff, 0xc0, 0x00, 0x11, 0x08, 0x00, 0x48, 0x00, 0x48, 0x03, 0x01, 0x22, 0x00, 44 | 0x02, 0x11, 0x01, 0x03, 0x11, 0x01, 0xff, 0xc4, 0x00, 0x1f, 0x00, 0x00, 0x01, 0x05, 0x01, 0x01, 0x01, 0x01, 0x01, 45 | 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 46 | 0x0b, 0xff, 0xc4, 0x00, 0xb5, 0x10, 0x00, 0x02, 0x01, 0x03, 0x03, 0x02, 0x04, 0x03, 0x05, 0x05, 0x04, 0x04, 0x00, 47 | 0x00, 0x01, 0x7d, 0x01, 0x02, 0x03, 0x00, 0x04, 0x11, 0x05, 0x12, 0x21, 0x31, 0x41, 0x06, 0x13, 0x51, 0x61, 0x07, 48 | 0x22, 0x71, 0x14, 0x32, 0x81, 0x91, 0xa1, 0x08, 0x23, 0x42, 0xb1, 0xc1, 0x15, 0x52, 0xd1, 0xf0, 0x24, 0x33, 0x62, 49 | 0x72, 0x82, 0x09, 0x0a, 0x16, 0x17, 0x18, 0x19, 0x1a, 0x25, 0x26, 0x27, 0x28, 0x29, 0x2a, 0x34, 0x35, 0x36, 0x37, 50 | 0x38, 0x39, 0x3a, 0x43, 0x44, 0x45, 0x46, 0x47, 0x48, 0x49, 0x4a, 0x53, 0x54, 0x55, 0x56, 0x57, 0x58, 0x59, 0x5a, 51 | 0x63, 0x64, 0x65, 0x66, 0x67, 0x68, 0x69, 0x6a, 0x73, 0x74, 0x75, 0x76, 0x77, 0x78, 0x79, 0x7a, 0x83, 0x84, 0x85, 52 | 0x86, 0x87, 0x88, 0x89, 0x8a, 0x92, 0x93, 0x94, 0x95, 0x96, 0x97, 0x98, 0x99, 0x9a, 0xa2, 0xa3, 0xa4, 0xa5, 0xa6, 53 | 0xa7, 0xa8, 0xa9, 0xaa, 0xb2, 0xb3, 0xb4, 0xb5, 0xb6, 0xb7, 0xb8, 0xb9, 0xba, 0xc2, 0xc3, 0xc4, 0xc5, 0xc6, 0xc7, 54 | 0xc8, 0xc9, 0xca, 0xd2, 0xd3, 0xd4, 0xd5, 0xd6, 0xd7, 0xd8, 0xd9, 0xda, 0xe1, 0xe2, 0xe3, 0xe4, 0xe5, 0xe6, 0xe7, 55 | 0xe8, 0xe9, 0xea, 0xf1, 0xf2, 0xf3, 0xf4, 0xf5, 0xf6, 0xf7, 0xf8, 0xf9, 0xfa, 0xff, 0xc4, 0x00, 0x1f, 0x01, 0x00, 56 | 0x03, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x02, 0x03, 57 | 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0xff, 0xc4, 0x00, 0xb5, 0x11, 0x00, 0x02, 0x01, 0x02, 0x04, 0x04, 58 | 0x03, 0x04, 0x07, 0x05, 0x04, 0x04, 0x00, 0x01, 0x02, 0x77, 0x00, 0x01, 0x02, 0x03, 0x11, 0x04, 0x05, 0x21, 0x31, 59 | 0x06, 0x12, 0x41, 0x51, 0x07, 0x61, 0x71, 0x13, 0x22, 0x32, 0x81, 0x08, 0x14, 0x42, 0x91, 0xa1, 0xb1, 0xc1, 0x09, 60 | 0x23, 0x33, 0x52, 0xf0, 0x15, 0x62, 0x72, 0xd1, 0x0a, 0x16, 0x24, 0x34, 0xe1, 0x25, 0xf1, 0x17, 0x18, 0x19, 0x1a, 61 | 0x26, 0x27, 0x28, 0x29, 0x2a, 0x35, 0x36, 0x37, 0x38, 0x39, 0x3a, 0x43, 0x44, 0x45, 0x46, 0x47, 0x48, 0x49, 0x4a, 62 | 0x53, 0x54, 0x55, 0x56, 0x57, 0x58, 0x59, 0x5a, 0x63, 0x64, 0x65, 0x66, 0x67, 0x68, 0x69, 0x6a, 0x73, 0x74, 0x75, 63 | 0x76, 0x77, 0x78, 0x79, 0x7a, 0x82, 0x83, 0x84, 0x85, 0x86, 0x87, 0x88, 0x89, 0x8a, 0x92, 0x93, 0x94, 0x95, 0x96, 64 | 0x97, 0x98, 0x99, 0x9a, 0xa2, 0xa3, 0xa4, 0xa5, 0xa6, 0xa7, 0xa8, 0xa9, 0xaa, 0xb2, 0xb3, 0xb4, 0xb5, 0xb6, 0xb7, 65 | 0xb8, 0xb9, 0xba, 0xc2, 0xc3, 0xc4, 0xc5, 0xc6, 0xc7, 0xc8, 0xc9, 0xca, 0xd2, 0xd3, 0xd4, 0xd5, 0xd6, 0xd7, 0xd8, 66 | 0xd9, 0xda, 0xe2, 0xe3, 0xe4, 0xe5, 0xe6, 0xe7, 0xe8, 0xe9, 0xea, 0xf2, 0xf3, 0xf4, 0xf5, 0xf6, 0xf7, 0xf8, 0xf9, 67 | 0xfa, 0xff, 0xda, 0x00, 0x0c, 0x03, 0x01, 0x00, 0x02, 0x11, 0x03, 0x11, 0x00, 0x3f, 0x00, 0xf9, 0xfe, 0x8a, 0x28, 68 | 0xa0, 0x02, 0x8a, 0x28, 0xa0, 0x02, 0x8a, 0x28, 0xa0, 0x02, 0x8a, 0x28, 0xa0, 0x02, 0x8a, 0x28, 0xa0, 0x02, 0x8a, 69 | 0x28, 0xa0, 0x02, 0x8a, 0x28, 0xa0, 0x02, 0x8a, 0x28, 0xa0, 0x02, 0x8a, 0x28, 0xa0, 0x02, 0x8a, 0x28, 0xa0, 0x02, 70 | 0x8a, 0x28, 0xa0, 0x02, 0x8a, 0x28, 0xa0, 0x02, 0x8a, 0x28, 0xa0, 0x02, 0x8a, 0x28, 0xa0, 0x02, 0x8a, 0x28, 0xa0, 71 | 0x02, 0x8a, 0x28, 0xa0, 0x02, 0x8a, 0x28, 0xa0, 0x02, 0x8a, 0x28, 0xa0, 0x02, 0x8a, 0x28, 0xa0, 0x02, 0x8a, 0x28, 72 | 0xa0, 0x02, 0x8a, 0x28, 0xa0, 0x02, 0x8a, 0x28, 0xa0, 0x02, 0x8a, 0x28, 0xa0, 0x02, 0x8a, 0x28, 0xa0, 0x02, 0x8a, 73 | 0x28, 0xa0, 0x0f, 0xff, 0xd9 74 | ] 75 | 76 | def _read_control_states(self): 77 | states = self.device.read(4 + self.KEY_COUNT) 78 | if states is None: 79 | return None 80 | 81 | states = states[4:] 82 | return { 83 | ControlType.KEY: [bool(s) for s in states] 84 | } 85 | 86 | def _reset_key_stream(self): 87 | payload = bytearray(self.IMAGE_REPORT_LENGTH) 88 | payload[0] = 0x02 89 | self.device.write(payload) 90 | 91 | def reset(self): 92 | payload = bytearray(32) 93 | payload[0:2] = [0x03, 0x02] 94 | self.device.write_feature(payload) 95 | 96 | def set_brightness(self, percent): 97 | if isinstance(percent, float): 98 | percent = int(100.0 * percent) 99 | 100 | percent = min(max(percent, 0), 100) 101 | 102 | payload = bytearray(32) 103 | payload[0:2] = [0x03, 0x08, percent] 104 | self.device.write_feature(payload) 105 | 106 | def get_serial_number(self): 107 | serial = self.device.read_feature(0x06, 32) 108 | return self._extract_string(serial[2:]) 109 | 110 | def get_firmware_version(self): 111 | version = self.device.read_feature(0x05, 32) 112 | return self._extract_string(version[6:]) 113 | 114 | def set_key_image(self, key, image): 115 | if min(max(key, 0), self.KEY_COUNT) != key: 116 | raise IndexError("Invalid key index {}.".format(key)) 117 | 118 | image = bytes(image or self.BLANK_KEY_IMAGE) 119 | 120 | page_number = 0 121 | bytes_remaining = len(image) 122 | while bytes_remaining > 0: 123 | this_length = min(bytes_remaining, self.IMAGE_REPORT_PAYLOAD_LENGTH) 124 | bytes_sent = page_number * self.IMAGE_REPORT_PAYLOAD_LENGTH 125 | 126 | header = [ 127 | 0x02, 128 | 0x07, 129 | key, 130 | 1 if this_length == bytes_remaining else 0, 131 | this_length & 0xFF, 132 | this_length >> 8, 133 | page_number & 0xFF, 134 | page_number >> 8 135 | ] 136 | 137 | payload = bytes(header) + image[bytes_sent:bytes_sent + this_length] 138 | padding = bytearray(self.IMAGE_REPORT_LENGTH - len(payload)) 139 | self.device.write(payload + padding) 140 | 141 | bytes_remaining = bytes_remaining - this_length 142 | page_number = page_number + 1 143 | 144 | def set_touchscreen_image(self, image, x_pos=0, y_pos=0, width=0, height=0): 145 | pass 146 | 147 | def set_key_color(self, key, r, g, b): 148 | pass 149 | 150 | def set_screen_image(self, image): 151 | pass 152 | -------------------------------------------------------------------------------- /src/StreamDeck/Devices/StreamDeckPedal.py: -------------------------------------------------------------------------------- 1 | # Python Stream Deck Library 2 | # Released under the MIT license 3 | # 4 | # dean [at] fourwalledcubicle [dot] com 5 | # www.fourwalledcubicle.com 6 | # 7 | 8 | from .StreamDeck import StreamDeck, ControlType 9 | 10 | 11 | class StreamDeckPedal(StreamDeck): 12 | """ 13 | Represents a physically attached StreamDeck Pedal device. 14 | """ 15 | 16 | KEY_COUNT = 3 17 | KEY_COLS = 3 18 | KEY_ROWS = 1 19 | 20 | DECK_TYPE = "Stream Deck Pedal" 21 | DECK_VISUAL = False 22 | 23 | def _read_control_states(self): 24 | states = self.device.read(4 + self.KEY_COUNT) 25 | if states is None: 26 | return None 27 | 28 | states = states[4:] 29 | return { 30 | ControlType.KEY: [bool(s) for s in states] 31 | } 32 | 33 | def _reset_key_stream(self): 34 | pass 35 | 36 | def reset(self): 37 | pass 38 | 39 | def set_brightness(self, percent): 40 | pass 41 | 42 | def get_serial_number(self): 43 | serial = self.device.read_feature(0x06, 32) 44 | return self._extract_string(serial[2:]) 45 | 46 | def get_firmware_version(self): 47 | version = self.device.read_feature(0x05, 32) 48 | return self._extract_string(version[6:]) 49 | 50 | def set_key_image(self, key, image): 51 | pass 52 | 53 | def set_touchscreen_image(self, image, x_pos=0, y_pos=0, width=0, height=0): 54 | pass 55 | 56 | def set_key_color(self, key, r, g, b): 57 | pass 58 | 59 | def set_screen_image(self, image): 60 | pass 61 | -------------------------------------------------------------------------------- /src/StreamDeck/Devices/StreamDeckXL.py: -------------------------------------------------------------------------------- 1 | # Python Stream Deck Library 2 | # Released under the MIT license 3 | # 4 | # dean [at] fourwalledcubicle [dot] com 5 | # www.fourwalledcubicle.com 6 | # 7 | 8 | from .StreamDeck import StreamDeck, ControlType 9 | 10 | 11 | class StreamDeckXL(StreamDeck): 12 | """ 13 | Represents a physically attached StreamDeck XL device. 14 | """ 15 | 16 | KEY_COUNT = 32 17 | KEY_COLS = 8 18 | KEY_ROWS = 4 19 | 20 | KEY_PIXEL_WIDTH = 96 21 | KEY_PIXEL_HEIGHT = 96 22 | KEY_IMAGE_FORMAT = "JPEG" 23 | KEY_FLIP = (True, True) 24 | KEY_ROTATION = 0 25 | 26 | DECK_TYPE = "Stream Deck XL" 27 | DECK_VISUAL = True 28 | 29 | IMAGE_REPORT_LENGTH = 1024 30 | IMAGE_REPORT_HEADER_LENGTH = 8 31 | IMAGE_REPORT_PAYLOAD_LENGTH = IMAGE_REPORT_LENGTH - IMAGE_REPORT_HEADER_LENGTH 32 | 33 | # 96 x 96 black JPEG 34 | BLANK_KEY_IMAGE = [ 35 | 0xff, 0xd8, 0xff, 0xe0, 0x00, 0x10, 0x4a, 0x46, 0x49, 0x46, 0x00, 0x01, 0x01, 0x00, 0x00, 0x01, 0x00, 0x01, 0x00, 36 | 0x00, 0xff, 0xdb, 0x00, 0x43, 0x00, 0x08, 0x06, 0x06, 0x07, 0x06, 0x05, 0x08, 0x07, 0x07, 0x07, 0x09, 0x09, 0x08, 37 | 0x0a, 0x0c, 0x14, 0x0d, 0x0c, 0x0b, 0x0b, 0x0c, 0x19, 0x12, 0x13, 0x0f, 0x14, 0x1d, 0x1a, 0x1f, 0x1e, 0x1d, 0x1a, 38 | 0x1c, 0x1c, 0x20, 0x24, 0x2e, 0x27, 0x20, 0x22, 0x2c, 0x23, 0x1c, 0x1c, 0x28, 0x37, 0x29, 0x2c, 0x30, 0x31, 0x34, 39 | 0x34, 0x34, 0x1f, 0x27, 0x39, 0x3d, 0x38, 0x32, 0x3c, 0x2e, 0x33, 0x34, 0x32, 0xff, 0xdb, 0x00, 0x43, 0x01, 0x09, 40 | 0x09, 0x09, 0x0c, 0x0b, 0x0c, 0x18, 0x0d, 0x0d, 0x18, 0x32, 0x21, 0x1c, 0x21, 0x32, 0x32, 0x32, 0x32, 0x32, 0x32, 41 | 0x32, 0x32, 0x32, 0x32, 0x32, 0x32, 0x32, 0x32, 0x32, 0x32, 0x32, 0x32, 0x32, 0x32, 0x32, 0x32, 0x32, 0x32, 0x32, 42 | 0x32, 0x32, 0x32, 0x32, 0x32, 0x32, 0x32, 0x32, 0x32, 0x32, 0x32, 0x32, 0x32, 0x32, 0x32, 0x32, 0x32, 0x32, 0x32, 43 | 0x32, 0x32, 0x32, 0x32, 0x32, 0x32, 0xff, 0xc0, 0x00, 0x11, 0x08, 0x00, 0x60, 0x00, 0x60, 0x03, 0x01, 0x22, 0x00, 44 | 0x02, 0x11, 0x01, 0x03, 0x11, 0x01, 0xff, 0xc4, 0x00, 0x1f, 0x00, 0x00, 0x01, 0x05, 0x01, 0x01, 0x01, 0x01, 0x01, 45 | 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 46 | 0x0b, 0xff, 0xc4, 0x00, 0xb5, 0x10, 0x00, 0x02, 0x01, 0x03, 0x03, 0x02, 0x04, 0x03, 0x05, 0x05, 0x04, 0x04, 0x00, 47 | 0x00, 0x01, 0x7d, 0x01, 0x02, 0x03, 0x00, 0x04, 0x11, 0x05, 0x12, 0x21, 0x31, 0x41, 0x06, 0x13, 0x51, 0x61, 0x07, 48 | 0x22, 0x71, 0x14, 0x32, 0x81, 0x91, 0xa1, 0x08, 0x23, 0x42, 0xb1, 0xc1, 0x15, 0x52, 0xd1, 0xf0, 0x24, 0x33, 0x62, 49 | 0x72, 0x82, 0x09, 0x0a, 0x16, 0x17, 0x18, 0x19, 0x1a, 0x25, 0x26, 0x27, 0x28, 0x29, 0x2a, 0x34, 0x35, 0x36, 0x37, 50 | 0x38, 0x39, 0x3a, 0x43, 0x44, 0x45, 0x46, 0x47, 0x48, 0x49, 0x4a, 0x53, 0x54, 0x55, 0x56, 0x57, 0x58, 0x59, 0x5a, 51 | 0x63, 0x64, 0x65, 0x66, 0x67, 0x68, 0x69, 0x6a, 0x73, 0x74, 0x75, 0x76, 0x77, 0x78, 0x79, 0x7a, 0x83, 0x84, 0x85, 52 | 0x86, 0x87, 0x88, 0x89, 0x8a, 0x92, 0x93, 0x94, 0x95, 0x96, 0x97, 0x98, 0x99, 0x9a, 0xa2, 0xa3, 0xa4, 0xa5, 0xa6, 53 | 0xa7, 0xa8, 0xa9, 0xaa, 0xb2, 0xb3, 0xb4, 0xb5, 0xb6, 0xb7, 0xb8, 0xb9, 0xba, 0xc2, 0xc3, 0xc4, 0xc5, 0xc6, 0xc7, 54 | 0xc8, 0xc9, 0xca, 0xd2, 0xd3, 0xd4, 0xd5, 0xd6, 0xd7, 0xd8, 0xd9, 0xda, 0xe1, 0xe2, 0xe3, 0xe4, 0xe5, 0xe6, 0xe7, 55 | 0xe8, 0xe9, 0xea, 0xf1, 0xf2, 0xf3, 0xf4, 0xf5, 0xf6, 0xf7, 0xf8, 0xf9, 0xfa, 0xff, 0xc4, 0x00, 0x1f, 0x01, 0x00, 56 | 0x03, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x02, 0x03, 57 | 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0xff, 0xc4, 0x00, 0xb5, 0x11, 0x00, 0x02, 0x01, 0x02, 0x04, 0x04, 58 | 0x03, 0x04, 0x07, 0x05, 0x04, 0x04, 0x00, 0x01, 0x02, 0x77, 0x00, 0x01, 0x02, 0x03, 0x11, 0x04, 0x05, 0x21, 0x31, 59 | 0x06, 0x12, 0x41, 0x51, 0x07, 0x61, 0x71, 0x13, 0x22, 0x32, 0x81, 0x08, 0x14, 0x42, 0x91, 0xa1, 0xb1, 0xc1, 0x09, 60 | 0x23, 0x33, 0x52, 0xf0, 0x15, 0x62, 0x72, 0xd1, 0x0a, 0x16, 0x24, 0x34, 0xe1, 0x25, 0xf1, 0x17, 0x18, 0x19, 0x1a, 61 | 0x26, 0x27, 0x28, 0x29, 0x2a, 0x35, 0x36, 0x37, 0x38, 0x39, 0x3a, 0x43, 0x44, 0x45, 0x46, 0x47, 0x48, 0x49, 0x4a, 62 | 0x53, 0x54, 0x55, 0x56, 0x57, 0x58, 0x59, 0x5a, 0x63, 0x64, 0x65, 0x66, 0x67, 0x68, 0x69, 0x6a, 0x73, 0x74, 0x75, 63 | 0x76, 0x77, 0x78, 0x79, 0x7a, 0x82, 0x83, 0x84, 0x85, 0x86, 0x87, 0x88, 0x89, 0x8a, 0x92, 0x93, 0x94, 0x95, 0x96, 64 | 0x97, 0x98, 0x99, 0x9a, 0xa2, 0xa3, 0xa4, 0xa5, 0xa6, 0xa7, 0xa8, 0xa9, 0xaa, 0xb2, 0xb3, 0xb4, 0xb5, 0xb6, 0xb7, 65 | 0xb8, 0xb9, 0xba, 0xc2, 0xc3, 0xc4, 0xc5, 0xc6, 0xc7, 0xc8, 0xc9, 0xca, 0xd2, 0xd3, 0xd4, 0xd5, 0xd6, 0xd7, 0xd8, 66 | 0xd9, 0xda, 0xe2, 0xe3, 0xe4, 0xe5, 0xe6, 0xe7, 0xe8, 0xe9, 0xea, 0xf2, 0xf3, 0xf4, 0xf5, 0xf6, 0xf7, 0xf8, 0xf9, 67 | 0xfa, 0xff, 0xda, 0x00, 0x0c, 0x03, 0x01, 0x00, 0x02, 0x11, 0x03, 0x11, 0x00, 0x3f, 0x00, 0xf9, 0xfe, 0x8a, 0x28, 68 | 0xa0, 0x02, 0x8a, 0x28, 0xa0, 0x02, 0x8a, 0x28, 0xa0, 0x02, 0x8a, 0x28, 0xa0, 0x02, 0x8a, 0x28, 0xa0, 0x02, 0x8a, 69 | 0x28, 0xa0, 0x02, 0x8a, 0x28, 0xa0, 0x02, 0x8a, 0x28, 0xa0, 0x02, 0x8a, 0x28, 0xa0, 0x02, 0x8a, 0x28, 0xa0, 0x02, 70 | 0x8a, 0x28, 0xa0, 0x02, 0x8a, 0x28, 0xa0, 0x02, 0x8a, 0x28, 0xa0, 0x02, 0x8a, 0x28, 0xa0, 0x02, 0x8a, 0x28, 0xa0, 71 | 0x02, 0x8a, 0x28, 0xa0, 0x02, 0x8a, 0x28, 0xa0, 0x02, 0x8a, 0x28, 0xa0, 0x02, 0x8a, 0x28, 0xa0, 0x02, 0x8a, 0x28, 72 | 0xa0, 0x02, 0x8a, 0x28, 0xa0, 0x02, 0x8a, 0x28, 0xa0, 0x02, 0x8a, 0x28, 0xa0, 0x02, 0x8a, 0x28, 0xa0, 0x02, 0x8a, 73 | 0x28, 0xa0, 0x02, 0x8a, 0x28, 0xa0, 0x02, 0x8a, 0x28, 0xa0, 0x02, 0x8a, 0x28, 0xa0, 0x02, 0x8a, 0x28, 0xa0, 0x02, 74 | 0x8a, 0x28, 0xa0, 0x02, 0x8a, 0x28, 0xa0, 0x02, 0x8a, 0x28, 0xa0, 0x02, 0x8a, 0x28, 0xa0, 0x02, 0x8a, 0x28, 0xa0, 75 | 0x02, 0x8a, 0x28, 0xa0, 0x02, 0x8a, 0x28, 0xa0, 0x0f, 0xff, 0xd9 76 | ] 77 | 78 | def _read_control_states(self): 79 | states = self.device.read(4 + self.KEY_COUNT) 80 | if states is None: 81 | return None 82 | 83 | states = states[4:] 84 | return { 85 | ControlType.KEY: [bool(s) for s in states] 86 | } 87 | 88 | def _reset_key_stream(self): 89 | payload = bytearray(self.IMAGE_REPORT_LENGTH) 90 | payload[0] = 0x02 91 | self.device.write(payload) 92 | 93 | def reset(self): 94 | payload = bytearray(32) 95 | payload[0:2] = [0x03, 0x02] 96 | self.device.write_feature(payload) 97 | 98 | def set_brightness(self, percent): 99 | if isinstance(percent, float): 100 | percent = int(100.0 * percent) 101 | 102 | percent = min(max(percent, 0), 100) 103 | 104 | payload = bytearray(32) 105 | payload[0:2] = [0x03, 0x08, percent] 106 | self.device.write_feature(payload) 107 | 108 | def get_serial_number(self): 109 | serial = self.device.read_feature(0x06, 32) 110 | return self._extract_string(serial[2:]) 111 | 112 | def get_firmware_version(self): 113 | version = self.device.read_feature(0x05, 32) 114 | return self._extract_string(version[6:]) 115 | 116 | def set_key_image(self, key, image): 117 | if min(max(key, 0), self.KEY_COUNT) != key: 118 | raise IndexError("Invalid key index {}.".format(key)) 119 | 120 | image = bytes(image or self.BLANK_KEY_IMAGE) 121 | 122 | page_number = 0 123 | bytes_remaining = len(image) 124 | while bytes_remaining > 0: 125 | this_length = min(bytes_remaining, self.IMAGE_REPORT_PAYLOAD_LENGTH) 126 | bytes_sent = page_number * self.IMAGE_REPORT_PAYLOAD_LENGTH 127 | 128 | header = [ 129 | 0x02, 130 | 0x07, 131 | key, 132 | 1 if this_length == bytes_remaining else 0, 133 | this_length & 0xFF, 134 | this_length >> 8, 135 | page_number & 0xFF, 136 | page_number >> 8 137 | ] 138 | 139 | payload = bytes(header) + image[bytes_sent:bytes_sent + this_length] 140 | padding = bytearray(self.IMAGE_REPORT_LENGTH - len(payload)) 141 | self.device.write(payload + padding) 142 | 143 | bytes_remaining = bytes_remaining - this_length 144 | page_number = page_number + 1 145 | 146 | def set_touchscreen_image(self, image, x_pos=0, y_pos=0, width=0, height=0): 147 | pass 148 | 149 | def set_key_color(self, key, r, g, b): 150 | pass 151 | 152 | def set_screen_image(self, image): 153 | pass 154 | -------------------------------------------------------------------------------- /src/StreamDeck/Devices/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/abcminiuser/python-elgato-streamdeck/34dbf63a79d50834d554e955c8df3644eb0530be/src/StreamDeck/Devices/__init__.py -------------------------------------------------------------------------------- /src/StreamDeck/ImageHelpers/PILHelper.py: -------------------------------------------------------------------------------- 1 | # Python Stream Deck Library 2 | # Released under the MIT license 3 | # 4 | # dean [at] fourwalledcubicle [dot] com 5 | # www.fourwalledcubicle.com 6 | # 7 | 8 | import io 9 | from PIL import Image 10 | 11 | from ..Devices.StreamDeck import StreamDeck 12 | 13 | 14 | def _create_image(image_format, background): 15 | return Image.new("RGB", image_format['size'], background) 16 | 17 | 18 | def _scale_image(image, image_format, margins=(0, 0, 0, 0), background='black'): 19 | if len(margins) != 4: 20 | raise ValueError("Margins should be given as an array of four integers.") 21 | 22 | final_image = _create_image(image_format, background=background) 23 | 24 | thumbnail_max_width = final_image.width - (margins[1] + margins[3]) 25 | thumbnail_max_height = final_image.height - (margins[0] + margins[2]) 26 | 27 | thumbnail = image.convert("RGBA") 28 | thumbnail.thumbnail((thumbnail_max_width, thumbnail_max_height), Image.LANCZOS) 29 | 30 | thumbnail_x = (margins[3] + (thumbnail_max_width - thumbnail.width) // 2) 31 | thumbnail_y = (margins[0] + (thumbnail_max_height - thumbnail.height) // 2) 32 | 33 | final_image.paste(thumbnail, (thumbnail_x, thumbnail_y), thumbnail) 34 | 35 | return final_image 36 | 37 | 38 | def _to_native_format(image, image_format): 39 | if image.size != image_format['size']: 40 | image.thumbnail(image_format['size']) 41 | 42 | if image_format['rotation']: 43 | image = image.rotate(image_format['rotation']) 44 | 45 | if image_format['flip'][0]: 46 | image = image.transpose(Image.FLIP_LEFT_RIGHT) 47 | 48 | if image_format['flip'][1]: 49 | image = image.transpose(Image.FLIP_TOP_BOTTOM) 50 | 51 | # We want a compressed image in a given codec, convert. 52 | with io.BytesIO() as compressed_image: 53 | image.save(compressed_image, image_format['format'], quality=100) 54 | return compressed_image.getvalue() 55 | 56 | 57 | def create_image(deck: StreamDeck, background: str = 'black') -> Image.Image: 58 | """ 59 | .. deprecated:: 0.9.5 60 | Use :func:`~PILHelper.create_key_image` method instead. 61 | """ 62 | return create_key_image(deck, background) 63 | 64 | 65 | def create_key_image(deck: StreamDeck, background: str = 'black') -> Image.Image: 66 | """ 67 | Creates a new PIL Image with the correct image dimensions for the given 68 | StreamDeck device's keys. 69 | 70 | .. seealso:: See :func:`~PILHelper.to_native_key_format` method for converting a 71 | PIL image instance to the native key image format of a given 72 | StreamDeck device. 73 | 74 | :param StreamDeck deck: StreamDeck device to generate a compatible image for. 75 | :param str background: Background color to use, compatible with `PIL.Image.new()`. 76 | 77 | :rtype: PIL.Image 78 | :return: Created PIL image 79 | """ 80 | return _create_image(deck.key_image_format(), background) 81 | 82 | 83 | def create_touchscreen_image(deck: StreamDeck, background: str = 'black') -> Image.Image: 84 | """ 85 | Creates a new PIL Image with the correct image dimensions for the given 86 | StreamDeck device's touchscreen. 87 | 88 | .. seealso:: See :func:`~PILHelper.to_native_touchscreen_format` method for converting a 89 | PIL image instance to the native touchscreen image format of a given 90 | StreamDeck device. 91 | 92 | :param StreamDeck deck: StreamDeck device to generate a compatible image for. 93 | :param str background: Background color to use, compatible with `PIL.Image.new()`. 94 | 95 | :rtype: PIL.Image 96 | :return: Created PIL image 97 | """ 98 | return _create_image(deck.touchscreen_image_format(), background) 99 | 100 | 101 | def create_screen_image(deck: StreamDeck, background: str = 'black') -> Image.Image: 102 | """ 103 | Creates a new PIL Image with the correct image dimensions for the given 104 | StreamDeck device's screen. 105 | 106 | .. seealso:: See :func:`~PILHelper.to_native_screen_format` method for converting a 107 | PIL image instance to the native screen image format of a given 108 | StreamDeck device. 109 | 110 | :param StreamDeck deck: StreamDeck device to generate a compatible image for. 111 | :param str background: Background color to use, compatible with `PIL.Image.new()`. 112 | 113 | :rtype: PIL.Image 114 | :return: Created PIL image 115 | """ 116 | return _create_image(deck.screen_image_format(), background) 117 | 118 | 119 | def create_scaled_image(deck: StreamDeck, image: Image.Image, margins: tuple[int, int, int, int] = (0, 0, 0, 0), background: str = 'black') -> Image.Image: 120 | """ 121 | .. deprecated:: 0.9.5 122 | Use :func:`~PILHelper.create_scaled_key_image` method instead. 123 | """ 124 | return create_scaled_key_image(deck, image, margins, background) 125 | 126 | 127 | def create_scaled_key_image(deck: StreamDeck, image: Image.Image, margins: tuple[int, int, int, int] = (0, 0, 0, 0), background: str = 'black') -> Image.Image: 128 | """ 129 | Creates a new key image that contains a scaled version of a given image, 130 | resized to best fit the given StreamDeck device's keys with the given 131 | margins around each side. 132 | 133 | The scaled image is centered within the new key image, offset by the given 134 | margins. The aspect ratio of the image is preserved. 135 | 136 | .. seealso:: See :func:`~PILHelper.to_native_key_format` method for converting a 137 | PIL image instance to the native key image format of a given 138 | StreamDeck device. 139 | 140 | :param StreamDeck deck: StreamDeck device to generate a compatible image for. 141 | :param Image image: PIL Image object to scale 142 | :param list(int): Array of margin pixels in (top, right, bottom, left) order. 143 | :param str background: Background color to use, compatible with `PIL.Image.new()`. 144 | 145 | :rtrype: PIL.Image 146 | :return: Loaded PIL image scaled and centered 147 | """ 148 | return _scale_image(image, deck.key_image_format(), margins, background) 149 | 150 | 151 | def create_scaled_touchscreen_image(deck: StreamDeck, image: Image.Image, margins: tuple[int, int, int, int] = (0, 0, 0, 0), background: str = 'black') -> Image.Image: 152 | """ 153 | Creates a new touchscreen image that contains a scaled version of a given image, 154 | resized to best fit the given StreamDeck device's touchscreen with the given 155 | margins around each side. 156 | 157 | The scaled image is centered within the new touchscreen image, offset by the given 158 | margins. The aspect ratio of the image is preserved. 159 | 160 | .. seealso:: See :func:`~PILHelper.to_native_touchscreen_format` method for converting a 161 | PIL image instance to the native key image format of a given 162 | StreamDeck device. 163 | 164 | :param StreamDeck deck: StreamDeck device to generate a compatible image for. 165 | :param Image image: PIL Image object to scale 166 | :param list(int): Array of margin pixels in (top, right, bottom, left) order. 167 | :param str background: Background color to use, compatible with `PIL.Image.new()`. 168 | 169 | :rtrype: PIL.Image 170 | :return: Loaded PIL image scaled and centered 171 | """ 172 | return _scale_image(image, deck.touchscreen_image_format(), margins, background) 173 | 174 | 175 | def create_scaled_screen_image(deck: StreamDeck, image: Image.Image, margins: tuple[int, int, int, int] = (0, 0, 0, 0), background: str = 'black') -> Image.Image: 176 | """ 177 | Creates a new screen image that contains a scaled version of a given image, 178 | resized to best fit the given StreamDeck device's screen with the given 179 | margins around each side. 180 | 181 | The scaled image is centered within the new screen image, offset by the given 182 | margins. The aspect ratio of the image is preserved. 183 | 184 | .. seealso:: See :func:`~PILHelper.to_native_screen_format` method for converting a 185 | PIL image instance to the native key image format of a given 186 | StreamDeck device. 187 | 188 | :param StreamDeck deck: StreamDeck device to generate a compatible image for. 189 | :param Image image: PIL Image object to scale 190 | :param list(int): Array of margin pixels in (top, right, bottom, left) order. 191 | :param str background: Background color to use, compatible with `PIL.Image.new()`. 192 | 193 | :rtrype: PIL.Image 194 | :return: Loaded PIL image scaled and centered 195 | """ 196 | return _scale_image(image, deck.screen_image_format(), margins, background) 197 | 198 | 199 | def to_native_format(deck: StreamDeck, image: Image.Image) -> bytes: 200 | """ 201 | .. deprecated:: 0.9.5 202 | Use :func:`~PILHelper.to_native_key_format` method instead. 203 | """ 204 | return to_native_key_format(deck, image) 205 | 206 | 207 | def to_native_key_format(deck: StreamDeck, image: Image.Image) -> bytes: 208 | """ 209 | Converts a given PIL image to the native key image format for a StreamDeck, 210 | suitable for passing to :func:`~StreamDeck.set_key_image`. 211 | 212 | .. seealso:: See :func:`~PILHelper.create_image` method for creating a PIL 213 | image instance for a given StreamDeck device. 214 | 215 | :param StreamDeck deck: StreamDeck device to generate a compatible native image for. 216 | :param PIL.Image image: PIL Image to convert to the native StreamDeck image format 217 | 218 | :rtype: enumerable() 219 | :return: Image converted to the given StreamDeck's native format 220 | """ 221 | return _to_native_format(image, deck.key_image_format()) 222 | 223 | 224 | def to_native_touchscreen_format(deck: StreamDeck, image: Image.Image) -> bytes: 225 | """ 226 | Converts a given PIL image to the native touchscreen image format for a StreamDeck, 227 | suitable for passing to :func:`~StreamDeck.set_touchscreen_image`. 228 | 229 | .. seealso:: See :func:`~PILHelper.create_touchscreen_image` method for creating a PIL 230 | image instance for a given StreamDeck device. 231 | 232 | :param StreamDeck deck: StreamDeck device to generate a compatible native image for. 233 | :param PIL.Image image: PIL Image to convert to the native StreamDeck image format 234 | 235 | :rtype: enumerable() 236 | :return: Image converted to the given StreamDeck's native touchscreen format 237 | """ 238 | return _to_native_format(image, deck.touchscreen_image_format()) 239 | 240 | 241 | def to_native_screen_format(deck: StreamDeck, image: Image.Image) -> bytes: 242 | """ 243 | Converts a given PIL image to the native screen image format for a StreamDeck, 244 | suitable for passing to :func:`~StreamDeck.set_screen_image`. 245 | 246 | .. seealso:: See :func:`~PILHelper.create_screen_image` method for creating a PIL 247 | image instance for a given StreamDeck device. 248 | 249 | :param StreamDeck deck: StreamDeck device to generate a compatible native image for. 250 | :param PIL.Image image: PIL Image to convert to the native StreamDeck image format 251 | 252 | :rtype: enumerable() 253 | :return: Image converted to the given StreamDeck's native screen format 254 | """ 255 | return _to_native_format(image, deck.screen_image_format()) 256 | -------------------------------------------------------------------------------- /src/StreamDeck/ImageHelpers/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/abcminiuser/python-elgato-streamdeck/34dbf63a79d50834d554e955c8df3644eb0530be/src/StreamDeck/ImageHelpers/__init__.py -------------------------------------------------------------------------------- /src/StreamDeck/ProductIDs.py: -------------------------------------------------------------------------------- 1 | # Python Stream Deck Library 2 | # Released under the MIT license 3 | # 4 | # dean [at] fourwalledcubicle [dot] com 5 | # www.fourwalledcubicle.com 6 | # 7 | 8 | 9 | class USBVendorIDs: 10 | """ 11 | USB Vendor IDs for known StreamDeck devices. 12 | """ 13 | 14 | USB_VID_ELGATO = 0x0fd9 15 | 16 | 17 | class USBProductIDs: 18 | """ 19 | USB Product IDs for known StreamDeck devices. 20 | """ 21 | 22 | USB_PID_STREAMDECK_ORIGINAL = 0x0060 23 | USB_PID_STREAMDECK_ORIGINAL_V2 = 0x006d 24 | USB_PID_STREAMDECK_MINI = 0x0063 25 | USB_PID_STREAMDECK_NEO = 0x009a 26 | USB_PID_STREAMDECK_XL = 0x006c 27 | USB_PID_STREAMDECK_XL_V2 = 0x008f 28 | USB_PID_STREAMDECK_MK2 = 0x0080 29 | USB_PID_STREAMDECK_PEDAL = 0x0086 30 | USB_PID_STREAMDECK_MINI_MK2 = 0x0090 31 | USB_PID_STREAMDECK_PLUS = 0x0084 32 | -------------------------------------------------------------------------------- /src/StreamDeck/Transport/Dummy.py: -------------------------------------------------------------------------------- 1 | # Python Stream Deck Library 2 | # Released under the MIT license 3 | # 4 | # dean [at] fourwalledcubicle [dot] com 5 | # www.fourwalledcubicle.com 6 | # 7 | 8 | import binascii 9 | import logging 10 | 11 | from .Transport import Transport, TransportError 12 | 13 | 14 | class Dummy(Transport): 15 | """ 16 | Dummy transport layer, for testing. 17 | """ 18 | 19 | class Device(Transport.Device): 20 | def __init__(self, vid, pid): 21 | self.vid = vid 22 | self.pid = pid 23 | self.id = "{}:{}".format(vid, pid) 24 | self.is_open = False 25 | 26 | def open(self): 27 | if self.is_open: 28 | return 29 | 30 | logging.info("Deck opened") 31 | self.is_open = True 32 | 33 | def close(self): 34 | if not self.is_open: 35 | return 36 | 37 | logging.info("Deck closed") 38 | self.is_open = False 39 | 40 | def is_open(self): 41 | return True 42 | 43 | def connected(self): 44 | return True 45 | 46 | def vendor_id(self): 47 | return self.vid 48 | 49 | def product_id(self): 50 | return self.pid 51 | 52 | def path(self): 53 | return self.id 54 | 55 | def write_feature(self, payload): 56 | if not self.is_open: 57 | raise TransportError("Deck feature write while deck not open.") 58 | 59 | logging.info("Deck feature write (length %s):\n%s", len(payload), binascii.hexlify(payload, ' ').decode('utf-8')) 60 | return True 61 | 62 | def read_feature(self, report_id, length): 63 | if not self.is_open: 64 | raise TransportError("Deck feature read while deck not open.") 65 | 66 | logging.info("Deck feature read (length %s)", length) 67 | return bytearray(length) 68 | 69 | def write(self, payload): 70 | if not self.is_open: 71 | raise TransportError("Deck write while deck not open.") 72 | 73 | logging.info("Deck report write (length %s):\n%s", len(payload), binascii.hexlify(payload, ' ').decode('utf-8')) 74 | return True 75 | 76 | def read(self, length): 77 | if not self.is_open: 78 | raise TransportError("Deck read while deck not open.") 79 | 80 | logging.info("Deck report read (length %s)", length) 81 | return bytearray(length) 82 | 83 | @staticmethod 84 | def probe(): 85 | pass 86 | 87 | def enumerate(self, vid, pid): 88 | return [Dummy.Device(vid=vid, pid=pid)] 89 | -------------------------------------------------------------------------------- /src/StreamDeck/Transport/LibUSBHIDAPI.py: -------------------------------------------------------------------------------- 1 | # Python Stream Deck Library 2 | # Released under the MIT license 3 | # 4 | # dean [at] fourwalledcubicle [dot] com 5 | # www.fourwalledcubicle.com 6 | # 7 | 8 | import atexit 9 | import ctypes 10 | import ctypes.util 11 | import os 12 | import platform 13 | import threading 14 | 15 | from .Transport import Transport, TransportError 16 | 17 | 18 | class LibUSBHIDAPI(Transport): 19 | """ 20 | USB HID transport layer, using the LibUSB HIDAPI dynamically linked library 21 | directly via ctypes. 22 | """ 23 | 24 | class Library(): 25 | HIDAPI_INSTANCE = None 26 | HOMEBREW_PREFIX = None 27 | 28 | def _get_homebrew_path(self): 29 | if self.platform_name != "Darwin": 30 | return None 31 | 32 | homebrew_path = os.environ.get('HOMEBREW_PREFIX') 33 | if not homebrew_path: 34 | try: 35 | import subprocess # nosec B404 36 | 37 | homebrew_path = subprocess.run(['brew', '--prefix'], stdout=subprocess.PIPE, text=True, check=True).stdout.strip() # nosec 38 | except: # nosec B110 39 | pass 40 | 41 | return homebrew_path 42 | 43 | def _load_hidapi_library(self, library_search_list): 44 | """ 45 | Loads the given LibUSB HIDAPI dynamic library from the host system, 46 | if available. 47 | 48 | :rtype: ctypes.CDLL 49 | :return: Loaded HIDAPI library instance, or None if no library was found. 50 | """ 51 | 52 | # If the library's already loaded, we can use the existing instance and skip the slow load process. 53 | if self.HIDAPI_INSTANCE: 54 | return self.HIDAPI_INSTANCE 55 | 56 | # If we're running on MacOS, we very likely need to search for the library in the Homebrew path if it's installed. 57 | # The ctypes loader won't look in there by default unless the user has a Homebrew installed python, which gets patched 58 | # on installation. 59 | if not self.HOMEBREW_PREFIX: 60 | type(self).HOMEBREW_PREFIX = self._get_homebrew_path() 61 | 62 | for lib_name in library_search_list: 63 | # We'll try to use ctypes' utility function to find the library first, using 64 | # its default search paths. It requires the name of the library only (minus all 65 | # path prefix and extension suffix). 66 | library_name_no_extension = os.path.basename(os.path.splitext(lib_name)[0]) 67 | try: 68 | found_lib = ctypes.util.find_library(library_name_no_extension) 69 | except: 70 | found_lib = None 71 | 72 | # If we've running with a Homebrew installation, and find_library() didn't find the library in 73 | # any of the default search paths, we'll look in Homebrew instead as a fallback. 74 | if not found_lib and self.HOMEBREW_PREFIX: 75 | library_path_homebrew = os.path.join(self.HOMEBREW_PREFIX, 'lib', lib_name) 76 | 77 | if os.path.exists(library_path_homebrew): 78 | found_lib = library_path_homebrew 79 | 80 | try: 81 | type(self).HIDAPI_INSTANCE = ctypes.cdll.LoadLibrary(found_lib if found_lib else lib_name) 82 | break 83 | except: # nosec B110 84 | pass 85 | else: 86 | return None 87 | 88 | class hid_device_info(ctypes.Structure): 89 | """ 90 | Structure definition for the hid_device_info structure defined 91 | in the LibUSB HIDAPI library API. 92 | """ 93 | pass 94 | 95 | hid_device_info._fields_ = [ 96 | ('path', ctypes.c_char_p), 97 | ('vendor_id', ctypes.c_ushort), 98 | ('product_id', ctypes.c_ushort), 99 | ('serial_number', ctypes.c_wchar_p), 100 | ('release_number', ctypes.c_ushort), 101 | ('manufacturer_string', ctypes.c_wchar_p), 102 | ('product_string', ctypes.c_wchar_p), 103 | ('usage_page', ctypes.c_ushort), 104 | ('usage', ctypes.c_ushort), 105 | ('interface_number', ctypes.c_int), 106 | ('next', ctypes.POINTER(hid_device_info)) 107 | ] 108 | 109 | self.HIDAPI_INSTANCE.hid_init.argtypes = [] 110 | self.HIDAPI_INSTANCE.hid_init.restype = ctypes.c_int 111 | 112 | self.HIDAPI_INSTANCE.hid_exit.argtypes = [] 113 | self.HIDAPI_INSTANCE.hid_exit.restype = ctypes.c_int 114 | 115 | self.HIDAPI_INSTANCE.hid_enumerate.argtypes = [ctypes.c_ushort, ctypes.c_ushort] 116 | self.HIDAPI_INSTANCE.hid_enumerate.restype = ctypes.POINTER(hid_device_info) 117 | 118 | self.HIDAPI_INSTANCE.hid_free_enumeration.argtypes = [ctypes.POINTER(hid_device_info)] 119 | self.HIDAPI_INSTANCE.hid_free_enumeration.restype = None 120 | 121 | self.HIDAPI_INSTANCE.hid_open_path.argtypes = [ctypes.c_char_p] 122 | self.HIDAPI_INSTANCE.hid_open_path.restype = ctypes.c_void_p 123 | 124 | self.HIDAPI_INSTANCE.hid_close.argtypes = [ctypes.c_void_p] 125 | self.HIDAPI_INSTANCE.hid_close.restype = None 126 | 127 | self.HIDAPI_INSTANCE.hid_set_nonblocking.argtypes = [ctypes.c_void_p, ctypes.c_int] 128 | self.HIDAPI_INSTANCE.hid_set_nonblocking.restype = ctypes.c_int 129 | 130 | self.HIDAPI_INSTANCE.hid_send_feature_report.argtypes = [ctypes.c_void_p, ctypes.POINTER(ctypes.c_char), ctypes.c_size_t] 131 | self.HIDAPI_INSTANCE.hid_send_feature_report.restype = ctypes.c_int 132 | 133 | self.HIDAPI_INSTANCE.hid_get_feature_report.argtypes = [ctypes.c_void_p, ctypes.POINTER(ctypes.c_char), ctypes.c_size_t] 134 | self.HIDAPI_INSTANCE.hid_get_feature_report.restype = ctypes.c_int 135 | 136 | self.HIDAPI_INSTANCE.hid_write.argtypes = [ctypes.c_void_p, ctypes.POINTER(ctypes.c_char), ctypes.c_size_t] 137 | self.HIDAPI_INSTANCE.hid_write.restype = ctypes.c_int 138 | 139 | self.HIDAPI_INSTANCE.hid_read.argtypes = [ctypes.c_void_p, ctypes.POINTER(ctypes.c_char), ctypes.c_size_t] 140 | self.HIDAPI_INSTANCE.hid_read.restype = ctypes.c_int 141 | 142 | self.HIDAPI_INSTANCE.hid_init() 143 | atexit.register(self.HIDAPI_INSTANCE.hid_exit) 144 | 145 | return self.HIDAPI_INSTANCE 146 | 147 | def __init__(self): 148 | """ 149 | Creates a new LibUSB HIDAPI library instance, used to interface with 150 | HID devices attached to the host system. 151 | """ 152 | 153 | search_library_names = { 154 | "Windows": ["hidapi.dll", "libhidapi-0.dll", "./hidapi.dll"], 155 | "Linux": ["libhidapi-libusb.so", "libhidapi-libusb.so.0"], 156 | "Darwin": ["libhidapi.dylib"], 157 | "FreeBSD": ["libhidapi.so"], 158 | } 159 | 160 | self.platform_name = platform.system() 161 | platform_search_library_names = search_library_names.get(self.platform_name) 162 | 163 | if not platform_search_library_names: 164 | raise TransportError("No suitable LibUSB HIDAPI library search names were found for this system.") 165 | 166 | self.hidapi = self._load_hidapi_library(platform_search_library_names) 167 | if not self.hidapi: 168 | raise TransportError("No suitable LibUSB HIDAPI library found on this system. Is the '{}' library installed?".format(platform_search_library_names[0])) 169 | 170 | self.mutex = threading.Lock() 171 | 172 | def enumerate(self, vendor_id=None, product_id=None): 173 | """ 174 | Enumerates all available USB HID devices on the system. 175 | 176 | :param int vid: USB Vendor ID to filter all devices by, `None` if the 177 | device list should not be filtered by vendor. 178 | :param int pid: USB Product ID to filter all devices by, `None` if the 179 | device list should not be filtered by product. 180 | 181 | :rtype: list(dict()) 182 | :return: List of discovered USB HID device attributes. 183 | """ 184 | 185 | vendor_id = vendor_id or 0 186 | product_id = product_id or 0 187 | 188 | device_list = [] 189 | 190 | with self.mutex: 191 | device_enumeration = self.hidapi.hid_enumerate(vendor_id, product_id) 192 | 193 | if device_enumeration: 194 | current_device = device_enumeration 195 | 196 | while current_device: 197 | device_list.append({ 198 | 'path': current_device.contents.path.decode('utf-8'), 199 | 'vendor_id': current_device.contents.vendor_id, 200 | 'product_id': current_device.contents.product_id, 201 | }) 202 | 203 | current_device = current_device.contents.next 204 | 205 | self.hidapi.hid_free_enumeration(device_enumeration) 206 | 207 | return device_list 208 | 209 | def open_device(self, path): 210 | """ 211 | Opens a HID device by its canonical path on the host system. 212 | 213 | :rtype: Handle 214 | :return: Device handle if opened successfully, None if open failed. 215 | """ 216 | with self.mutex: 217 | if type(path) is not bytes: 218 | path = bytes(path, 'utf-8') 219 | 220 | handle = self.hidapi.hid_open_path(path) 221 | 222 | if not handle: 223 | raise TransportError("Could not open HID device.") 224 | 225 | self.hidapi.hid_set_nonblocking(handle, 1) 226 | 227 | return handle 228 | 229 | def close_device(self, handle): 230 | """ 231 | Closes a HID device by its open device handle on the host system. 232 | 233 | :param Handle handle: Device handle to close. 234 | """ 235 | with self.mutex: 236 | if handle: 237 | self.hidapi.hid_close(handle) 238 | 239 | def send_feature_report(self, handle, data): 240 | """ 241 | Sends a HID Feature report to an open HID device. 242 | 243 | :param Handle handle: Device handle to access. 244 | :param bytearray() data: Array of bytes to send to the device, as a 245 | feature report. The first byte of the 246 | report should be the Report ID of the 247 | report being sent. 248 | 249 | :rtype: int 250 | :return: Number of bytes successfully sent to the device. 251 | """ 252 | if not handle: 253 | raise TransportError("No HID device.") 254 | 255 | with self.mutex: 256 | result = self.hidapi.hid_send_feature_report(handle, bytes(data), len(data)) 257 | 258 | if result < 0: 259 | raise TransportError("Failed to write feature report (%d)" % result) 260 | 261 | return result 262 | 263 | def get_feature_report(self, handle, report_id, length): 264 | """ 265 | Retrieves a HID Feature report from an open HID device. 266 | 267 | :param Handle handle: Device handle to access. 268 | :param int report_id: Report ID of the report being read. 269 | :param int length: Maximum length of the Feature report to read. 270 | 271 | :rtype: bytearray() 272 | :return: Array of bytes containing the read Feature report. The 273 | first byte of the report will be the Report ID of the 274 | report that was read. 275 | """ 276 | if not handle: 277 | raise TransportError("No HID device.") 278 | 279 | # We may need to oversize our read due a bug in some versions of 280 | # HIDAPI. Only applied on Mac systems, as this will cause other 281 | # issues on other platforms. 282 | read_length = (length + 1) if self.platform_name == 'Darwin' else length 283 | 284 | data = ctypes.create_string_buffer(read_length) 285 | data[0] = report_id 286 | 287 | with self.mutex: 288 | result = self.hidapi.hid_get_feature_report(handle, data, len(data)) 289 | 290 | if result < 0: 291 | raise TransportError("Failed to read feature report (%d)" % result) 292 | 293 | if length < read_length and result == read_length: 294 | # Mac HIDAPI 0.9.0 bug, we read one less than we expected (not including report ID). 295 | # We requested an over-sized report, so we actually got the amount we wanted. 296 | return data.raw 297 | 298 | # We read an extra byte (as expected). Just return the first length requested bytes. 299 | return data.raw[:length] 300 | 301 | def write(self, handle, data): 302 | """ 303 | Writes a HID Out report to an open HID device. 304 | 305 | :param Handle handle: Device handle to access. 306 | :param bytearray() data: Array of bytes to send to the device, as an 307 | out report. The first byte of the report 308 | should be the Report ID of the report being 309 | sent. 310 | 311 | :rtype: int 312 | :return: Number of bytes successfully sent to the device. 313 | """ 314 | if not handle: 315 | raise TransportError("No HID device.") 316 | 317 | with self.mutex: 318 | result = self.hidapi.hid_write(handle, bytes(data), len(data)) 319 | 320 | if result < 0: 321 | raise TransportError("Failed to write out report (%d)" % result) 322 | 323 | return result 324 | 325 | def read(self, handle, length): 326 | """ 327 | Performs a non-blocking read of a HID In report from an open HID device. 328 | 329 | :param Handle handle: Device handle to access. 330 | :param int length: Maximum length of the In report to read. 331 | 332 | :rtype: bytearray() 333 | :return: Array of bytes containing the read In report. The 334 | first byte of the report will be the Report ID of the 335 | report that was read. 336 | """ 337 | if not handle: 338 | raise TransportError("No HID device.") 339 | 340 | data = ctypes.create_string_buffer(length) 341 | 342 | with self.mutex: 343 | result = self.hidapi.hid_read(handle, data, len(data)) 344 | 345 | if result < 0: 346 | raise TransportError("Failed to read in report (%d)" % result) 347 | elif result == 0: 348 | return None 349 | 350 | return data.raw[:length] 351 | 352 | class Device(Transport.Device): 353 | def __init__(self, hidapi, device_info): 354 | self.hidapi = hidapi 355 | self.device_info = device_info 356 | self.device_handle = None 357 | self.mutex = threading.Lock() 358 | 359 | def __del__(self): 360 | self.close() 361 | 362 | def __exit__(self): 363 | self.close() 364 | 365 | def open(self): 366 | with self.mutex: 367 | if self.device_handle: 368 | return 369 | 370 | self.device_handle = self.hidapi.open_device(self.device_info['path']) 371 | 372 | def close(self): 373 | with self.mutex: 374 | if self.device_handle: 375 | self.hidapi.close_device(self.device_handle) 376 | self.device_handle = None 377 | 378 | def is_open(self): 379 | with self.mutex: 380 | return self.device_handle is not None 381 | 382 | def connected(self): 383 | with self.mutex: 384 | return any([d['path'] == self.device_info['path'] for d in self.hidapi.enumerate()]) 385 | 386 | def vendor_id(self): 387 | return self.device_info['vendor_id'] 388 | 389 | def product_id(self): 390 | return self.device_info['product_id'] 391 | 392 | def path(self): 393 | return self.device_info['path'] 394 | 395 | def write_feature(self, payload): 396 | with self.mutex: 397 | return self.hidapi.send_feature_report(self.device_handle, payload) 398 | 399 | def read_feature(self, report_id, length): 400 | with self.mutex: 401 | return self.hidapi.get_feature_report(self.device_handle, report_id, length) 402 | 403 | def write(self, payload): 404 | with self.mutex: 405 | return self.hidapi.write(self.device_handle, payload) 406 | 407 | def read(self, length): 408 | with self.mutex: 409 | return self.hidapi.read(self.device_handle, length) 410 | 411 | @staticmethod 412 | def probe(): 413 | LibUSBHIDAPI.Library() 414 | 415 | def enumerate(self, vid, pid): 416 | hidapi = LibUSBHIDAPI.Library() 417 | 418 | return [LibUSBHIDAPI.Device(hidapi, d) for d in hidapi.enumerate(vendor_id=vid, product_id=pid)] 419 | -------------------------------------------------------------------------------- /src/StreamDeck/Transport/Transport.py: -------------------------------------------------------------------------------- 1 | # Python Stream Deck Library 2 | # Released under the MIT license 3 | # 4 | # dean [at] fourwalledcubicle [dot] com 5 | # www.fourwalledcubicle.com 6 | # 7 | 8 | from abc import ABC, abstractmethod 9 | 10 | 11 | class TransportError(Exception): 12 | """ 13 | Exception thrown when attempting to access a device using a backend 14 | transport that has failed (for example, if the requested device could not 15 | be accessed). 16 | """ 17 | 18 | pass 19 | 20 | 21 | class Transport(ABC): 22 | """ 23 | Base transport layer, representing an abstract communication back-end which 24 | can be used to discovery attached StreamDeck devices. 25 | """ 26 | 27 | class Device(ABC): 28 | """ 29 | Base connection device, representing an abstract connected device which 30 | can be communicated with by an upper layer high level protocol. 31 | """ 32 | 33 | @abstractmethod 34 | def open(self) -> None: 35 | """ 36 | Opens the device for input/output. This must be called prior to 37 | sending or receiving any reports. 38 | 39 | .. seealso:: See :func:`~Transport.Device.close` for the 40 | corresponding close method. 41 | """ 42 | pass 43 | 44 | @abstractmethod 45 | def close(self) -> None: 46 | """ 47 | Closes the device for input/output. 48 | 49 | .. seealso:: See :func:`~~Transport.Device.open` for the 50 | corresponding open method. 51 | """ 52 | pass 53 | 54 | @abstractmethod 55 | def is_open(self) -> bool: 56 | """ 57 | Indicates if the physical device object this instance is attached 58 | to has been opened by the host. 59 | 60 | :rtype: bool 61 | :return: `True` if the device is open, `False` otherwise. 62 | """ 63 | pass 64 | 65 | @abstractmethod 66 | def connected(self) -> bool: 67 | """ 68 | Indicates if the physical device object this instance is attached 69 | to is still connected to the host. 70 | 71 | :rtype: bool 72 | :return: `True` if the device is still connected, `False` otherwise. 73 | """ 74 | pass 75 | 76 | @abstractmethod 77 | def path(self) -> str: 78 | """ 79 | Retrieves the logical path of the attached device within the 80 | current system. This can be used to uniquely differentiate one 81 | device from another. 82 | 83 | :rtype: str 84 | :return: Logical device path for the attached device. 85 | """ 86 | pass 87 | 88 | @abstractmethod 89 | def vendor_id(self) -> int: 90 | """ 91 | Retrieves the vendor ID value of the attached device. 92 | 93 | :rtype: int 94 | :return: Vendor ID of the attached device. 95 | """ 96 | pass 97 | 98 | @abstractmethod 99 | def product_id(self) -> int: 100 | """ 101 | Retrieves the product ID value of the attached device. 102 | 103 | :rtype: int 104 | :return: Product ID of the attached device. 105 | """ 106 | pass 107 | 108 | @abstractmethod 109 | def write_feature(self, payload: bytes) -> int: 110 | """ 111 | Sends a HID Feature report to the open HID device. 112 | 113 | :param enumerable() payload: Enumerate list of bytes to send to the 114 | device, as a feature report. The first 115 | byte of the report should be the Report 116 | ID of the report being sent. 117 | 118 | :rtype: int 119 | :return: Number of bytes successfully sent to the device. 120 | """ 121 | pass 122 | 123 | @abstractmethod 124 | def read_feature(self, report_id: int, length: int) -> bytes: 125 | """ 126 | Reads a HID Feature report from the open HID device. 127 | 128 | :param int report_id: Report ID of the report being read. 129 | :param int length: Maximum length of the Feature report to read. 130 | 131 | :rtype: list(byte) 132 | :return: List of bytes containing the read Feature report. The 133 | first byte of the report will be the Report ID of the 134 | report that was read. 135 | """ 136 | pass 137 | 138 | @abstractmethod 139 | def write(self, payload: bytes) -> int: 140 | """ 141 | Sends a HID Out report to the open HID device. 142 | 143 | :param enumerable() payload: Enumerate list of bytes to send to the 144 | device, as an Out report. The first 145 | byte of the report should be the Report 146 | ID of the report being sent. 147 | 148 | :rtype: int 149 | :return: Number of bytes successfully sent to the device. 150 | """ 151 | pass 152 | 153 | @abstractmethod 154 | def read(self, length: int) -> bytes: 155 | """ 156 | Performs a blocking read of a HID In report from the open HID device. 157 | 158 | :param int length: Maximum length of the In report to read. 159 | 160 | :rtype: list(byte) 161 | :return: List of bytes containing the read In report. The first byte 162 | of the report will be the Report ID of the report that was 163 | read. 164 | """ 165 | pass 166 | 167 | @staticmethod 168 | @abstractmethod 169 | def probe() -> None: 170 | """ 171 | Attempts to determine if the back-end is installed and usable. It is 172 | expected that probe failures throw exceptions detailing their exact 173 | cause of failure. 174 | """ 175 | pass 176 | 177 | @abstractmethod 178 | def enumerate(self, vid: int, pid: int) -> list[Device]: 179 | """ 180 | Enumerates all available devices on the system using the current 181 | transport back-end. 182 | 183 | :param int vid: USB Vendor ID to filter all devices by, `None` if the 184 | device list should not be filtered by vendor. 185 | :param int pid: USB Product ID to filter all devices by, `None` if the 186 | device list should not be filtered by product. 187 | 188 | :rtype: list(Transport.Device) 189 | :return: List of discovered devices that are available through this 190 | transport back-end. 191 | """ 192 | pass 193 | -------------------------------------------------------------------------------- /src/StreamDeck/Transport/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/abcminiuser/python-elgato-streamdeck/34dbf63a79d50834d554e955c8df3644eb0530be/src/StreamDeck/Transport/__init__.py -------------------------------------------------------------------------------- /src/StreamDeck/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/abcminiuser/python-elgato-streamdeck/34dbf63a79d50834d554e955c8df3644eb0530be/src/StreamDeck/__init__.py -------------------------------------------------------------------------------- /src/example_animated.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | # Python Stream Deck Library 4 | # Released under the MIT license 5 | # 6 | # dean [at] fourwalledcubicle [dot] com 7 | # www.fourwalledcubicle.com 8 | # 9 | 10 | # Example script showing one way to display animated images using the 11 | # library, by pre-rendering all the animation frames into the StreamDeck 12 | # device's native image format, and displaying them with a periodic 13 | # timer. 14 | 15 | import itertools 16 | import os 17 | import threading 18 | import time 19 | 20 | from fractions import Fraction 21 | from PIL import Image, ImageSequence 22 | from StreamDeck.DeviceManager import DeviceManager 23 | from StreamDeck.ImageHelpers import PILHelper 24 | from StreamDeck.Transport.Transport import TransportError 25 | 26 | # Folder location of image assets used by this example. 27 | ASSETS_PATH = os.path.join(os.path.dirname(__file__), "Assets") 28 | 29 | # Animation frames per second to attempt to display on the StreamDeck devices. 30 | FRAMES_PER_SECOND = 30 31 | 32 | 33 | # Loads in a source image, extracts out the individual animation frames (if 34 | # any) and returns a list of animation frames in the StreamDeck device's 35 | # native image format. 36 | def create_animation_frames(deck, image_filename): 37 | icon_frames = list() 38 | 39 | # Open the source image asset. 40 | icon = Image.open(os.path.join(ASSETS_PATH, image_filename)) 41 | 42 | # Iterate through each animation frame of the source image 43 | for frame in ImageSequence.Iterator(icon): 44 | # Create new key image of the correct dimensions, black background. 45 | frame_image = PILHelper.create_scaled_key_image(deck, frame) 46 | 47 | # Pre-convert the generated image to the native format of the StreamDeck 48 | # so we don't need to keep converting it when showing it on the device. 49 | native_frame_image = PILHelper.to_native_key_format(deck, frame_image) 50 | 51 | # Store the rendered animation frame for later user. 52 | icon_frames.append(native_frame_image) 53 | 54 | # Return the decoded list of frames - the caller will need to decide how to 55 | # sequence them for display. 56 | return icon_frames 57 | 58 | 59 | # Closes the StreamDeck device on key state change. 60 | def key_change_callback(deck, key, state): 61 | # Use a scoped-with on the deck to ensure we're the only thread using it 62 | # right now. 63 | with deck: 64 | # Reset deck, clearing all button images. 65 | deck.reset() 66 | 67 | # Close deck handle, terminating internal worker threads. 68 | deck.close() 69 | 70 | 71 | if __name__ == "__main__": 72 | streamdecks = DeviceManager().enumerate() 73 | 74 | print("Found {} Stream Deck(s).\n".format(len(streamdecks))) 75 | 76 | for index, deck in enumerate(streamdecks): 77 | # This example only works with devices that have screens. 78 | if not deck.is_visual(): 79 | continue 80 | 81 | deck.open() 82 | deck.reset() 83 | 84 | print("Opened '{}' device (serial number: '{}')".format(deck.deck_type(), deck.get_serial_number())) 85 | 86 | # Set initial screen brightness to 30%. 87 | deck.set_brightness(30) 88 | 89 | # Pre-render a list of animation frames for each source image, in the 90 | # native display format so that they can be quickly sent to the device. 91 | print("Loading animations...") 92 | animations = [ 93 | create_animation_frames(deck, "Elephant_Walking_animated.gif"), 94 | create_animation_frames(deck, "RGB_color_space_animated_view.gif"), 95 | create_animation_frames(deck, "Simple_CV_Joint_animated.gif"), 96 | ] 97 | print("Ready.") 98 | 99 | # Create a mapping of StreamDeck keys to animation image sets that will 100 | # be displayed. 101 | key_images = dict() 102 | for k in range(deck.key_count()): 103 | # Each key gets an infinite cycle generator bound to the animation 104 | # frames, so it will loop the animated sequence forever. 105 | key_images[k] = itertools.cycle(animations[k % len(animations)]) 106 | 107 | # Helper function that will run a periodic loop which updates the 108 | # images on each key. 109 | def animate(fps): 110 | # Convert frames per second to frame time in seconds. 111 | # 112 | # Frame time often cannot be fully expressed by a float type, 113 | # meaning that we have to use fractions. 114 | frame_time = Fraction(1, fps) 115 | 116 | # Get a starting absolute time reference point. 117 | # 118 | # We need to use an absolute time clock, instead of relative sleeps 119 | # with a constant value, to avoid drifting. 120 | # 121 | # Drifting comes from an overhead of scheduling the sleep itself - 122 | # it takes some small amount of time for `time.sleep()` to execute. 123 | next_frame = Fraction(time.monotonic()) 124 | 125 | # Periodic loop that will render every frame at the set FPS until 126 | # the StreamDeck device we're using is closed. 127 | while deck.is_open(): 128 | try: 129 | # Use a scoped-with on the deck to ensure we're the only 130 | # thread using it right now. 131 | with deck: 132 | # Update the key images with the next animation frame. 133 | for key, frames in key_images.items(): 134 | deck.set_key_image(key, next(frames)) 135 | except TransportError as err: 136 | print("TransportError: {0}".format(err)) 137 | # Something went wrong while communicating with the device 138 | # (closed?) - don't re-schedule the next animation frame. 139 | break 140 | 141 | # Set the next frame absolute time reference point. 142 | # 143 | # We are running at the fixed `fps`, so this is as simple as 144 | # adding the frame time we calculated earlier. 145 | next_frame += frame_time 146 | 147 | # Knowing the start of the next frame, we can calculate how long 148 | # we have to sleep until its start. 149 | sleep_interval = float(next_frame) - time.monotonic() 150 | 151 | # Schedule the next periodic frame update. 152 | # 153 | # `sleep_interval` can be a negative number when current FPS 154 | # setting is too high for the combination of host and 155 | # StreamDeck to handle. If this is the case, we skip sleeping 156 | # immediately render the next frame to try to catch up. 157 | if sleep_interval >= 0: 158 | time.sleep(sleep_interval) 159 | 160 | # Kick off the key image animating thread. 161 | threading.Thread(target=animate, args=[FRAMES_PER_SECOND]).start() 162 | 163 | # Register callback function for when a key state changes. 164 | deck.set_key_callback(key_change_callback) 165 | 166 | # Wait until all application threads have terminated (for this example, 167 | # this is when all deck handles are closed). 168 | for t in threading.enumerate(): 169 | try: 170 | t.join() 171 | except (TransportError, RuntimeError): 172 | pass 173 | -------------------------------------------------------------------------------- /src/example_basic.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | # Python Stream Deck Library 4 | # Released under the MIT license 5 | # 6 | # dean [at] fourwalledcubicle [dot] com 7 | # www.fourwalledcubicle.com 8 | # 9 | 10 | # Example script showing basic library usage - updating key images with new 11 | # tiles generated at runtime, and responding to button state change events. 12 | 13 | import os 14 | import threading 15 | 16 | from PIL import Image, ImageDraw, ImageFont 17 | from StreamDeck.DeviceManager import DeviceManager 18 | from StreamDeck.ImageHelpers import PILHelper 19 | from StreamDeck.Transport.Transport import TransportError 20 | 21 | # Folder location of image assets used by this example. 22 | ASSETS_PATH = os.path.join(os.path.dirname(__file__), "Assets") 23 | 24 | 25 | # Generates a custom tile with run-time generated text and custom image via the 26 | # PIL module. 27 | def render_key_image(deck, icon_filename, font_filename, label_text): 28 | # Resize the source image asset to best-fit the dimensions of a single key, 29 | # leaving a margin at the bottom so that we can draw the key title 30 | # afterwards. 31 | icon = Image.open(icon_filename) 32 | image = PILHelper.create_scaled_key_image(deck, icon, margins=[0, 0, 20, 0]) 33 | 34 | # Load a custom TrueType font and use it to overlay the key index, draw key 35 | # label onto the image a few pixels from the bottom of the key. 36 | draw = ImageDraw.Draw(image) 37 | font = ImageFont.truetype(font_filename, 14) 38 | draw.text((image.width / 2, image.height - 5), text=label_text, font=font, anchor="ms", fill="white") 39 | 40 | return PILHelper.to_native_key_format(deck, image) 41 | 42 | 43 | # Returns styling information for a key based on its position and state. 44 | def get_key_style(deck, key, state): 45 | # Last button in the example application is the exit button. 46 | exit_key_index = deck.key_count() - 1 47 | 48 | if key == exit_key_index: 49 | name = "exit" 50 | icon = "{}.png".format("Exit") 51 | font = "Roboto-Regular.ttf" 52 | label = "Bye" if state else "Exit" 53 | else: 54 | name = "emoji" 55 | icon = "{}.png".format("Pressed" if state else "Released") 56 | font = "Roboto-Regular.ttf" 57 | label = "Pressed!" if state else "Key {}".format(key) 58 | 59 | return { 60 | "name": name, 61 | "icon": os.path.join(ASSETS_PATH, icon), 62 | "font": os.path.join(ASSETS_PATH, font), 63 | "label": label 64 | } 65 | 66 | 67 | # Creates a new key image based on the key index, style and current key state 68 | # and updates the image on the StreamDeck. 69 | def update_key_image(deck, key, state): 70 | # Determine what icon and label to use on the generated key. 71 | key_style = get_key_style(deck, key, state) 72 | 73 | # Generate the custom key with the requested image and label. 74 | image = render_key_image(deck, key_style["icon"], key_style["font"], key_style["label"]) 75 | 76 | # Use a scoped-with on the deck to ensure we're the only thread using it 77 | # right now. 78 | with deck: 79 | # Update requested key with the generated image. 80 | deck.set_key_image(key, image) 81 | 82 | 83 | # Prints key state change information, updates rhe key image and performs any 84 | # associated actions when a key is pressed. 85 | def key_change_callback(deck, key, state): 86 | # Print new key state 87 | print("Deck {} Key {} = {}".format(deck.id(), key, state), flush=True) 88 | 89 | # Don't try to draw an image on a touch button 90 | if key >= deck.key_count(): 91 | return 92 | 93 | # Update the key image based on the new key state. 94 | update_key_image(deck, key, state) 95 | 96 | # Check if the key is changing to the pressed state. 97 | if state: 98 | key_style = get_key_style(deck, key, state) 99 | 100 | # When an exit button is pressed, close the application. 101 | if key_style["name"] == "exit": 102 | # Use a scoped-with on the deck to ensure we're the only thread 103 | # using it right now. 104 | with deck: 105 | # Reset deck, clearing all button images. 106 | deck.reset() 107 | 108 | # Close deck handle, terminating internal worker threads. 109 | deck.close() 110 | 111 | 112 | if __name__ == "__main__": 113 | streamdecks = DeviceManager().enumerate() 114 | 115 | print("Found {} Stream Deck(s).\n".format(len(streamdecks))) 116 | 117 | for index, deck in enumerate(streamdecks): 118 | # This example only works with devices that have screens. 119 | if not deck.is_visual(): 120 | continue 121 | 122 | deck.open() 123 | deck.reset() 124 | 125 | print("Opened '{}' device (serial number: '{}', fw: '{}')".format( 126 | deck.deck_type(), deck.get_serial_number(), deck.get_firmware_version() 127 | )) 128 | 129 | # Set initial screen brightness to 30%. 130 | deck.set_brightness(30) 131 | 132 | # Set initial key images. 133 | for key in range(deck.key_count()): 134 | update_key_image(deck, key, False) 135 | 136 | # Register callback function for when a key state changes. 137 | deck.set_key_callback(key_change_callback) 138 | 139 | # Wait until all application threads have terminated (for this example, 140 | # this is when all deck handles are closed). 141 | for t in threading.enumerate(): 142 | try: 143 | t.join() 144 | except (TransportError, RuntimeError): 145 | pass 146 | -------------------------------------------------------------------------------- /src/example_deckinfo.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | # Python Stream Deck Library 4 | # Released under the MIT license 5 | # 6 | # dean [at] fourwalledcubicle [dot] com 7 | # www.fourwalledcubicle.com 8 | # 9 | 10 | # Example script that prints out information about any discovered StreamDeck 11 | # devices to the console. 12 | 13 | from StreamDeck.DeviceManager import DeviceManager 14 | 15 | 16 | # Prints diagnostic information about a given StreamDeck. 17 | def print_deck_info(index, deck): 18 | key_image_format = deck.key_image_format() 19 | touchscreen_image_format = deck.touchscreen_image_format() 20 | 21 | flip_description = { 22 | (False, False): "not mirrored", 23 | (True, False): "mirrored horizontally", 24 | (False, True): "mirrored vertically", 25 | (True, True): "mirrored horizontally/vertically", 26 | } 27 | 28 | print("Deck {} - {}.".format(index, deck.deck_type())) 29 | print("\t - ID: {}".format(deck.id())) 30 | print("\t - Serial: '{}'".format(deck.get_serial_number())) 31 | print("\t - Firmware Version: '{}'".format(deck.get_firmware_version())) 32 | print("\t - Key Count: {} (in a {}x{} grid)".format( 33 | deck.key_count(), 34 | deck.key_layout()[0], 35 | deck.key_layout()[1])) 36 | if deck.is_visual(): 37 | print("\t - Key Images: {}x{} pixels, {} format, rotated {} degrees, {}".format( 38 | key_image_format['size'][0], 39 | key_image_format['size'][1], 40 | key_image_format['format'], 41 | key_image_format['rotation'], 42 | flip_description[key_image_format['flip']])) 43 | 44 | if deck.is_touch(): 45 | print("\t - Touchscreen: {}x{} pixels, {} format, rotated {} degrees, {}".format( 46 | touchscreen_image_format['size'][0], 47 | touchscreen_image_format['size'][1], 48 | touchscreen_image_format['format'], 49 | touchscreen_image_format['rotation'], 50 | flip_description[touchscreen_image_format['flip']])) 51 | else: 52 | print("\t - No Visual Output") 53 | 54 | 55 | if __name__ == "__main__": 56 | streamdecks = DeviceManager().enumerate() 57 | 58 | print("Found {} Stream Deck(s).\n".format(len(streamdecks))) 59 | 60 | for index, deck in enumerate(streamdecks): 61 | deck.open() 62 | deck.reset() 63 | 64 | print_deck_info(index, deck) 65 | 66 | deck.close() 67 | -------------------------------------------------------------------------------- /src/example_neo.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | # Python Stream Deck Library 4 | # Released under the MIT license 5 | # 6 | # dean [at] fourwalledcubicle [dot] com 7 | # www.fourwalledcubicle.com 8 | # 9 | 10 | # Example script showing basic library usage - updating key images with new 11 | # tiles generated at runtime, and responding to button state change events. 12 | 13 | import os 14 | import threading 15 | import random 16 | 17 | from PIL import Image, ImageDraw, ImageFont 18 | from StreamDeck.DeviceManager import DeviceManager 19 | from StreamDeck.ImageHelpers import PILHelper 20 | from StreamDeck.Transport.Transport import TransportError 21 | 22 | # Folder location of image assets used by this example. 23 | ASSETS_PATH = os.path.join(os.path.dirname(__file__), "Assets") 24 | 25 | 26 | # Generates a custom tile with run-time generated text and custom image via the 27 | # PIL module. 28 | def render_key_image(deck, icon_filename, font_filename, label_text): 29 | # Resize the source image asset to best-fit the dimensions of a single key, 30 | # leaving a margin at the bottom so that we can draw the key title 31 | # afterwards. 32 | icon = Image.open(icon_filename) 33 | image = PILHelper.create_scaled_key_image(deck, icon, margins=[0, 0, 20, 0]) 34 | 35 | # Load a custom TrueType font and use it to overlay the key index, draw key 36 | # label onto the image a few pixels from the bottom of the key. 37 | draw = ImageDraw.Draw(image) 38 | font = ImageFont.truetype(font_filename, 14) 39 | draw.text((image.width / 2, image.height - 5), text=label_text, font=font, anchor="ms", fill="white") 40 | 41 | return PILHelper.to_native_key_format(deck, image) 42 | 43 | 44 | # Generate an image for the screen 45 | def render_screen_image(deck, font_filename, text): 46 | image = PILHelper.create_screen_image(deck) 47 | # Load a custom TrueType font and use it to create an image 48 | draw = ImageDraw.Draw(image) 49 | font = ImageFont.truetype(font_filename, 20) 50 | draw.text((image.width / 2, image.height - 25), text=text, font=font, anchor="ms", fill="white") 51 | 52 | return PILHelper.to_native_screen_format(deck, image) 53 | 54 | 55 | # Returns styling information for a key based on its position and state. 56 | def get_key_style(deck, key, state): 57 | # Last button in the example application is the exit button. 58 | exit_key_index = deck.key_count() - 1 59 | 60 | if key == exit_key_index: 61 | name = "exit" 62 | icon = "{}.png".format("Exit") 63 | font = "Roboto-Regular.ttf" 64 | label = "Bye" if state else "Exit" 65 | else: 66 | name = "emoji" 67 | icon = "{}.png".format("Pressed" if state else "Released") 68 | font = "Roboto-Regular.ttf" 69 | label = "Pressed!" if state else "Key {}".format(key) 70 | 71 | return { 72 | "name": name, 73 | "icon": os.path.join(ASSETS_PATH, icon), 74 | "font": os.path.join(ASSETS_PATH, font), 75 | "label": label 76 | } 77 | 78 | 79 | # Creates a new key image based on the key index, style and current key state 80 | # and updates the image on the StreamDeck. 81 | def update_key_image(deck, key, state): 82 | # Determine what icon and label to use on the generated key. 83 | key_style = get_key_style(deck, key, state) 84 | 85 | # Generate the custom key with the requested image and label. 86 | image = render_key_image(deck, key_style["icon"], key_style["font"], key_style["label"]) 87 | 88 | # Use a scoped-with on the deck to ensure we're the only thread using it 89 | # right now. 90 | with deck: 91 | # Update requested key with the generated image. 92 | deck.set_key_image(key, image) 93 | 94 | 95 | # Prints key state change information, updates the key image and performs any 96 | # associated actions when a key is pressed. 97 | def key_change_callback(deck, key, state): 98 | # Print new key state 99 | print("Deck {} Key {} = {}".format(deck.id(), key, state), flush=True) 100 | 101 | # Don't try to set an image for touch buttons but set a random color 102 | if key >= deck.key_count(): 103 | set_random_touch_color(deck, key) 104 | return 105 | 106 | # Update the key image based on the new key state. 107 | update_key_image(deck, key, state) 108 | 109 | # Check if the key is changing to the pressed state. 110 | if state: 111 | key_style = get_key_style(deck, key, state) 112 | 113 | # When an exit button is pressed, close the application. 114 | if key_style["name"] == "exit": 115 | # Use a scoped-with on the deck to ensure we're the only thread 116 | # using it right now. 117 | with deck: 118 | # Reset deck, clearing all button images. 119 | deck.reset() 120 | 121 | # Close deck handle, terminating internal worker threads. 122 | deck.close() 123 | 124 | 125 | # Set a random color for the specified key 126 | def set_random_touch_color(deck, key): 127 | r = random.randint(0, 255) 128 | g = random.randint(0, 255) 129 | b = random.randint(0, 255) 130 | 131 | deck.set_key_color(key, r, g, b) 132 | 133 | 134 | if __name__ == "__main__": 135 | streamdecks = DeviceManager().enumerate() 136 | 137 | print("Found {} Stream Deck(s).\n".format(len(streamdecks))) 138 | 139 | for index, deck in enumerate(streamdecks): 140 | # This example only works with devices that have screens. 141 | if not deck.is_visual(): 142 | continue 143 | 144 | deck.open() 145 | deck.reset() 146 | 147 | print("Opened '{}' device (serial number: '{}', fw: '{}')".format( 148 | deck.deck_type(), deck.get_serial_number(), deck.get_firmware_version() 149 | )) 150 | 151 | # Set initial screen brightness to 30%. 152 | deck.set_brightness(30) 153 | 154 | # Set initial key images. 155 | for key in range(deck.key_count()): 156 | update_key_image(deck, key, False) 157 | 158 | # Register callback function for when a key state changes. 159 | deck.set_key_callback(key_change_callback) 160 | 161 | # Set a screen image 162 | image = render_screen_image(deck, os.path.join(ASSETS_PATH, "Roboto-Regular.ttf"), "Python StreamDeck") 163 | deck.set_screen_image(image) 164 | 165 | # Wait until all application threads have terminated (for this example, 166 | # this is when all deck handles are closed). 167 | for t in threading.enumerate(): 168 | try: 169 | t.join() 170 | except (TransportError, RuntimeError): 171 | pass 172 | -------------------------------------------------------------------------------- /src/example_pedal.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | # Python Stream Deck Library 4 | # Released under the MIT license 5 | # 6 | # dean [at] fourwalledcubicle [dot] com 7 | # www.fourwalledcubicle.com 8 | # 9 | 10 | # Example script showing basic library usage, printing button presses. This 11 | # example only shows key events, and is intended to demonstrate how to get 12 | # such events from device that lack screens, i.e. the StreamDeck Pedal. 13 | 14 | import threading 15 | 16 | from StreamDeck.DeviceManager import DeviceManager 17 | from StreamDeck.Transport.Transport import TransportError 18 | 19 | 20 | def key_change_callback(deck, key, state): 21 | print("Deck {} Key {} = {}".format(deck.id(), key, "down" if state else "up"), flush=True) 22 | 23 | 24 | if __name__ == "__main__": 25 | streamdecks = DeviceManager().enumerate() 26 | 27 | print("Found {} Stream Deck(s).\n".format(len(streamdecks))) 28 | 29 | for index, deck in enumerate(streamdecks): 30 | deck.open() 31 | 32 | print("Opened '{}' device (serial number: '{}', fw: '{}')".format( 33 | deck.deck_type(), deck.get_serial_number(), deck.get_firmware_version() 34 | )) 35 | 36 | # Register callback function for when a key state changes. 37 | deck.set_key_callback(key_change_callback) 38 | 39 | # Wait until all application threads have terminated (for this example, 40 | # this is when all deck handles are closed). 41 | for t in threading.enumerate(): 42 | try: 43 | t.join() 44 | except (TransportError, RuntimeError): 45 | pass 46 | -------------------------------------------------------------------------------- /src/example_plus.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | # Python Stream Deck Library 4 | # Released under the MIT license 5 | # 6 | # 7 | 8 | # Example script showing some Stream Deck + specific functions 9 | 10 | import os 11 | import threading 12 | import io 13 | 14 | from PIL import Image 15 | from StreamDeck.DeviceManager import DeviceManager 16 | from StreamDeck.Devices.StreamDeck import DialEventType, TouchscreenEventType 17 | from StreamDeck.Transport.Transport import TransportError 18 | 19 | # Folder location of image assets used by this example. 20 | ASSETS_PATH = os.path.join(os.path.dirname(__file__), "Assets") 21 | 22 | # image for idle state 23 | img = Image.new('RGB', (120, 120), color='black') 24 | released_icon = Image.open(os.path.join(ASSETS_PATH, 'Released.png')).resize((80, 80)) 25 | img.paste(released_icon, (20, 20), released_icon) 26 | 27 | img_byte_arr = io.BytesIO() 28 | img.save(img_byte_arr, format='JPEG') 29 | img_released_bytes = img_byte_arr.getvalue() 30 | 31 | # image for pressed state 32 | img = Image.new('RGB', (120, 120), color='black') 33 | pressed_icon = Image.open(os.path.join(ASSETS_PATH, 'Pressed.png')).resize((80, 80)) 34 | img.paste(pressed_icon, (20, 20), pressed_icon) 35 | 36 | img_byte_arr = io.BytesIO() 37 | img.save(img_byte_arr, format='JPEG') 38 | img_pressed_bytes = img_byte_arr.getvalue() 39 | 40 | 41 | # callback when buttons are pressed or released 42 | def key_change_callback(deck, key, key_state): 43 | print("Key: " + str(key) + " state: " + str(key_state)) 44 | 45 | deck.set_key_image(key, img_pressed_bytes if key_state else img_released_bytes) 46 | 47 | 48 | # callback when dials are pressed or released 49 | def dial_change_callback(deck, dial, event, value): 50 | if event == DialEventType.PUSH: 51 | print(f"dial pushed: {dial} state: {value}") 52 | if dial == 3 and value: 53 | deck.reset() 54 | deck.close() 55 | else: 56 | # build an image for the touch lcd 57 | img = Image.new('RGB', (800, 100), 'black') 58 | icon = Image.open(os.path.join(ASSETS_PATH, 'Exit.png')).resize((80, 80)) 59 | img.paste(icon, (690, 10), icon) 60 | 61 | for k in range(0, deck.DIAL_COUNT - 1): 62 | img.paste(pressed_icon if (dial == k and value) else released_icon, (30 + (k * 220), 10), 63 | pressed_icon if (dial == k and value) else released_icon) 64 | 65 | img_byte_arr = io.BytesIO() 66 | img.save(img_byte_arr, format='JPEG') 67 | img_byte_arr = img_byte_arr.getvalue() 68 | 69 | deck.set_touchscreen_image(img_byte_arr, 0, 0, 800, 100) 70 | elif event == DialEventType.TURN: 71 | print(f"dial {dial} turned: {value}") 72 | 73 | 74 | # callback when lcd is touched 75 | def touchscreen_event_callback(deck, evt_type, value): 76 | if evt_type == TouchscreenEventType.SHORT: 77 | print("Short touch @ " + str(value['x']) + "," + str(value['y'])) 78 | 79 | elif evt_type == TouchscreenEventType.LONG: 80 | 81 | print("Long touch @ " + str(value['x']) + "," + str(value['y'])) 82 | 83 | elif evt_type == TouchscreenEventType.DRAG: 84 | 85 | print("Drag started @ " + str(value['x']) + "," + str(value['y']) + " ended @ " + str(value['x_out']) + "," + str(value['y_out'])) 86 | 87 | 88 | if __name__ == "__main__": 89 | streamdecks = DeviceManager().enumerate() 90 | 91 | print("Found {} Stream Deck(s).\n".format(len(streamdecks))) 92 | 93 | for index, deck in enumerate(streamdecks): 94 | # This example only works with devices that have screens. 95 | 96 | if deck.DECK_TYPE != 'Stream Deck +': 97 | print(deck.DECK_TYPE) 98 | print("Sorry, this example only works with Stream Deck +") 99 | continue 100 | 101 | deck.open() 102 | deck.reset() 103 | 104 | deck.set_key_callback(key_change_callback) 105 | deck.set_dial_callback(dial_change_callback) 106 | deck.set_touchscreen_callback(touchscreen_event_callback) 107 | 108 | print("Opened '{}' device (serial number: '{}')".format(deck.deck_type(), deck.get_serial_number())) 109 | 110 | # Set initial screen brightness to 30%. 111 | deck.set_brightness(100) 112 | 113 | for key in range(0, deck.KEY_COUNT): 114 | deck.set_key_image(key, img_released_bytes) 115 | 116 | # build an image for the touch lcd 117 | img = Image.new('RGB', (800, 100), 'black') 118 | icon = Image.open(os.path.join(ASSETS_PATH, 'Exit.png')).resize((80, 80)) 119 | img.paste(icon, (690, 10), icon) 120 | 121 | for dial in range(0, deck.DIAL_COUNT - 1): 122 | img.paste(released_icon, (30 + (dial * 220), 10), released_icon) 123 | 124 | img_bytes = io.BytesIO() 125 | img.save(img_bytes, format='JPEG') 126 | touchscreen_image_bytes = img_bytes.getvalue() 127 | 128 | deck.set_touchscreen_image(touchscreen_image_bytes, 0, 0, 800, 100) 129 | 130 | # Wait until all application threads have terminated (for this example, 131 | # this is when all deck handles are closed). 132 | for t in threading.enumerate(): 133 | try: 134 | t.join() 135 | except (TransportError, RuntimeError): 136 | pass 137 | -------------------------------------------------------------------------------- /src/example_tileimage.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | # Python Stream Deck Library 4 | # Released under the MIT license 5 | # 6 | # dean [at] fourwalledcubicle [dot] com 7 | # www.fourwalledcubicle.com 8 | # 9 | 10 | # Example script showing how to tile a larger image across multiple buttons, by 11 | # first generating an image suitable for the entire deck, then cropping out and 12 | # applying key-sized tiles to individual keys of a StreamDeck. 13 | 14 | import os 15 | import threading 16 | 17 | from PIL import Image, ImageOps 18 | from StreamDeck.DeviceManager import DeviceManager 19 | from StreamDeck.ImageHelpers import PILHelper 20 | from StreamDeck.Transport.Transport import TransportError 21 | 22 | # Folder location of image assets used by this example. 23 | ASSETS_PATH = os.path.join(os.path.dirname(__file__), "Assets") 24 | 25 | 26 | # Generates an image that is correctly sized to fit across all keys of a given 27 | # StreamDeck. 28 | def create_full_deck_sized_image(deck, key_spacing, image_filename): 29 | key_rows, key_cols = deck.key_layout() 30 | key_width, key_height = deck.key_image_format()['size'] 31 | spacing_x, spacing_y = key_spacing 32 | 33 | # Compute total size of the full StreamDeck image, based on the number of 34 | # buttons along each axis. This doesn't take into account the spaces between 35 | # the buttons that are hidden by the bezel. 36 | key_width *= key_cols 37 | key_height *= key_rows 38 | 39 | # Compute the total number of extra non-visible pixels that are obscured by 40 | # the bezel of the StreamDeck. 41 | spacing_x *= key_cols - 1 42 | spacing_y *= key_rows - 1 43 | 44 | # Compute final full deck image size, based on the number of buttons and 45 | # obscured pixels. 46 | full_deck_image_size = (key_width + spacing_x, key_height + spacing_y) 47 | 48 | # Resize the image to suit the StreamDeck's full image size. We use the 49 | # helper function in Pillow's ImageOps module so that the image's aspect 50 | # ratio is preserved. 51 | image = Image.open(os.path.join(ASSETS_PATH, image_filename)).convert("RGBA") 52 | image = ImageOps.fit(image, full_deck_image_size, Image.LANCZOS) 53 | return image 54 | 55 | 56 | # Crops out a key-sized image from a larger deck-sized image, at the location 57 | # occupied by the given key index. 58 | def crop_key_image_from_deck_sized_image(deck, image, key_spacing, key): 59 | key_rows, key_cols = deck.key_layout() 60 | key_width, key_height = deck.key_image_format()['size'] 61 | spacing_x, spacing_y = key_spacing 62 | 63 | # Determine which row and column the requested key is located on. 64 | row = key // key_cols 65 | col = key % key_cols 66 | 67 | # Compute the starting X and Y offsets into the full size image that the 68 | # requested key should display. 69 | start_x = col * (key_width + spacing_x) 70 | start_y = row * (key_height + spacing_y) 71 | 72 | # Compute the region of the larger deck image that is occupied by the given 73 | # key, and crop out that segment of the full image. 74 | region = (start_x, start_y, start_x + key_width, start_y + key_height) 75 | segment = image.crop(region) 76 | 77 | # Create a new key-sized image, and paste in the cropped section of the 78 | # larger image. 79 | key_image = PILHelper.create_key_image(deck) 80 | key_image.paste(segment) 81 | 82 | return PILHelper.to_native_key_format(deck, key_image) 83 | 84 | 85 | # Closes the StreamDeck device on key state change. 86 | def key_change_callback(deck, key, state): 87 | # Use a scoped-with on the deck to ensure we're the only thread using it 88 | # right now. 89 | with deck: 90 | # Reset deck, clearing all button images. 91 | deck.reset() 92 | 93 | # Close deck handle, terminating internal worker threads. 94 | deck.close() 95 | 96 | 97 | if __name__ == "__main__": 98 | streamdecks = DeviceManager().enumerate() 99 | 100 | print("Found {} Stream Deck(s).\n".format(len(streamdecks))) 101 | 102 | for index, deck in enumerate(streamdecks): 103 | # This example only works with devices that have screens. 104 | if not deck.is_visual(): 105 | continue 106 | 107 | deck.open() 108 | deck.reset() 109 | 110 | print("Opened '{}' device (serial number: '{}')".format(deck.deck_type(), deck.get_serial_number())) 111 | 112 | # Set initial screen brightness to 30%. 113 | deck.set_brightness(30) 114 | 115 | # Approximate number of (non-visible) pixels between each key, so we can 116 | # take those into account when cutting up the image to show on the keys. 117 | key_spacing = (36, 36) 118 | 119 | # Load and resize a source image so that it will fill the given 120 | # StreamDeck. 121 | image = create_full_deck_sized_image(deck, key_spacing, "Harold.jpg") 122 | 123 | print("Created full deck image size of {}x{} pixels.".format(image.width, image.height)) 124 | 125 | # Extract out the section of the image that is occupied by each key. 126 | key_images = dict() 127 | for k in range(deck.key_count()): 128 | key_images[k] = crop_key_image_from_deck_sized_image(deck, image, key_spacing, k) 129 | 130 | # Use a scoped-with on the deck to ensure we're the only thread 131 | # using it right now. 132 | with deck: 133 | # Draw the individual key images to each of the keys. 134 | for k in range(deck.key_count()): 135 | key_image = key_images[k] 136 | 137 | # Show the section of the main image onto the key. 138 | deck.set_key_image(k, key_image) 139 | 140 | # Register callback function for when a key state changes. 141 | deck.set_key_callback(key_change_callback) 142 | 143 | # Wait until all application threads have terminated (for this example, 144 | # this is when all deck handles are closed). 145 | for t in threading.enumerate(): 146 | try: 147 | t.join() 148 | except (TransportError, RuntimeError): 149 | pass 150 | -------------------------------------------------------------------------------- /test/test.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | # Python Stream Deck Library 4 | # Released under the MIT license 5 | # 6 | # dean [at] fourwalledcubicle [dot] com 7 | # www.fourwalledcubicle.com 8 | # 9 | 10 | import argparse 11 | import logging 12 | import os 13 | import sys 14 | 15 | sys.path.insert(0, os.path.join(os.path.dirname(__file__), '../src')) 16 | 17 | from StreamDeck.DeviceManager import DeviceManager 18 | from StreamDeck.ImageHelpers import PILHelper 19 | from PIL import ImageDraw 20 | 21 | 22 | def test_pil_helpers(deck): 23 | if not deck.is_visual(): 24 | return 25 | 26 | test_key_image_pil = PILHelper.create_key_image(deck) 27 | test_scaled_key_image_pil = PILHelper.create_scaled_key_image(deck, test_key_image_pil) # noqa: F841 28 | test_key_image_native = PILHelper.to_native_key_format(deck, test_scaled_key_image_pil) # noqa: F841 29 | 30 | if deck.is_touch(): 31 | test_touchscreen_image_pil = PILHelper.create_touchscreen_image(deck) 32 | test_scaled_touchscreen_image_pil = PILHelper.create_scaled_touchscreen_image(deck, test_touchscreen_image_pil) # noqa: F841 33 | test_touchscreen_image_native = PILHelper.to_native_touchscreen_format(deck, test_scaled_touchscreen_image_pil) # noqa: F841 34 | 35 | 36 | def test_basic_apis(deck): 37 | with deck: 38 | deck.open() 39 | 40 | connected = deck.connected() # noqa: F841 41 | deck_id = deck.id() # noqa: F841 42 | key_count = deck.key_count() # noqa: F841 43 | vendor_id = deck.vendor_id() # noqa: F841 44 | product_id = deck.product_id() # noqa: F841 45 | deck_type = deck.deck_type() # noqa: F841 46 | key_layout = deck.key_layout() # noqa: F841 47 | key_image_format = deck.key_image_format() if deck.is_visual() else None # noqa: F841 48 | key_states = deck.key_states() # noqa: F841 49 | dial_states = deck.dial_states() # noqa: F841 50 | touchscreen_image_format = deck.touchscreen_image_format() if deck.is_touch() else None # noqa: F841 51 | 52 | deck.set_key_callback(None) 53 | deck.reset() 54 | 55 | if deck.is_visual(): 56 | deck.set_brightness(30) 57 | 58 | test_key_image_pil = PILHelper.create_key_image(deck) 59 | test_key_image_native = PILHelper.to_native_key_format(deck, test_key_image_pil) 60 | deck.set_key_image(0, None) 61 | deck.set_key_image(0, test_key_image_native) 62 | 63 | if deck.is_touch(): 64 | test_touchscreen_image_pil = PILHelper.create_touchscreen_image(deck) 65 | test_touchscreen_image_native = PILHelper.to_native_touchscreen_format(deck, test_touchscreen_image_pil) 66 | deck.set_touchscreen_image(None) 67 | deck.set_touchscreen_image(test_touchscreen_image_native, 0, 0, test_touchscreen_image_pil.width, test_touchscreen_image_pil.height) 68 | 69 | deck.close() 70 | 71 | 72 | def test_key_pattern(deck): 73 | if not deck.is_visual(): 74 | return 75 | 76 | test_key_image = PILHelper.create_key_image(deck) 77 | 78 | draw = ImageDraw.Draw(test_key_image) 79 | draw.rectangle((0, 0) + test_key_image.size, fill=(0x11, 0x22, 0x33), outline=(0x44, 0x55, 0x66)) 80 | 81 | test_key_image = PILHelper.to_native_key_format(deck, test_key_image) 82 | 83 | with deck: 84 | deck.open() 85 | deck.set_key_image(0, test_key_image) 86 | deck.close() 87 | 88 | 89 | if __name__ == "__main__": 90 | logging.basicConfig(level=logging.INFO) 91 | 92 | parser = argparse.ArgumentParser(description="StreamDeck Library test.") 93 | parser.add_argument("--model", help="Stream Deck model name to test") 94 | parser.add_argument("--test", help="Stream Deck test to run") 95 | args = parser.parse_args() 96 | 97 | manager = DeviceManager(transport="dummy") 98 | streamdecks = manager.enumerate() 99 | 100 | test_streamdecks = streamdecks 101 | if args.model: 102 | test_streamdecks = [deck for deck in test_streamdecks if deck.deck_type() == args.model] 103 | 104 | if len(test_streamdecks) == 0: 105 | logging.error("Error: No Stream Decks to test. Known models: {}".format([d.deck_type() for d in streamdecks])) 106 | sys.exit(1) 107 | 108 | tests = { 109 | "PIL Helpers": test_pil_helpers, 110 | "Basic APIs": test_basic_apis, 111 | "Key Pattern": test_key_pattern, 112 | } 113 | 114 | test_runners = tests 115 | if args.test: 116 | test_runners = {name: test for (name, test) in test_runners.items() if name == args.test} 117 | 118 | if len(test_runners) == 0: 119 | logging.error("Error: No Stream Decks tests to run. Known tests: {}".format([name for name, test in tests.items()])) 120 | sys.exit(1) 121 | 122 | for deck_index, deck in enumerate(test_streamdecks): 123 | logging.info("Using Deck Type: {}".format(deck.deck_type())) 124 | 125 | for name, test in test_runners.items(): 126 | logging.info("Running Test: {}".format(name)) 127 | test(deck) 128 | logging.info("Finished Test: {}".format(name)) 129 | --------------------------------------------------------------------------------