22 Commits
1.0 ... master

Author SHA1 Message Date
12ed601446 added notice 2017-12-13 16:32:23 +01:00
Giovanni Harting
bf4dcfaaf8 Update README.md 2017-01-31 07:43:42 +01:00
Giovanni Harting
7d6d289178 fixed wrong calculation of read pwm frequency
some more error handling
2015-11-14 14:35:42 +01:00
Giovanni Harting
db379ccdf0 reverted some derps 2015-11-13 23:58:41 +01:00
Giovanni Harting
99a7252194 added default val for pwm_freq 2/2 2015-11-13 23:56:09 +01:00
Giovanni Harting
a99d879298 added default val for pwm_freq 2015-11-13 23:51:53 +01:00
Giovanni Harting
6ae2cc4fb8 added some more error handling
fixed pwm_freq not getting set on init
2015-11-13 23:41:38 +01:00
Giovanni Harting
a65bb770fd added error handling when color can't get read 2015-11-13 23:17:49 +01:00
Giovanni Harting
02f6837d27 removed more unnecessary debug output 2015-11-13 23:13:17 +01:00
Giovanni Harting
df90bc8a68 fixed some bugs in the installation description 2015-11-13 22:25:45 +01:00
Giovanni Harting
f9d696b1b3 removed more unnecessary debug output 2015-10-31 22:47:09 +01:00
Giovanni Harting
aadcfafd00 added error handling of oserror 70 mostly caused by a busy i2c line
need to add a proper rate limiting and requeue of requests for a future release
2015-10-31 22:41:07 +01:00
Giovanni Harting
6f2a8faa6a Update README.md 2015-10-15 13:41:48 +02:00
Giovanni Harting
01ba0a4d2c added set_all_channel method
reintroduced caching of stripes & controllers
added session cleanup handling
renamed --daemon to --detach for better understanding
added coloredlogs to make logs better readable and more appealing
2015-10-15 13:26:36 +02:00
Giovanni Harting
4c052b8edb moved json encoder to a monkey patch
added bugs introduced in latest sqlalchemy and json-rpc switch (d5f403d557)
2015-10-11 21:43:33 +02:00
Giovanni Harting
2bb52aa3a4 added paramcheck too all functions
updated documentation
updated codestyle
2015-10-11 17:04:54 +02:00
Giovanni Harting
d5f403d557 Moved to SQLAlchemy
Finished switch to JSONRPC

commit 9cf6dd9a0e03c71135c01d4ad4f7d3be0f1e3066
Author: Giovanni Harting <giovanni.harting@touchdata.net>
Date:   Sat Oct 10 21:09:20 2015 +0200

    fixed some bugs
    added some missing things from transition

commit 8ed44b8fcde739b541b1834049025b055a50e6fe
Author: Marius Schiffer <marius@mschiffer.de>
Date:   Sat Oct 10 05:53:41 2015 +0200

    Creating fresh database works now. Fixed pwm_freq property.

commit dc88ef0df427f90746a499912eff70dfce967c55
Author: Marius Schiffer <marius@mschiffer.de>
Date:   Sat Oct 10 04:40:41 2015 +0200

    Completed SQLAlchemy integration. Completed JSON-RPC integration.
    All daemon class functionality is now on module-level (required for
    JSON-RPC decorators).
    Migrations will have to be reimplemented with alembic.

commit a4cabdcd00a3e2a3cbbd92a3c9d59a4235e4d277
Author: Marius Schiffer <marius@mschiffer.de>
Date:   Sat Oct 10 03:00:19 2015 +0200

    First steps towards SQLAlchemy integration.
2015-10-10 21:13:29 +02:00
Giovanni Harting
87f2d12c22 fixed missing json + encode 2015-10-09 21:07:05 +02:00
Giovanni Harting
8ee1dd395c Update README.md 2015-10-09 20:59:02 +02:00
Giovanni Harting
4ad3fd50fb first json rpc implementation 2015-10-09 20:44:55 +02:00
Giovanni Harting
7b67984c80 Update README.md 2015-10-09 19:42:27 +02:00
Giovanni Harting
1a568b194c renamed stuff
added first gamma correction, closes #1
added db upgrade logic
2015-10-04 08:10:52 +02:00
12 changed files with 537 additions and 495 deletions

View File

