@ -0,0 +1,21 @@ | |||
The MIT License (MIT) | |||
Copyright (c) 2020 Lars Hoogestraat <inbox-kodi@hoogi.eu> (https://hoogi.eu) | |||
Permission is hereby granted, free of charge, to any person obtaining a copy | |||
of this software and associated documentation files (the "Software"), to deal | |||
in the Software without restriction, including without limitation the rights | |||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell | |||
copies of the Software, and to permit persons to whom the Software is | |||
furnished to do so, subject to the following conditions: | |||
The above copyright notice and this permission notice shall be included in | |||
all copies or substantial portions of the Software. | |||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR | |||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, | |||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE | |||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER | |||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, | |||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN | |||
THE SOFTWARE. |
@ -0,0 +1,74 @@ | |||
## Description | |||
Bias lighting for your Kodi media center using Pimoroni Mote lights. | |||
## How to install? | |||
**Requirements** | |||
* Pimoroni Mote ([https://shop.pimoroni.com/products/mote)](https://shop.pimoroni.com/products/mote) | |||
* Tape | |||
* Kodi Leia 18.x version supported | |||
I'm just starting with some Kodi and Python development, there is no repository available. Just copy the zip to some folder | |||
accessible by the user running kodi and choose to install from zip file. You have to enable unknown sources (Settings -> System -> Addons) | |||
The service will run in the background. | |||
The add-on integrates the Python Mote library from Pimoroni: [https://github.com/pimoroni/mote](https://github.com/pimoroni/mote) | |||
## How to configure? | |||
The configuration can be done in the Kodi add-on setting page. | |||
You can configure the position of your Mote LED stripes like the following: | |||
``` | |||
TOP LEFT TOP MIDDLE TOP RIGHT | |||
---------------- ---------------- ------------- | |||
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - | |||
SIDE | | | | SIDE | |||
LEFT | | | | RIGHT | |||
TOP | | | | TOP | |||
| | | |||
| | | |||
| | | |||
SIDE | | | | SIDE | |||
LEFT | | | | RIGHT | |||
MIDDLE | | | | MIDDLE | |||
| | | |||
| | | |||
| | | |||
SIDE | | | | SIDE | |||
LEFT | | | | RIGHT | |||
BOTTOM | | | | BOTTOM | |||
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - | |||
---------------- ---------------- ------------- | |||
BOTTOM LEFT BOTTOM MIDDLE BOTTOM RIGHT | |||
``` | |||
Inverting the output is also supported, you don't have to pay attention to correct direction. | |||
For setting up the correct positions download the test_color.mp4 video and play/pause it in Kodi while configuring your Mote. | |||
You must deactivate / activate add-on to apply settings. | |||
## Todos | |||
* If capture is switching colors fast the lightning is noisy | |||
* Something like a color picker for custom colors? | |||
## Bugs, patches and feedback always welcome. | |||
* Without registration: inbox-kodi@hoogi.eu | |||
## Credits | |||
* Pimoroni Mote Python Library [https://github.com/pimoroni/mote](https://github.com/pimoroni/mote) | |||
* resources/fanart.jpg and resources/icon.png | |||
* Stephan Legachev ([https://commons.wikimedia.org/wiki/File:Ambilight-2.jpg](https://commons.wikimedia.org/wiki/File:Ambilight-2.jpg)), „Ambilight-2“, [https://creativecommons.org/licenses/by/3.0/legalcode](https://creativecommons.org/licenses/by/3.0/legalcode) |
@ -0,0 +1,41 @@ | |||
================================================================================ | |||
Pimoroni Mote | |||
Github: | |||
https://github.com/pimoroni/mote | |||
================================================================================ | |||
MIT License | |||
Copyright (c) 2017 Pimoroni Ltd. | |||
Permission is hereby granted, free of charge, to any person obtaining a copy | |||
of this software and associated documentation files (the "Software"), to deal | |||
in the Software without restriction, including without limitation the rights | |||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell | |||
copies of the Software, and to permit persons to whom the Software is | |||
furnished to do so, subject to the following conditions: | |||
The above copyright notice and this permission notice shall be included in all | |||
copies or substantial portions of the Software. | |||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR | |||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, | |||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE | |||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER | |||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, | |||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE | |||
SOFTWARE. | |||
================================================================================ | |||
Stephan Legachev | |||
- resources/fanart.jpg | |||
- resources/icong.png | |||
Wikimedia: | |||
https://commons.wikimedia.org/wiki/File:Ambilight-2.jpg | |||
================================================================================ | |||
This file is licensed under the Creative Commons Attribution 3.0 Unported license. | |||
https://creativecommons.org/licenses/by/3.0/legalcode |
@ -0,0 +1,307 @@ | |||
# -*- coding: utf-8 -*- | |||
import xbmc | |||
import xbmcaddon | |||
from PIL import Image | |||
from resources.lib.mote import Mote | |||
NUM_PIXEL = 16 | |||
colors = [ | |||
(0, 0, 0), # None | |||
(255, 0, 0), # Red | |||
(0, 255, 0), # Green | |||
(0, 0, 255), # Blue | |||
(255, 255, 0), # Yellow | |||
(255, 255, 255) # White | |||
] | |||
mote = Mote() | |||
mote.configure_channel(1, NUM_PIXEL, True) | |||
mote.configure_channel(2, NUM_PIXEL, True) | |||
mote.configure_channel(3, NUM_PIXEL, True) | |||
mote.configure_channel(4, NUM_PIXEL, True) | |||
mote.set_clear_on_exit() | |||
class Position(): | |||
top = 1 | |||
bottom = 2 | |||
left = 3 | |||
right = 4 | |||
def set_static_color(channels, index, r, g, b, brightness=None): | |||
for k, v in channels.items(): | |||
mote.set_pixel(v.get("channel"), index, r, g, b, brightness) | |||
def clear_mote(): | |||
mote.clear() | |||
mote.show() | |||
monitor = xbmc.Monitor() | |||
width, height = 32, 32 | |||
settings = xbmcaddon.Addon(id='script.service.biaslightmote') | |||
mote.clear() | |||
mote1 = settings.getSetting("mote1") | |||
mote1_pos = int(settings.getSetting("mote1_position")) | |||
mote1_inv = settings.getSetting("mote1_invert") | |||
mote2 = settings.getSetting("mote2") | |||
mote2_pos = int(settings.getSetting("mote2_position")) | |||
mote2_inv = settings.getSetting("mote2_invert") | |||
mote3 = settings.getSetting("mote3") | |||
mote3_pos = int(settings.getSetting("mote3_position")) | |||
mote3_inv = settings.getSetting("mote3_invert") | |||
mote4 = settings.getSetting("mote4") | |||
mote4_pos = int(settings.getSetting("mote4_position")) | |||
mote4_inv = settings.getSetting("mote4_invert") | |||
mode = int(settings.getSetting("mode")) | |||
color = int(settings.getSetting("color")) | |||
custom_color_red = int(settings.getSetting("color_custom_red")) | |||
custom_color_green = int(settings.getSetting("color_custom_green")) | |||
custom_color_blue = int(settings.getSetting("color_custom_blue")) | |||
colors.append((custom_color_red, custom_color_green, custom_color_blue)) | |||
brightness = float(float(settings.getSetting("brightness")) / float(100)) | |||
channels = {} | |||
if mote1 == "true": | |||
channels.update({"1": {"channel": 1, "invert": mote1_inv}}) | |||
# TOP LEFT | |||
if mote1_pos == 0: | |||
channels.get("1").update({"direction": Position.top, "start": 0, "stop": 16}) | |||
# TOP MIDDLE | |||
elif mote1_pos == 1: | |||
channels.get("1").update({"direction": Position.top, "start": 8, "stop": 24}) | |||
# TOP RIGHT | |||
elif mote1_pos == 2: | |||
channels.get("1").update({"direction": Position.top, "start": 16, "stop": 32}) | |||
# SIDE LEFT TOP | |||
elif mote1_pos == 3: | |||
channels.get("1").update({"direction": Position.left, "start": 0, "stop": 16}) | |||
# SIDE LEFT MIDDLE | |||
elif mote1_pos == 4: | |||
channels.get("1").update({"direction": Position.left, "start": 8, "stop": 24}) | |||
# SIDE LEFT BOTTOM | |||
elif mote1_pos == 5: | |||
channels.get("1").update({"direction": Position.left, "start": 16, "stop": 32}) | |||
# SIDE RIGHT TOP | |||
elif mote1_pos == 6: | |||
channels.get("1").update({"direction": Position.right, "start": 0, "stop": 16}) | |||
# SIDE RIGHT MIDDLE | |||
elif mote1_pos == 7: | |||
channels.get("1").update({"direction": Position.right, "start": 8, "stop": 24}) | |||
# SIDE RIGHT BOTTOM | |||
elif mote1_pos == 8: | |||
channels.get("1").update({"direction": Position.right, "start": 16, "stop": 32}) | |||
# BOTTOM LEFT | |||
elif mote1_pos == 9: | |||
channels.get("1").update({"direction": Position.bottom, "start": 0, "stop": 16}) | |||
# BOTTOM MIDDLE | |||
elif mote1_pos == 10: | |||
channels.get("1").update({"direction": Position.bottom, "start": 8, "stop": 24}) | |||
# BOTTOM RIGHT | |||
elif mote1_pos == 11: | |||
channels.get("1").update({"direction": Position.bottom, "start": 16, "stop": 32}) | |||
if mote2 == "true": | |||
channels.update({"2": {"channel": 2, "invert": mote2_inv}}) | |||
# TOP LEFT | |||
if mote2_pos == 0: | |||
channels.get("2").update({"direction": Position.top, "start": 0, "stop": 16}) | |||
# TOP MIDDLE | |||
elif mote2_pos == 1: | |||
channels.get("2").update({"direction": Position.top, "start": 8, "stop": 24}) | |||
# TOP RIGHT | |||
elif mote2_pos == 2: | |||
channels.get("2").update({"direction": Position.top, "start": 16, "stop": 32}) | |||
# SIDE LEFT TOP | |||
elif mote2_pos == 3: | |||
channels.get("2").update({"direction": Position.left, "start": 0, "stop": 16}) | |||
# SIDE LEFT MIDDLE | |||
elif mote2_pos == 4: | |||
channels.get("2").update({"direction": Position.left, "start": 8, "stop": 24}) | |||
# SIDE LEFT BOTTOM | |||
elif mote2_pos == 5: | |||
channels.get("2").update({"direction": Position.left, "start": 16, "stop": 32}) | |||
# SIDE RIGHT TOP | |||
elif mote2_pos == 6: | |||
channels.get("2").update({"direction": Position.right, "start": 0, "stop": 16}) | |||
# SIDE RIGHT MIDDLE | |||
elif mote2_pos == 7: | |||
channels.get("2").update({"direction": Position.right, "start": 8, "stop": 24}) | |||
# SIDE RIGHT BOTTOM | |||
elif mote2_pos == 8: | |||
channels.get("2").update({"direction": Position.right, "start": 16, "stop": 32}) | |||
# BOTTOM LEFT | |||
elif mote2_pos == 9: | |||
channels.get("2").update({"direction": Position.bottom, "start": 0, "stop": 16}) | |||
# BOTTOM MIDDLE | |||
elif mote2_pos == 10: | |||
channels.get("2").update({"direction": Position.bottom, "start": 8, "stop": 24}) | |||
# BOTTOM RIGHT | |||
elif mote2_pos == 11: | |||
channels.get("2").update({"direction": Position.bottom, "start": 16, "stop": 32}) | |||
if mote3 == "true": | |||
channels.update({"3": {"channel": 3, "invert": mote3_inv}}) | |||
# TOP LEFT | |||
if mote3_pos == 0: | |||
channels.get("3").update({"direction": Position.top, "start": 0, "stop": 16}) | |||
# TOP MIDDLE | |||
elif mote3_pos == 1: | |||
channels.get("3").update({"direction": Position.top, "start": 8, "stop": 24}) | |||
# TOP RIGHT | |||
elif mote3_pos == 2: | |||
channels.get("3").update({"direction": Position.top, "start": 16, "stop": 32}) | |||
# SIDE LEFT TOP | |||
elif mote3_pos == 3: | |||
channels.get("3").update({"direction": Position.left, "start": 0, "stop": 16}) | |||
# SIDE LEFT MIDDLE | |||
elif mote3_pos == 4: | |||
channels.get("3").update({"direction": Position.left, "start": 8, "stop": 24}) | |||
# SIDE LEFT BOTTOM | |||
elif mote3_pos == 5: | |||
channels.get("3").update({"direction": Position.left, "start": 16, "stop": 32}) | |||
# SIDE RIGHT TOP | |||
elif mote3_pos == 6: | |||
channels.get("3").update({"direction": Position.right, "start": 0, "stop": 16}) | |||
# SIDE RIGHT MIDDLE | |||
elif mote3_pos == 7: | |||
channels.get("3").update({"direction": Position.right, "start": 8, "stop": 24}) | |||
# SIDE RIGHT BOTTOM | |||
elif mote3_pos == 8: | |||
channels.get("3").update({"direction": Position.right, "start": 16, "stop": 32}) | |||
# BOTTOM LEFT | |||
elif mote3_pos == 9: | |||
channels.get("3").update({"direction": Position.bottom, "start": 0, "stop": 16}) | |||
# BOTTOM MIDDLE | |||
elif mote3_pos == 10: | |||
channels.get("3").update({"direction": Position.bottom, "start": 8, "stop": 24}) | |||
# BOTTOM RIGHT | |||
elif mote3_pos == 11: | |||
channels.get("3").update({"direction": Position.bottom, "start": 16, "stop": 32}) | |||
if mote4 == "true": | |||
channels.update({"4": {"channel": 4, "invert": mote4_inv}}) | |||
# TOP LEFT | |||
if mote4_pos == 0: | |||
channels.get("4").update({"direction": Position.top, "start": 0, "stop": 16}) | |||
# TOP MIDDLE | |||
elif mote4_pos == 1: | |||
channels.get("4").update({"direction": Position.top, "start": 8, "stop": 24}) | |||
# TOP RIGHT | |||
elif mote4_pos == 2: | |||
channels.get("4").update({"direction": Position.top, "start": 16, "stop": 32}) | |||
# SIDE LEFT TOP | |||
elif mote4_pos == 3: | |||
channels.get("4").update({"direction": Position.left, "start": 0, "stop": 16}) | |||
# SIDE LEFT MIDDLE | |||
elif mote4_pos == 4: | |||
channels.get("4").update({"direction": Position.left, "start": 8, "stop": 24}) | |||
# SIDE LEFT BOTTOM | |||
elif mote4_pos == 5: | |||
channels.get("4").update({"direction": Position.left, "start": 16, "stop": 32}) | |||
# SIDE RIGHT TOP | |||
elif mote4_pos == 6: | |||
channels.get("4").update({"direction": Position.right, "start": 0, "stop": 16}) | |||
# SIDE RIGHT MIDDLE | |||
elif mote4_pos == 7: | |||
channels.get("4").update({"direction": Position.right, "start": 8, "stop": 24}) | |||
# SIDE RIGHT BOTTOM | |||
elif mote4_pos == 8: | |||
channels.get("4").update({"direction": Position.right, "start": 16, "stop": 32}) | |||
# BOTTOM LEFT | |||
elif mote4_pos == 9: | |||
channels.get("4").update({"direction": Position.bottom, "start": 0, "stop": 16}) | |||
# BOTTOM MIDDLE | |||
elif mote4_pos == 10: | |||
channels.get("4").update({"direction": Position.bottom, "start": 8, "stop": 24}) | |||
# BOTTOM RIGHT | |||
elif mote4_pos == 11: | |||
channels.get("4").update({"direction": Position.bottom, "start": 16, "stop": 32}) | |||
if __name__ == '__main__': | |||
monitor = xbmc.Monitor() | |||
if mode == 0: | |||
if color == 0: | |||
clear_mote() | |||
for c in channels.items(): | |||
for pixel in range(NUM_PIXEL): | |||
set_static_color(channels, pixel, colors[color][0], colors[color][1], colors[color][2], brightness) | |||
mote.show() | |||
elif mode == 1: | |||
while not monitor.abortRequested(): | |||
if monitor.waitForAbort(0.6): | |||
break | |||
capture = xbmc.RenderCapture() | |||
capture.capture(width, height) | |||
pixels = capture.getImage(1000) | |||
if not pixels: | |||
if color == 0: | |||
clear_mote() | |||
else: | |||
for pixel in range(NUM_PIXEL): | |||
set_static_color(channels, pixel, colors[color][0], colors[color][1], colors[color][2], brightness) | |||
mote.show() | |||
else: | |||
image = Image.frombytes("RGBA", (width, height), str(pixels), "raw", "BGRA") | |||
for k, v in channels.items(): | |||
channel = v["channel"] | |||
invert = v["invert"] | |||
inv_index = 0 | |||
if invert == "true": | |||
inv_index = 15 | |||
direction = v["direction"] | |||
start = v["start"] | |||
stop = v["stop"] | |||
if direction == Position.top: | |||
index = 0 | |||
while start < stop: | |||
pixel = image.getpixel((0, start)) | |||
mote.set_pixel(channel, abs(index-inv_index), pixel[0], pixel[1], pixel[2], brightness) | |||
start += 1 | |||
index += 1 | |||
if direction == Position.left: | |||
index = 0 | |||
while start < stop: | |||
pixel = image.getpixel((start, 0)) | |||
mote.set_pixel(channel, abs(index-inv_index), pixel[0], pixel[1], pixel[2], brightness) | |||
start += 1 | |||
index += 1 | |||
if direction == Position.right: | |||
index = 0 | |||
while start < stop: | |||
pixel = image.getpixel((width-1, start)) | |||
mote.set_pixel(channel, abs(index-inv_index), pixel[0], pixel[1], pixel[2], brightness) | |||
start += 1 | |||
index += 1 | |||
if direction == Position.bottom: | |||
index = 0 | |||
while start < stop: | |||
pixel = image.getpixel((start, height-1)) | |||
mote.set_pixel(channel, abs(index-inv_index), pixel[0], pixel[1], pixel[2], brightness) | |||
start += 1 | |||
index += 1 | |||
mote.show() | |||
clear_mote() |
@ -0,0 +1,28 @@ | |||
<?xml version="1.0" encoding="UTF-8" standalone="yes"?> | |||
<addon id="script.service.biaslightmote" name="Bias lighting for Pimoroni Mote" version="0.1.0" provider-name="snafu"> | |||
<requires> | |||
<import addon="xbmc.python" version="2.26.0"/> | |||
<import addon="script.module.pyserial"/> | |||
<import addon="script.module.pil"/> | |||
</requires> | |||
<extension point="xbmc.service" library="addon.py" /> | |||
<extension point="xbmc.addon.metadata"> | |||
<summary lang="en_GB">Bias lighting for your Kodi media center using Pimoroni Mote lights</summary> | |||
<description lang="en_GB"> | |||
Bias lighting for your Kodi media center using Pimoroni Mote lights. | |||
Picture credit: | |||
Stephan Legachev (https://commons.wikimedia.org/wiki/File:Ambilight-2.jpg), „Ambilight-2“, https://creativecommons.org/licenses/by/3.0/legalcode | |||
</description> | |||
<platform>all</platform> | |||
<license>MIT</license> | |||
<website>https://hoogi.eu</website> | |||
<email>inbox-kodi@hoogi.eu</email> | |||
<source>https://git.hoogi.eu/snafu/script.service.biaslightmote</source> | |||
<news>v0.1.0 | |||
- initial release</news> | |||
<assets> | |||
<icon>resources/icon.png</icon> | |||
<fanart>resources/fanart.jpg</fanart> | |||
</assets> | |||
</extension> | |||
</addon> |
@ -0,0 +1 @@ | |||
This folder will be the home of your translations |
@ -0,0 +1,162 @@ | |||
# Kodi Media Center language file | |||
# Addon Name: Pimote | |||
# Addon id: script.service.pimote | |||
# Addon Provider: snafu | |||
msgid "" | |||
msgstr "" | |||
"Project-Id-Version: XBMC Addons\n" | |||
"Report-Msgid-Bugs-To: alanwww1@xbmc.org\n" | |||
"POT-Creation-Date: YEAR-MO-DA HO:MI+ZONE\n" | |||
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" | |||
"Last-Translator: Kodi Translation Team\n" | |||
"Language-Team: English (http://www.transifex.com/projects/p/xbmc-addons/language/en/)\n" | |||
"MIME-Version: 1.0\n" | |||
"Content-Type: text/plain; charset=UTF-8\n" | |||
"Content-Transfer-Encoding: 8bit\n" | |||
"Language: en\n" | |||
"Plural-Forms: nplurals=2; plural=(n != 1);\n" | |||
msgctxt "#30000" | |||
msgid "Mote general" | |||
msgstr "" | |||
msgctxt "#30201" | |||
msgid "Activate Mote 1 Channel" | |||
msgstr "" | |||
msgctxt "#30202" | |||
msgid "Activate Mote 2 Channel" | |||
msgstr "" | |||
msgctxt "#30203" | |||
msgid "Activate Mote 3 Channel" | |||
msgstr "" | |||
msgctxt "#30204" | |||
msgid "Activate Mote 4 Channel" | |||
msgstr "" | |||
msgctxt "#30210" | |||
msgid "Position" | |||
msgstr "" | |||
msgctxt "#30211" | |||
msgid "Top Left" | |||
msgstr "" | |||
msgctxt "#30212" | |||
msgid "Top Middle" | |||
msgstr "" | |||
msgctxt "#30213" | |||
msgid "Top Right" | |||
msgstr "" | |||
msgctxt "#30214" | |||
msgid "Side Left Top" | |||
msgstr "" | |||
msgctxt "#30215" | |||
msgid "Side Left Middle" | |||
msgstr "" | |||
msgctxt "#30216" | |||
msgid "Side Left Bottom" | |||
msgstr "" | |||
msgctxt "#30217" | |||
msgid "Side Right Top" | |||
msgstr "" | |||
msgctxt "#30218" | |||
msgid "Side Right Middle" | |||
msgstr "" | |||
msgctxt "#30219" | |||
msgid "Side Right Bottom" | |||
msgstr "" | |||
msgctxt "#30220" | |||
msgid "Bottom Left" | |||
msgstr "" | |||
msgctxt "#30221" | |||
msgid "Bottom Middle" | |||
msgstr "" | |||
msgctxt "#30222" | |||
msgid "Bottom Right" | |||
msgstr "" | |||
msgctxt "#30230" | |||
msgid "Invert" | |||
msgstr "" | |||
msgctxt "#30900" | |||
msgid "Clear Mote" | |||
msgstr "" | |||
msgctxt "#31000" | |||
msgid "Mote colors" | |||
msgstr "" | |||
msgctxt "#31100" | |||
msgid "Mode" | |||
msgstr "" | |||
msgctxt "#31101" | |||
msgid "Static color" | |||
msgstr "" | |||
msgctxt "#31102" | |||
msgid "Bias lighting" | |||
msgstr "" | |||
msgctxt "#31110" | |||
msgid "Color (static or when capture is not available)" | |||
msgstr "" | |||
msgctxt "#31111" | |||
msgid "Red" | |||
msgstr "" | |||
msgctxt "#31112" | |||
msgid "Green" | |||
msgstr "" | |||
msgctxt "#31113" | |||
msgid "Blue" | |||
msgstr "" | |||
msgctxt "#31114" | |||
msgid "Yellow" | |||
msgstr "" | |||
msgctxt "#31115" | |||
msgid "White" | |||
msgstr "" | |||
msgctxt "#31116" | |||
msgid "Custom" | |||
msgstr "" | |||
msgctxt "#31117" | |||
msgid "None" | |||
msgstr "" | |||
msgctxt "#31121" | |||
msgid "Red" | |||
msgstr "" | |||
msgctxt "#31122" | |||
msgid "Green" | |||
msgstr "" | |||
msgctxt "#31123" | |||
msgid "Blue" | |||
msgstr "" | |||
msgctxt "#31130" | |||
msgid "Brightness" | |||
msgstr "" |
@ -0,0 +1,295 @@ | |||
import time | |||
from sys import exit | |||
try: | |||
import serial | |||
import serial.tools.list_ports | |||
except ImportError: | |||
print("This library requires the serial module\nInstall with: sudo pip install pyserial") | |||
__version__ = '0.0.4' | |||
VID = 0x16d0 | |||
PID = 0x08c4 | |||
NAME = 'Mote USB Dock' | |||
MAX_PIXELS = 512 | |||
MAX_PIXELS_PER_CHANNEL = int(MAX_PIXELS / 4) | |||
class Mote: | |||
"""Represents a connected Mote device, communicating over USB serial. | |||
The Mote class allows you to configure the 4 channels and set individual pixels. | |||
It will attach to the first available Mote device unless `port_name` is specified at init. | |||
:param port_name: Override auto-detect and specify an explicit port to use. Must be a complete path ie: /dev/tty.usbmodem1234 (default None) | |||
""" | |||
def __init__(self, port_name=None, white_point=(1.0, 1.0, 1.0)): | |||
self.port_name = port_name | |||
self._channel_count = 4 | |||
self._channels = [None] * self._channel_count | |||
self._channel_flags = [0] * self._channel_count | |||
self._clear_on_exit = False | |||
self.white_point = white_point | |||
if self.port_name is None: | |||
self.port_name = self._find_serial_port(VID, PID, NAME) | |||
if self.port_name is None: | |||
raise IOError("Unable to find Mote device") | |||
self.port = serial.Serial(self.port_name, 115200, timeout=1) | |||
def _find_serial_port(self, vid, pid, name): | |||
"""Find a serial port by VID, PID and text name | |||
:param vid: USB vendor ID to locate | |||
:param pid: USB product ID to locate | |||
:param name: USB device name to find where VID/PID match fails | |||
""" | |||
check_for = "USB VID:PID={vid:04x}:{pid:04x}".format(vid=vid, pid=pid).upper() | |||
ports = serial.tools.list_ports.comports() | |||
for check_port in ports: | |||
if hasattr(serial.tools, 'list_ports_common'): | |||
if (check_port.vid, check_port.pid) == (VID, PID): | |||
return check_port.device | |||
continue | |||
if check_for in check_port[2].upper() or name == check_port[1]: | |||
return check_port[0] | |||
return None | |||
def configure_channel(self, channel, num_pixels, gamma_correction=False): | |||
"""Configure a channel | |||
:param channel: Channel, either 1, 2, 3 or 4 corresponding to numbers on Mote | |||
:param num_pixels: Number of pixels to configure for this channel | |||
:param gamma_correction: Whether to enable gamma correction (default False) | |||
""" | |||
if channel > self._channel_count or channel < 1: | |||
raise ValueError("Channel index must be between 1 and {}".format(self._channel_count)) | |||
if num_pixels > MAX_PIXELS_PER_CHANNEL: | |||
raise ValueError("Number of pixels can not be more than {max}".format(max=MAX_PIXELS_PER_CHANNEL)) | |||
self._channels[channel - 1] = [(0, 0, 0, 1.0)] * num_pixels | |||
self._channel_flags[channel - 1] = 1 if gamma_correction else 0 | |||
buf = bytearray() | |||
buf.append(channel) | |||
buf.append(num_pixels) | |||
buf.append(self._channel_flags[channel - 1]) | |||
self.port.write(b'mote') | |||
self.port.write(b'c') | |||
self.port.write(buf) | |||
def get_pixel_count(self, channel): | |||
"""Get the number of pixels a channel is configured to use | |||
:param channel: Channel, either 1, 2 3 or 4 corresponding to numbers on Mote | |||
""" | |||
if channel > self._channel_count or channel < 1: | |||
raise ValueError("Channel index must be between 1 and 4") | |||
if self._channels[channel - 1] is None: | |||
raise ValueError("Channel {channel} has not been set up".format(channel=channel)) | |||
return len(self._channels[channel - 1]) | |||
def get_pixel(self, channel, index): | |||
"""Get the RGB colour of a single pixel, on a single channel | |||
:param channel: Channel, either 1, 2, 3 or 4 corresponding to numbers on Mote | |||
:param index: Index of pixel to set, from 0 up | |||
""" | |||
if channel > self._channel_count or channel < 1: | |||
raise ValueError("Channel index must be between 1 and 4") | |||
if self._channels[channel - 1] is None: | |||
raise ValueError("Please set up channel {channel} before using it".format(channel=channel)) | |||
if index >= len(self._channels[channel - 1]): | |||
raise ValueError("Pixel index must be < {length}".format(length=len(self._channels[channel - 1]))) | |||
return self._channels[channel - 1][index] | |||
def set_white_point(self, r, g, b): | |||
"""Set the white point. | |||
:param r: Red amount, from 0.0 to 1.0 | |||
:param g: Green amount, from 0.0 to 1.0 | |||
:param b: Blue amount, from 0.0 to 1.0 | |||
""" | |||
self.white_point = (r, g, b) | |||
def set_pixel(self, channel, index, r, g, b, brightness=None): | |||
"""Set the RGB colour of a single pixel, on a single channel | |||
:param channel: Channel, either 1, 2, 3 or 4 corresponding to numbers on Mote | |||
:param index: Index of pixel to set, from 0 up | |||
:param r: Amount of red: 0-255 | |||
:param g: Amount of green: 0-255 | |||
:param b: Amount of blue: 0-255 | |||
:param brightness: (Optional) brightness of pixel from 0.0 to 1.0. Will scale R,G,B accordingly. | |||
""" | |||
if channel > self._channel_count or channel < 1: | |||
raise ValueError("Channel index must be between 1 and 4") | |||
if self._channels[channel - 1] is None: | |||
raise ValueError("Please set up channel {channel} before using it!".format(channel=channel)) | |||
if index >= len(self._channels[channel - 1]): | |||
raise ValueError("Pixel index must be < {length}".format(length=len(self._channels[channel - 1]))) | |||
if brightness is None and self._channels[channel - 1][index] is not None: | |||
brightness = self._channels[channel - 1][index][3] | |||
self._channels[channel - 1][index] = (r & 0xff, g & 0xff, b & 0xff, brightness) | |||
def clear(self, channel=None): | |||
"""Clear the buffer of a specific or all channels | |||
:param channel: If set, clear a specific channel, otherwise all (default None) | |||
""" | |||
if channel is None: | |||
for index, data in enumerate(self._channels): | |||
if data is not None: | |||
self.clear(index + 1) | |||
return | |||
if channel > self._channel_count or channel < 1: | |||
raise ValueError("Channel index must be between 1 and 4") | |||
if self._channels[channel - 1] is None: | |||
raise ValueError("Please set up channel {channel} before using it!".format(channel=channel)) | |||
self._channels[channel - 1] = [(0, 0, 0, 1.0)] * len(self._channels[channel - 1]) | |||
def show(self): | |||
"""Send the pixel buffer to the hardware""" | |||
buf = bytearray() | |||
for index, data in enumerate(self._channels): | |||
if data is None: continue | |||
for pixel in data: | |||
r, g, b, brightness = pixel | |||
r, g, b = [int(x * brightness * self.white_point[i]) for i, x in enumerate([r, g, b])] | |||
buf.append(b) | |||
buf.append(g) | |||
buf.append(r) | |||
self.port.write(b'mote') | |||
self.port.write(b'o') | |||
self.port.write(buf) | |||
def set_all(self, r, g, b, brightness=None, channel=None): | |||
"""Set the RGB value and optionally brightness of all pixels | |||
If you don't supply a brightness value, the last value set for each pixel be kept. | |||
:param r: Amount of red: 0 to 255 | |||
:param g: Amount of green: 0 to 255 | |||
:param b: Amount of blue: 0 to 255 | |||
:param brightness: Brightness: 0.0 to 1.0 (default around 0.2) | |||
:param channel: Optional channel: 1, 2, 3 or 4 (default to all) | |||
""" | |||
if channel in range(1, self._channel_count + 1): | |||
for x in range(NUM_PIXELS_PER_CHANNEL): | |||
self.set_pixel(channel, x, r, g, b, brightness) | |||
return | |||
for c in range(1, self._channel_count + 1): | |||
if self._channels[c - 1] is None: | |||
continue | |||
for x in range(len(self._channels[c - 1])): | |||
self.set_pixel(c, x, r, g, b, brightness) | |||
def set_brightness(self, brightness): | |||
"""Set the brightness of all pixels | |||
This sets the brightness of each pixel individually, it can be overriden | |||
by supplying the brightness argument to set_pixel. | |||
:param brightness: Brightness: 0.0 to 1.0 | |||
""" | |||
if not 0.0 <= brightness <= 1.0: | |||
raise ValueError("Brightness should be between 0.0 and 1.0, was: {}".format(brightness)) | |||
for channel in range(self._channel_count): | |||
pixels = self.get_pixel_count(channel + 1) | |||
for pixel in range(pixels): | |||
r, g, b, br = self._channels[channel][pixel] | |||
self._channels[channel][pixel] = (r, g, b, brightness) | |||
def set_clear_on_exit(self, value=True): | |||
"""Set whether Mote should be cleared upon exit. | |||
By default Mote will leave the pixels on upon exit, but calling:: | |||
mote.set_clear_on_exit() | |||
Will ensure that they are cleared. | |||
""" | |||
self._clear_on_exit = value | |||
def __exit__(self): | |||
if self._clear_on_exit: | |||
self.clear() | |||
self.port.close() | |||
if __name__ == "__main__": | |||
from colorsys import hsv_to_rgb | |||
mote = Mote() | |||
num_pixels = 16 | |||
mote.configure_channel(1, num_pixels, False) | |||
mote.configure_channel(2, num_pixels, False) | |||
mote.configure_channel(3, num_pixels, False) | |||
mote.configure_channel(4, num_pixels, False) | |||
colors = [ | |||
(255, 0, 0), | |||
(0, 255, 0), | |||
(0, 0, 255), | |||
(255, 255, 255) | |||
] | |||
try: | |||
for step in range(4): | |||
for channel in range(4): | |||
for pixel in range(num_pixels): | |||
r, g, b = colors[channel] | |||
mote.set_pixel(channel + 1, pixel, r, g, b) | |||
mote.show() | |||
time.sleep(0.01) | |||
colors.append(colors.pop(0)) | |||
for step in range(170): | |||
for channel in range(4): | |||
for pixel in range(num_pixels): | |||
r, g, b, z = [int(c * 0.99) for c in mote.get_pixel(channel + 1, pixel)] | |||
mote.set_pixel(channel + 1, pixel, r, g, b) | |||
time.sleep(0.001) | |||
mote.show() | |||
brightness = 0 | |||
for h in range(10000): | |||
for channel in range(4): | |||
for pixel in range(num_pixels): | |||
hue = (h + (channel * num_pixels * 4) + (pixel * 4)) % 360 | |||
r, g, b = [int(c * brightness) for c in hsv_to_rgb(hue / 360.0, 1.0, 1.0)] | |||
mote.set_pixel(channel + 1, pixel, r, g, b) | |||
mote.show() | |||
time.sleep(0.01) | |||
if brightness < 255: brightness += 1 | |||
except KeyboardInterrupt: | |||
mote.clear() | |||
mote.show() | |||
time.sleep(0.1) |
@ -0,0 +1,26 @@ | |||
<?xml version="1.0" encoding="utf-8" standalone="yes"?> | |||
<settings> | |||
<category label="30000"> | |||
<setting label="30201" type="bool" id="mote1" default="false"/> | |||
<setting id="mote1_position" type="select" label="30201" lvalues="30211|30212|30213|30214|30215|30216|30217|30218|30219|30220|30221|30222" subsetting="true" enable="eq(-1,true)" default="0" /> | |||
<setting label="30230" type="bool" id="mote1_invert" default="false" enable="eq(-2,true)" subsetting="true" /> | |||
<setting label="30202" type="bool" id="mote2" default="false"/> | |||
<setting id="mote2_position" type="select" label="30201" lvalues="30211|30212|30213|30214|30215|30216|30217|30218|30219|30220|30221|30222" subsetting="true" enable="eq(-1,true)" default="2" /> | |||
<setting label="30230" type="bool" id="mote2_invert" default="false" enable="eq(-2,true)" subsetting="true" /> | |||
<setting label="30203" type="bool" id="mote3" default="false"/> | |||
<setting id="mote3_position" type="select" label="30201" lvalues="30211|30212|30213|30214|30215|30216|30217|30218|30219|30220|30221|30222" subsetting="true" enable="eq(-1,true)" default="5" /> | |||
<setting label="30230" type="bool" id="mote3_invert" default="false" enable="eq(-2,true)" subsetting="true" /> | |||
<setting label="30204" type="bool" id="mote4" default="false"/> | |||
<setting id="mote4_position" type="select" label="30201" lvalues="30211|30212|30213|30214|30215|30216|30217|30218|30219|30220|30221|30222" subsetting="true" enable="eq(-1,true)" default="8" /> | |||
<setting label="30230" type="bool" id="mote4_invert" default="false" enable="eq(-2,true)" subsetting="true"/> | |||
<setting label="30900" type="action" action="RunScript(script.service.pimote, clear_mote)"/> | |||
</category> | |||
<category label="31000"> | |||
<setting label="31100" type="select" id="mode" lvalues="31101|31102" default="0"/> | |||
<setting label="31110" type="select" id="color" lvalues="31117|31111|31112|31113|31114|31115|31116" default="0" subsetting="true" /> | |||
<setting label="31121" type="slider" id="color_custom_red" range="0,255" option="int" default="0" subsetting="true" visible="eq(-1,6)" enable="eq(-1,6)" /> | |||
<setting label="31122" type="slider" id="color_custom_green" range="0,255" option="int" default="0" subsetting="true" visible="eq(-2,6)" enable="eq(-2,6)" /> | |||
<setting label="31123" type="slider" id="color_custom_blue" range="0,255" option="int" default="0" subsetting="true" visible="eq(-3,6)" enable="eq(-3,6)" /> | |||
<setting label="31130" type="slider" id="brightness" default="100" range="0,100" option="int" /> | |||
</category> | |||
</settings> |