Browse Source

initial commit

master v0.1.0
Lars Hoogestraat 6 months ago
commit
fae3804518
17 changed files with 955 additions and 0 deletions
  1. +21
    -0
      LICENSE.txt
  2. +74
    -0
      README.md
  3. +41
    -0
      THIRDPARTY
  4. +307
    -0
      addon.py
  5. +28
    -0
      addon.xml
  6. +0
    -0
      resources/__init__.py
  7. BIN
      resources/__init__.pyc
  8. BIN
      resources/fanart.jpg
  9. BIN
      resources/icon.png
  10. +1
    -0
      resources/language/README.md
  11. +162
    -0
      resources/language/resource.language.en_gb/strings.po
  12. +0
    -0
      resources/lib/__init__.py
  13. BIN
      resources/lib/__init__.pyc
  14. +295
    -0
      resources/lib/mote.py
  15. BIN
      resources/lib/mote.pyc
  16. +26
    -0
      resources/settings.xml
  17. BIN
      resources/test_color.mp4

+ 21
- 0
LICENSE.txt View File

@ -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.

+ 74
- 0
README.md View File

@ -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)

+ 41
- 0
THIRDPARTY View File

@ -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

+ 307
- 0
addon.py View File

@ -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()

+ 28
- 0
addon.xml View File

@ -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
resources/__init__.py View File


BIN
resources/__init__.pyc View File


BIN
resources/fanart.jpg View File

Before After
Width: 1920  |  Height: 1080  |  Size: 297 KiB

BIN
resources/icon.png View File

Before After
Width: 512  |  Height: 512  |  Size: 316 KiB

+ 1
- 0
resources/language/README.md View File

@ -0,0 +1 @@
This folder will be the home of your translations

+ 162
- 0
resources/language/resource.language.en_gb/strings.po View File

@ -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
resources/lib/__init__.py View File


BIN
resources/lib/__init__.pyc View File


+ 295
- 0
resources/lib/mote.py View File

@ -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)

BIN
resources/lib/mote.pyc View File


+ 26
- 0
resources/settings.xml View File

@ -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>

BIN
resources/test_color.mp4 View File


Loading…
Cancel
Save