@@ -1,14 +1,16 @@
# LedD
[![][codeclimate img]][codeclimate] [![][license img]][license]
[![][cq img]][cq] [![][license img]][license]
LedD is a daemon for interfacing LED stripes written in python3. It provides an abstract interface for effects to control any kind of LED stripe through an controller, although it is intented to be used with the PCA9685 chip. An Android application can connect and natively change any settings for the effects and stripes.
# DEPRECATED
## Goals
This project is no longer maintained and has been superseded by LedD.Go.
- manage multiple stripes and controllers at the same time
- an open effects github repository with simple download-and-run system
- automatic enumeration of the connected controller devices, restart/reset and status querys
## General
LedD is a multipurpose daemon for interfacing LED(s) written in python. It provides an abstract interface for effects to control any kind of LED through an backend, although its original purpose was interfacing a PCA9685 chip via i2c. Since there are multiple ways to control leds we made the decision to write LedD as open as possible to other ways of controlling leds. As a result of this decision we split LedD in a server part (this repository) and multiple clients, which can be written in any kind of language and can use a custom way to control its LEDs, as long as they implement LedD's protobuf protocol they can be controled with LedD.
As for frontends there is only an Android app available at this time, there will be more some time in the future. (You are encouraged to write your own!)
## Requirements
@@ -18,6 +20,15 @@ LedD is a daemon for interfacing LED stripes written in python3. It provides an
- PCA9685
- __Note__: Plugins can have different permission requirements
## Installation
Make sure your i2c devices are available (modprobe i2c-dev) before you follow these steps.
1. `apt-get install python3-dev python3-pip python3-cffi python3-docopt python3-nose python3-sqlalchemy python-smbus libffi-dev`
2. `pip3 install -U cffi` (fixes a bug where smbus-cffi can't install)
3. `pip3 install coloredlogs spectra json-rpc smbus-cffi`
4. `adduser $USER i2c`
### Plugins & Effects
Plugin functionality is planned as we provide APIs for effects and plugins to use. Here are some we are going to provide when they are finished.
@@ -43,5 +54,5 @@ This project is licensed under the conditions of the GNU GPL 3.0.
[license]:LICENSE
[license img]:https://img.shields.io/github/license/led-freaks/ledd.svg?style=flat-square
[codeclimate]:https://codeclimate.com/github/LED-Freaks/LedD
[codeclimate img]:https://img.shields.io/codeclimate/github/LED-Freaks/LedD.svg?style=flat-square
[cq]:https://www.codacy.com/app/chefeificationful/LedD
[cq img]:https://img.shields.io/codacy/bb2de4e1587f48358141cd7465d2ea89.svg?style=flat-square

19
ledd.py
View File

@@ -1,3 +1,5 @@
#!/usr/bin/python3
# LEDD Project
# Copyright (C) 2015 LEDD Team
#
@@ -17,16 +19,16 @@
"""LedD Daemon
Usage:
ledd.py [--daemon] [-d | --debug] [-v | --verbose]
ledd.py [--detach] [-d | --debug] [-v | --verbose]
ledd.py -h | --help
ledd.py --version
Options:
-h --help Show this screen.
--version Show version.
-d --debug Show debug output. (not recommended for daily use)
-d --debug Show debug output. (not recommended)
-v --verbose Be verbose.
--daemon Run in daemon mode.
--detach Detach after start.
"""
import logging
@@ -34,6 +36,7 @@ import sys
import os
from pkgutil import iter_modules
import coloredlogs
from docopt import docopt
import ledd.daemon
@@ -89,10 +92,8 @@ if __name__ == "__main__":
if arguments['--debug']:
lvl = logging.DEBUG
logging.basicConfig(level=lvl,
format="[%(asctime)s] %(levelname)s [%(name)s.%(funcName)s:%(lineno)d] %(message)s",
datefmt="%H:%M:%S")
log = logging.getLogger(__name__)
coloredlogs.install(level=lvl)
try:
with open('ledd.pid', 'r') as f:
@@ -106,7 +107,7 @@ if __name__ == "__main__":
except FileNotFoundError:
pass
if arguments['--daemon']:
if arguments['--detach']:
wdir = os.path.dirname(os.path.realpath(__file__))
try:
pid = os.fork()
@@ -118,7 +119,7 @@ if __name__ == "__main__":
os.chdir(wdir)
with open("ledd.pid", 'w') as pidf:
pidf.write(str(os.getpid()) + '\n')
daemon = ledd.daemon.Daemon()
ledd.daemon.run()
else:
sys.exit()
else:
@@ -126,4 +127,4 @@ if __name__ == "__main__":
except OSError as e:
log.fatal("Start failed: %s", e)
else:
daemon = ledd.daemon.Daemon()
ledd.daemon.run()

View File

@@ -13,5 +13,23 @@
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
from json import JSONEncoder
from sqlalchemy.orm import sessionmaker, scoped_session
from sqlalchemy.ext.declarative import declarative_base
VERSION = "0.1"
engine = None
session = scoped_session(sessionmaker())
""" :type : sqlalchemy.orm.scoping.scoped_session """
Base = declarative_base()
Base.query = session.query_property()
def _default(self, obj):
return getattr(obj.__class__, "to_json", _default.default)(obj)
_default.default = JSONEncoder().default
JSONEncoder.default = _default

View File

@@ -14,13 +14,15 @@
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
from json import JSONEncoder
import errno
import logging
import time
import smbus
from sqlalchemy import Column, Integer, String
from sqlalchemy.orm import relationship, reconstructor
from ledd.stripe import Stripe
from . import Base
PCA9685_SUBADR1 = 0x2
PCA9685_SUBADR2 = 0x3
@@ -41,74 +43,60 @@ ALLLED_OFF_L = 0xFC
ALLLED_OFF_H = 0xFD
class Controller:
class Controller(Base):
__tablename__ = "controller"
id = Column(Integer, primary_key=True)
channels = Column(Integer)
i2c_device = Column(Integer)
address = Column(String)
stripes = relationship("Stripe", backref="controller")
_pwm_freq = Column("pwm_freq", Integer, default=1526)
"""
A controller controls a number of stripes.
"""
@classmethod
def from_row(cls, db, row):
# load from db
return cls(db, pwm_freq=row["pwm_freq"], channels=row["channels"], i2c_device=row["i2c_device"],
address=row["address"], cid=row["id"], from_db=True)
@staticmethod
def from_db(db):
l = []
cur = db.cursor()
for row in cur.execute("select * from controller"):
c = Controller.from_row(db, row)
l.append(c)
cur.close()
return l
def save_to_db(self):
cur = self.db.cursor()
if self.id == -1:
cur.execute("INSERT INTO controller (pwm_freq, channels, i2c_device, address) VALUES (?,?,?,?)",
(self._pwm_freq, self.channels, self.i2c_device, self.address))
self.id = cur.lastrowid
else:
cur.execute("UPDATE controller SET pwm_freq=?, channels=?, i2c_device=?, address=? WHERE id = ?",
(self._pwm_freq, self.channels, self.i2c_device, self.address, self.id))
cur.close()
self.db.commit()
def __init__(self, db, channels, i2c_device, address, pwm_freq=1526, cid=-1, from_db=False):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self._mode = None
self.channels = channels
self.i2c_device = i2c_device
self.bus = smbus.SMBus(i2c_device)
self.address = address
self._address = int(address, 16)
self.id = cid
self.db = db
self.stripes = []
self._pwm_freq = None
self.pwm_freq = pwm_freq
if not from_db:
self.save_to_db()
self.load_stripes()
self.bus = smbus.SMBus(self.i2c_device)
self._address = int(self.address, 16)
self.pwm_freq = self._pwm_freq
def load_stripes(self):
cur = self.db.cursor()
for stripe in cur.execute("select * from stripes where controller_id = ?", (self.id,)):
self.stripes.append(Stripe.from_db(self, stripe))
logging.getLogger(__name__).debug("Loaded %s stripes for controller %s", len(self.stripes), self.id)
cur.close()
@reconstructor
def init_on_load(self):
self._mode = None
self.bus = smbus.SMBus(self.i2c_device)
self._address = int(self.address, 16)
self.pwm_freq = self._pwm_freq
def __repr__(self):
return "<Controller stripes={} cid={}>".format(len(self.stripes), self.id)
def set_channel(self, channel, val):
self.bus.write_word_data(self._address, LED0_OFF_L + 4 * channel, int(val * 4095))
def set_channel(self, channel, val, gamma):
self.bus.write_word_data(self._address, LED0_OFF_L + 4 * channel, self.gamma_correct(gamma, int(val * 4095),
4095))
self.bus.write_word_data(self._address, LED0_ON_L + 4 * channel, 0)
def get_channel(self, channel):
return self.bus.read_word_data(self._address, LED0_OFF_L + 4 * channel) / 4095
def set_all_channel(self, val):
self.bus.write_word_data(self._address, ALLLED_OFF_L, int(val * 4095))
self.bus.write_word_data(self._address, ALLLED_ON_L, 0)
def add_stripe(self, stripe):
self.stripes.append(stripe)
@staticmethod
def gamma_correct(gamma, val, maxval):
corrected = int(pow(float(val) / float(maxval), float(gamma)) * float(maxval) + 0.5)
logging.getLogger(__name__).debug("GammaCorrect: in=%s out=%s, gamma=%s", val, corrected, gamma)
return corrected
def get_channel(self, channel):
try:
return self.bus.read_word_data(self._address, LED0_OFF_L + 4 * channel) / 4095
except OSError as e:
if int(e) == errno.ECOMM:
return 0
else:
raise
def reset(self):
self.mode = int("0b00100001", 2) # MODE1 -> 0b00000001
@@ -129,7 +117,7 @@ class Controller:
@property
def pwm_freq(self):
self._pwm_freq = (self.bus.read_byte_data(self._address, PCA9685_PRESCALE) + 1) / 4096 * 25000000
self._pwm_freq = round(390625 / ((self.bus.read_byte_data(self._address, PCA9685_PRESCALE) + 1) * 64))
return self._pwm_freq
@pwm_freq.setter
@@ -144,24 +132,17 @@ class Controller:
self.reset()
self._pwm_freq = value
def to_json(self):
return {
'id': self.id,
'pwm_freq': self.pwm_freq,
'channel': self.channels,
'address': self.address,
'stripes': self.stripes,
'cstripes': len(self.stripes),
'i2c_device': self.i2c_device,
'mode': self.mode
}
class ControllerEncoder(JSONEncoder):
def default(self, o):
if isinstance(o, Controller):
return {
'id': o.id,
'pwm_freq': o.pwm_freq,
'channel': o.channels,
'address': o.address,
'stripes': o.stripes,
'cstripes': len(o.stripes),
'i2c_device': o.i2c_device,
'mode': o.mode
}
elif isinstance(o, Stripe):
return {
'id': o.id,
'name': o.name,
'rgb': o.rgb,
'channel': o.channels
}
def close(self):
self.bus.close()

View File

@@ -14,378 +14,369 @@
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
import logging
import configparser
import json
import sqlite3
import os
import sys
import traceback
import asyncio
import configparser
import errno
import logging
import os
import signal
import sys
import spectra
from jsonrpc import JSONRPCResponseManager, dispatcher
from jsonrpc.exceptions import JSONRPCError, JSONRPCInvalidParams
from sqlalchemy import create_engine
from sqlalchemy.exc import OperationalError
from sqlalchemy.orm.exc import NoResultFound
from ledd import controller, VERSION
from ledd.decorators import ledd_protocol
from ledd import VERSION
from ledd.controller import Controller
from ledd.effectstack import EffectStack
from ledd.models import Meta
from ledd.stripe import Stripe
from . import Base, session
log = logging.getLogger(__name__)
daemonSection = 'daemon'
databaseSection = 'db'
""" :type : asyncio.BaseEventLoop """
effects = []
stripes = []
controller = []
class Daemon:
daemonSection = 'daemon'
databaseSection = 'db'
instance = None
""":type : Daemon """
loop = None
""" :type : asyncio.BaseEventLoop """
protocol = {}
effects = []
def __init__(self):
Daemon.instance = self
def run():
try:
# read config
config = configparser.ConfigParser()
try:
# read config
self.config = configparser.ConfigParser()
try:
with open('ledd.config', 'w+') as f:
self.config.read_file(f)
except FileNotFoundError:
log.info("No config file found!")
pass
with open('ledd.config', 'w+') as f:
config.read_file(f)
except FileNotFoundError:
log.info("No config file found!")
# SQL init
self.sqldb = sqlite3.connect(self.config.get(self.databaseSection, 'name', fallback='ledd.sqlite'))
self.sqldb.row_factory = sqlite3.Row
# SQL init
global engine
engine = create_engine("sqlite:///" + config.get(databaseSection, 'name', fallback='ledd.sqlite'),
echo=log.getEffectiveLevel() == logging.DEBUG)
session.configure(bind=engine)
Base.metadata.bind = engine
if not check_db():
init_db()
if not self.check_db():
self.init_db()
logging.getLogger("asyncio").setLevel(log.getEffectiveLevel())
self.sqldb.commit()
# Load to cache
global controller, stripes
controller = Controller.query.all()
# init controllers from db
self.controllers = controller.Controller.from_db(self.sqldb)
log.debug(self.controllers)
logging.getLogger("asyncio").setLevel(log.getEffectiveLevel())
for c in controller:
stripes.extend(c.stripes)
# sigterm handler
def sigterm_handler(_signo, _stack_frame):
sys.exit(0)
# sigterm handler
def sigterm_handler():
raise SystemExit
signal.signal(signal.SIGTERM, sigterm_handler)
signal.signal(signal.SIGTERM, sigterm_handler)
# main loop
self.loop = asyncio.get_event_loop()
coro = self.loop.create_server(LedDProtocol,
self.config.get(self.daemonSection, 'host', fallback='0.0.0.0'),
self.config.get(self.daemonSection, 'port', fallback=1425))
self.server = self.loop.run_until_complete(coro)
self.loop.run_forever()
except (KeyboardInterrupt, SystemExit):
log.info("Exiting")
try:
os.remove("ledd.pid")
except FileNotFoundError:
pass
self.sqldb.close()
self.server.close()
self.loop.run_until_complete(self.server.wait_closed())
self.loop.close()
sys.exit(0)
# init plugins
# TODO: check all plugins for existing hooks
def check_db(self):
"""
Checks database version
:return: database validity
:rtype: bool
"""
c = self.sqldb.cursor()
try:
c.execute("SELECT value FROM meta WHERE option = 'db_version'")
db_version = c.fetchone()
# main loop
global loop, server
loop = asyncio.get_event_loop()
coro = loop.create_server(LedDProtocol,
config.get(daemonSection, 'host', fallback='0.0.0.0'),
config.get(daemonSection, 'port', fallback=1425))
server = loop.run_until_complete(coro)
log.info("Start phase finished; starting main loop")
loop.run_forever()
except (KeyboardInterrupt, SystemExit):
log.info("Exiting")
for c in controller:
c.close()
if db_version is not None:
log.info("DB connection established; db-version=%s", db_version[0])
return True
else:
return False
except sqlite3.OperationalError:
c.close()
return False
def init_db(self):
self.sqldb.close()
if os.path.exists("ledd.sqlite"):
os.remove("ledd.sqlite")
self.sqldb = sqlite3.connect(self.config.get(self.databaseSection, 'name', fallback='ledd.sqlite'))
self.sqldb.row_factory = sqlite3.Row
with open("ledd/sql/ledd.sql", "r") as sqlfile:
c = self.sqldb.cursor()
c.executescript(sqlfile.read())
c.close()
self.check_db()
@ledd_protocol(protocol)
def start_effect(self, req_json):
"""
Part of the Color API. Used to start a specific effect.
Required JSON parameters: stripe IDs: sids; effect id: eid, effect options: eopt
:param req_json: dict of request json
"""
log.debug("recieved action: %s", req_json['action'])
stripes = []
if "sids" in req_json:
for sid in req_json['sids']:
found_s = self.find_stripe(sid)
if found_s is not None:
stripes.append(found_s)
if len(stripes) > 0:
# TODO: add anything required to start effect with req_json['eid']
# on stripes[] with options in req_json['eopt']
effect = EffectStack()
self.effects.append(effect)
effect.stripes.append(self.controllers[0].stripes[0])
effect.start()
# asyncio.ensure_future(asyncio.get_event_loop().run_in_executor(self.executor, effect.execute))
rjson = {
'success': True,
'eident': None, # unique effect identifier that identifies excatly this effect started on this set of
# stripes, used to stop them later and to give informations about running effects
'ref': req_json['ref']
}
return json.dumps(rjson)
else:
rjson = {
'success': False,
'message': "No stripe with this id found",
'ref': req_json['ref']
}
return json.dumps(rjson)
@ledd_protocol(protocol)
def stop_effect(self, req_json):
"""
Part of the Color API. Used to stop a specific effect.
Required JSON parameters: effect identifier: eident
:param req_json: dict of request json
"""
log.debug("recieved action: %s", req_json['action'])
# TODO: add stop effect by eident logic
@ledd_protocol(protocol)
def get_effects(self, req_json):
"""
Part of the Color API. Used to show all available and running effects.
Required JSON parameters: -
:param req_json: dict of request json
"""
log.debug("recieved action: %s", req_json['action'])
# TODO: list all effects here and on which stripes they run atm
# TODO: all effects get runtime only ids, "eid"'s. They are shown here for the client to start effects.
# TODO: All options that an effect may have need to be transmitted here too with "eopt".
@ledd_protocol(protocol)
def set_color(self, req_json):
"""
Part of the Color API. Used to set color of a stripe.
Required JSON parameters: stripe ID: sid; HSV values hsv: h,s,v, controller id: cid
:param req_json: dict of request json
"""
log.debug("recieved action: %s", req_json['action'])
found_s = self.find_stripe(req_json['sid'])
if found_s is None:
log.warning("Stripe not found: id=%s", req_json['sid'])
else:
found_s.set_color(spectra.hsv(req_json['hsv']['h'], req_json['hsv']['s'], req_json['hsv']['v']))
def find_stripe(self, sid):
"""
Finds a given stripeid in the currently known controllers
:param sid stripe id
:return: stripe if found or none
:rtype: ledd.Stripe | None
"""
for c in self.controllers:
for s in c.stripes:
if s.id == sid:
return s
return None
@ledd_protocol(protocol)
def add_controller(self, req_json):
"""
Part of the Color API. Used to add a controller.
Required JSON parameters: channels; i2c_dev: number of i2c device (e.g. /dev/i2c-1 would be i2c_dev = 1);
address: hexdecimal address of controller on i2c bus, e.g. 0x40
:param req_json: dict of request json
"""
log.debug("recieved action: %s", req_json['action'])
try:
ncontroller = controller.Controller(Daemon.instance.sqldb, req_json['channels'],
req_json['i2c_dev'], req_json['address'])
os.remove("ledd.pid")
except FileNotFoundError:
pass
session.commit()
session.close()
if server is not None:
server.close()
if loop is not None:
loop.run_until_complete(server.wait_closed())
loop.close()
sys.exit(0)
def check_db():
"""
Checks database version
:return: database validity
:rtype: bool
"""
try:
db_version = Meta.get_version()
if db_version is not None:
log.info("DB connection established; db_version=%s", db_version)
return True
except OperationalError:
return False
return False
def init_db():
Base.metadata.drop_all()
Base.metadata.create_all()
session.add(Meta(option="db_version", value="2"))
session.commit()
check_db()
@dispatcher.add_method
def start_effect(**kwargs):
"""
Part of the Color API. Used to start a specific effect.
Required parameters: stripe IDs: sids; effect id: eid, effect options: eopt
:param kwargs:
"""
if "sids" not in kwargs or "eid" not in kwargs or "eopt" not in kwargs:
return JSONRPCInvalidParams()
for stripe in Stripe.query.filter(Stripe.id.in_(kwargs['sids'])):
# TODO: add anything required to start effect with req_json['eid']
# on stripes[] with options in req_json['eopt']
effect = EffectStack()
effects.append(effect)
effect.stripes.append(stripe)
effect.start()
# asyncio.ensure_future(asyncio.get_event_loop().run_in_executor(executor, effect.execute))
rjson = {
'eident': None, # unique effect identifier that identifies excatly this effect started on this set of
# stripes, used to stop them later and to give informations about running effects
}
return rjson
return JSONRPCError(-1003, "Stripeid not found")
@dispatcher.add_method
def stop_effect(**kwargs):
"""
Part of the Color API. Used to stop a specific effect.
Required parameters: effect identifier: eident
"""
# TODO: add stop effect by eident logic
@dispatcher.add_method
def get_effects(**kwargs):
"""
Part of the Color API. Used to show all available and running effects.
Required parameters: -
"""
# TODO: list all effects here and on which stripes they run atm
# TODO: all effects get runtime only ids, "eid"'s. They are shown here for the client to start effects.
# TODO: All options that an effect may have need to be transmitted here too with "eopt".
@dispatcher.add_method
def set_color(**kwargs):
"""
Part of the Color API. Used to set color of a stripe.
Required parameters: stripe ID: sid; HSV values hsv: h,s,v, controller id: cid
"""
if "sid" not in kwargs or "hsv" not in kwargs:
return JSONRPCInvalidParams()
stripe = get_stripe(kwargs['sid'])
if stripe:
try:
stripe.set_color(spectra.hsv(kwargs['hsv']['h'], kwargs['hsv']['s'], kwargs['hsv']['v']))
except OSError as e:
log.error("Error opening i2c device: %s (%s)", req_json['i2c_dev'], os.strerror(int(str(e))))
rjson = {
'success': False,
'message': "Error while opening i2c device",
'message_detail': os.strerror(int(str(e))),
'ref': req_json['ref']
}
return json.dumps(rjson)
if int(e) == errno.ECOMM:
log.warning("Communication error on I2C Bus")
return e
else:
raise
else:
log.warning("Stripe not found: id=%s", kwargs['sid'])
return JSONRPCError(-1003, "Stripeid not found")
self.controllers.append(ncontroller)
return ""
rjson = {
'success': True,
'cid': ncontroller.id,
'ref': req_json['ref']
}
return json.dumps(rjson)
@dispatcher.add_method
def set_color_all(**kwargs):
"""
Part of the Color API. Used to set brightness of all stripes a controller owns.
Required parameters: controller id: cid, value: v
"""
@ledd_protocol(protocol)
def get_color(self, req_json):
"""
Part of the Color API. Used to get the current color of an stripe.
Required JSON parameters: stripes
:param req_json: dict of request json
"""
log.debug("recieved action: %s", req_json['action'])
if "cid" not in kwargs or "v" not in kwargs:
return JSONRPCInvalidParams()
found_s = self.find_stripe(req_json['sid'])
try:
c = get_controller(kwargs['cid'])
""" :type c: ledd.controller.Controller """
if found_s is None:
log.warning("Stripe not found: id=%s", req_json['sid'])
return {
'success': False,
'message': "Stripe not found",
'ref': req_json['ref']
}
c.set_all_channel(kwargs['v'])
except NoResultFound:
log.warning("Controller not found: id=%s", kwargs['cid'])
return JSONRPCError(-1002, "Controller not found")
rjson = {
'success': True,
'color': found_s.color.values,
'ref': req_json['ref']
}
return ""
return json.dumps(rjson)
@ledd_protocol(protocol)
def add_stripe(self, req_json):
"""
Part of the Color API. Used to add stripes.
Required JSON parameters: name; rgb: bool; map: r: r-channel, g: g-channel, b: b-channel, cid
:param req_json: dict of request json
"""
log.debug("recieved action: %s", req_json['action'])
@dispatcher.add_method
def add_controller(**kwargs):
"""
Part of the Color API. Used to add a controller.
Required parameters: channels; i2c_dev: number of i2c device (e.g. /dev/i2c-1 would be i2c_dev = 1);
address: hexdecimal address of controller on i2c bus, e.g. 0x40
"""
if "stripe" in req_json:
stripe = req_json['stripe']
c = next((x for x in self.controllers if x.id == stripe['cid']), None)
""" :type c: ledd.controller.Controller """
if "i2c_dev" not in kwargs or "channels" not in kwargs or "address" not in kwargs:
return JSONRPCInvalidParams()
if c is None:
return {
'success': False,
'message': "Controller not found",
'ref': stripe['ref']
}
try:
ncontroller = Controller(channels=int(kwargs['channels']), i2c_device=int(kwargs['i2c_dev']),
address=kwargs['address'], _pwm_freq=1526)
except OSError as e:
log.error("Error opening i2c device: %s (%s)", kwargs['i2c_dev'], e)
return JSONRPCError(-1004, "Error while opening i2c device", e)
s = Stripe(c, stripe['name'], stripe['rgb'],
(stripe['map']['r'], stripe['map']['g'], stripe['map']['b']))
session.add(ncontroller)
session.commit()
c.stripes.append(s)
log.debug("Added stripe %s to controller %s; new len %s", c.id, s.id, len(c.stripes))
controller.append(ncontroller)
rjson = {
'success': True,
'sid': s.id,
'ref': req_json['ref']
}
return {'cid': ncontroller.id}
return json.dumps(rjson)
@ledd_protocol(protocol)
def get_stripes(self, req_json):
"""
Part of the Color API. Used to get all registered stripes known to the daemon.
Required JSON parameters: none
:param req_json: dict of request json
"""
log.debug("recieved action: %s", req_json['action'])
@dispatcher.add_method
def get_color(**kwargs):
"""
Part of the Color API. Used to get the current color of an stripe.
Required parameters: sid
"""
rjson = {
'success': True,
'ccount': len(Daemon.instance.controllers),
'controller': Daemon.instance.controllers,
'ref': req_json['ref']
}
if "sid" not in kwargs:
return JSONRPCInvalidParams()
return json.dumps(rjson, cls=controller.ControllerEncoder)
stripe = get_stripe(kwargs['sid'])
@ledd_protocol(protocol)
def test_channel(self, req_json):
"""
Part of the Color API. Used to test a channel on a specified controller.
Required JSON parameters: controller id: cid, channel, value
:param req_json: dict of request json
"""
log.debug("recieved action: %s", req_json['action'])
if not stripe:
log.warning("Stripe not found: id=%s", kwargs['sid'])
return JSONRPCError(-1003, "Stripeid not found")
result = next(filter(lambda x: x.id == req_json['cid'], self.controllers), None)
""" :type : ledd.controller.Controller """
if stripe.color:
return {'color': stripe.color.values}
else:
log.warning("Stripe has no color: id=%s", kwargs['sid'])
return JSONRPCError(-1009, "Internal Error")
if result is not None:
result.set_channel(req_json['channel'], req_json['value'])
rjson = {
'success': True,
'ref': req_json['ref']
}
@dispatcher.add_method
def add_stripe(**kwargs):
"""
Part of the Color API. Used to add stripes.
Required parameters: name; rgb: bool; map: r: r-channel, g: g-channel, b: b-channel, cid
"""
return json.dumps(rjson)
if "name" not in kwargs or "rgb" not in kwargs or "map" not in kwargs or "cid" not in kwargs:
return JSONRPCInvalidParams()
@ledd_protocol(protocol)
def discover(self, req_json):
"""
Part of the Color API. Used by mobile applications to find the controller.
Required JSON parameters: none
:param req_json: dict of request json
"""
log.debug("recieved action: %s", req_json['action'])
c = get_controller(kwargs['cid'])
""" :type c: ledd.controller.Controller """
rjson = {
'success': True,
'ref': req_json['ref'],
'version': VERSION
}
if c is None:
log.warning("Controller not found: id=%s", kwargs['cid'])
return JSONRPCError(-1002, "Controller not found")
return json.dumps(rjson)
s = Stripe(name=kwargs['name'], rgb=bool(kwargs['rgb']),
channel_r=kwargs['map']['r'], channel_g=kwargs['map']['g'], channel_b=kwargs['map']['b'])
s.controller = c
log.debug("Added stripe %s to controller %s; new len %s", s.id, c.id, len(c.stripes))
def no_action_found(self, req_json):
rjson = {
'success': False,
'message': "No action found",
'ref': req_json['ref']
}
return json.dumps(rjson)
session.add(s)
session.commit()
stripes.append(s)
return {'sid': s.id}
@dispatcher.add_method
def get_stripes(**kwargs):
"""
Part of the Color API. Used to get all registered stripes known to the daemon.
Required parameters: -
"""
rjson = {
'ccount': len(controller),
'controller': controller
}
return rjson
@dispatcher.add_method
def test_channel(**kwargs):
"""
Part of the Color API. Used to test a channel on a specified controller.
Required parameters: controller id: cid, channel, value
"""
if "cid" not in kwargs or "channel" not in kwargs or "value" not in kwargs:
return JSONRPCInvalidParams()
contr = get_controller(kwargs['cid'])
""" :type : ledd.controller.Controller """
if contr is not None:
try:
contr.set_channel(kwargs['channel'], kwargs['value'], 2.8)
except OSError as e:
return JSONRPCError(-1009, "Internal Error", e)
else:
return JSONRPCError(-1002, "Controller not found")
return ""
@dispatcher.add_method
def discover(**kwargs):
"""
Part of the Color API. Used by mobile applications to find the controller.
Required parameters: -
"""
return {'version': VERSION}
def get_stripe(sid):
for s in stripes:
if s.id == sid:
return s
def get_controller(cid):
for c in controller:
if c.id == cid:
return c
class LedDProtocol(asyncio.Protocol):
@@ -401,32 +392,17 @@ class LedDProtocol(asyncio.Protocol):
except UnicodeDecodeError:
log.warning("Recieved undecodable data, ignoring")
else:
log.debug("Received: %s from: %s", d_decoded, self.transport.get_extra_info("peername"))
self.select_task(d_decoded)
def select_task(self, data):
if data:
try:
data_split = data.splitlines()
log.debug(data_split)
for line in data_split:
if line:
json_decoded = json.loads(line)
if "action" in json_decoded and "ref" in json_decoded:
return_data = Daemon.instance.protocol.get(json_decoded['action'], Daemon.no_action_found)(
Daemon.instance, json_decoded)
if return_data is not None:
self.transport.write("{}\n".format(return_data).encode())
else:
log.debug("no action or ref value found in JSON, ignoring")
except TypeError:
log.debug("No valid JSON found: %s", traceback.format_exc())
except ValueError:
log.debug("No valid JSON detected: %s", traceback.format_exc())
data_split = data.splitlines()
for line in data_split:
if line:
try:
self.transport.write(JSONRPCResponseManager.handle(line, dispatcher).json.encode())
except TypeError as te:
log.warning("Can't send response: %s", te)
def connection_lost(self, exc):
# The socket has been closed, stop the event loop
# Daemon.loop.stop()
log.info("Lost connection to %s", self.transport.get_extra_info("peername"))

View File

@@ -1,3 +1,19 @@
# LEDD Project
# Copyright (C) 2015 LEDD Team
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
import asyncio
from ledd.effects.fadeeffect import FadeEffect

View File

@@ -14,20 +14,16 @@
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
from sqlalchemy import String, Column
def ledd_protocol(proto):
"""
Decorator used to add functions to action dict
:param proto: dict to add to
:type proto: dict
"""
from . import Base
def wrap(f):
proto[f.__name__] = f
def wrapped_f(*args):
f(*args)
class Meta(Base):
__tablename__ = "meta"
option = Column(String, primary_key=True)
value = Column(String)
return wrapped_f
return wrap
@classmethod
def get_version(cls):
return cls.query.filter(Meta.option == "db_version").first()

15
ledd/plugins/__init__.py Normal file
View File

@@ -0,0 +1,15 @@
# LEDD Project
# Copyright (C) 2015 LEDD Team
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.

View File

@@ -1,21 +1,24 @@
CREATE TABLE `stripes` (
`id` INTEGER PRIMARY KEY AUTOINCREMENT UNIQUE,
`name` TEXT,
`rgb` INTEGER,
`controller_id` INTEGER,
`channel_r` INTEGER,
`channel_g` INTEGER,
`channel_b` INTEGER
`id` INTEGER PRIMARY KEY AUTOINCREMENT UNIQUE,
`name` TEXT,
`rgb` INTEGER,
`controller_id` INTEGER,
`channel_r` INTEGER,
`channel_g` INTEGER,
`channel_b` INTEGER,
`channel_r_gamma` REAL DEFAULT 2.8,
`channel_g_gamma` REAL DEFAULT 2.8,
`channel_b_gamma` REAL DEFAULT 2.8
);
CREATE TABLE "meta" (
`option` TEXT,
`value` TEXT
`option` TEXT,
`value` TEXT
);
INSERT INTO `meta` VALUES ('db_version','1');
INSERT INTO `meta` VALUES ('db_version', '2');
CREATE TABLE "controller" (
`id` INTEGER PRIMARY KEY AUTOINCREMENT UNIQUE,
`address` TEXT,
`i2c_device` INTEGER,
`channels` INTEGER,
`pwm_freq` INTEGER
`id` INTEGER PRIMARY KEY AUTOINCREMENT UNIQUE,
`address` TEXT,
`i2c_device` INTEGER,
`channels` INTEGER,
`pwm_freq` INTEGER
);

5
ledd/sql/upgrade_1_2.sql Normal file
View File

@@ -0,0 +1,5 @@
ALTER TABLE stripes ADD COLUMN channel_r_gamma REAL DEFAULT 2.8;
ALTER TABLE stripes ADD COLUMN channel_g_gamma REAL DEFAULT 2.8;
ALTER TABLE stripes ADD COLUMN channel_b_gamma REAL DEFAULT 2.8;
REPLACE INTO meta (`option`, `value`) VALUES (`db_version`, `2`);

View File

@@ -13,56 +13,76 @@
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
from spectra import Color
from sqlalchemy import Integer, ForeignKey, String, Float, Boolean
from sqlalchemy import Column
from sqlalchemy.orm import reconstructor
from . import Base
class Stripe:
class Stripe(Base):
"""
A stripe is the smallest controllable unit.
"""
__tablename__ = "stripe"
id = Column(Integer, primary_key=True)
controller_id = Column(Integer, ForeignKey('controller.id'))
name = Column(String)
channel_r = Column(Integer)
channel_g = Column(Integer)
channel_b = Column(Integer)
channel_r_gamma = Column(Float, default=2.8)
channel_g_gamma = Column(Float, default=2.8)
channel_b_gamma = Column(Float, default=2.8)
rgb = Column(Boolean)
def __init__(self, controller, name, rgb, channels, sid=-1, from_db=False):
self.controller = controller
self.name = name
self.rgb = bool(rgb)
self.channels = channels
self.id = sid
@property
def channels(self):
return self.channel_r, self.channel_g, self.channel_b
# TODO save channels to db
@channels.setter
def channels(self, t):
self.channel_r, self.channel_g, self.channel_b = t
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self._color = None
self.gamma_correct = (2.8, 2.8, 2.8) # TODO: add to DB
self.gamma_correct = (self.channel_r_gamma, self.channel_g_gamma, self.channel_b_gamma)
self.read_color()
if not from_db:
self.save_to_db()
def save_to_db(self):
cur = self.controller.db.cursor()
if self.id == -1:
cur.execute("INSERT INTO stripes DEFAULT VALUES")
self.id = cur.lastrowid
cur.execute(
"UPDATE stripes SET channel_r = ?, channel_g = ?, channel_b = ?,controller_id = ?, name = ? WHERE id = ?",
self.channels + (self.controller.id, self.name, self.id))
cur.close()
self.controller.db.commit()
@reconstructor
def init_on_load(self):
self._color = None
self.gamma_correct = (self.channel_r_gamma, self.channel_g_gamma, self.channel_b_gamma)
self.read_color()
def read_color(self):
rc = tuple([float(self.controller.get_channel(channel)) for channel in self.channels])
c = Color("rgb", rc[0], rc[1], rc[2])
self._color = c.to("hsv")
if self.controller:
rc = tuple([float(self.controller.get_channel(channel)) for channel in self.channels])
c = Color("rgb", rc[0], rc[1], rc[2])
self._color = c.to("hsv")
def __repr__(self):
return "<Stripe id={}>".format(self.id)
@classmethod
def from_db(cls, controller, row):
return cls(controller, name=row["name"], rgb=row["rgb"],
channels=(row["channel_r"], row["channel_g"], row["channel_b"]), sid=row["id"], from_db=True)
def set_color(self, c):
self._color = c
for channel, gamma_correct, value in zip(self.channels, self.gamma_correct, c.clamped_rgb):
self.controller.set_channel(channel, value)
self.controller.set_channel(channel, value, gamma_correct)
def get_color(self):
return self._color
def to_json(self):
return {
'id': self.id,
'name': self.name,
'rgb': self.rgb,
'channel': self.channels
}
color = property(get_color, set_color)

View File

@@ -25,6 +25,6 @@ setup(name='LedD',
license='GPLv3',
packages=['ledd'],
install_requires=[
'nose', 'spectra', 'docopt',
'nose', 'spectra', 'docopt', 'jsonrpc', 'sqlalchemy', 'coloredlogs'
],
zip_safe=False)