added setup.py

This commit is contained in:
Giovanni Harting
2015-08-27 21:36:48 +02:00
parent cd8ffd5f7e
commit d64aef6447
10 changed files with 50 additions and 8 deletions

17
ledd/__init__.py Normal file
View File

@@ -0,0 +1,17 @@
# 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/>.
VERSION = "0.1"

165
ledd/controller.py Normal file
View File

@@ -0,0 +1,165 @@
# 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/>.
from json import JSONEncoder
import smbus
from colour import Color
PCA9685_SUBADR1 = 0x2
PCA9685_SUBADR2 = 0x3
PCA9685_SUBADR3 = 0x4
PCA9685_MODE1 = 0x00
PCA9685_MODE2 = 0x01
PCA9685_PRESCALE = 0xFE
PCA9685_RESET = 0xFE
LED0_ON_L = 0x06
LED0_ON_H = 0x07
LED0_OFF_L = 0x08
LED0_OFF_H = 0x09
ALLLED_ON_L = 0xFA
ALLLED_ON_H = 0xFB
ALLLED_OFF_L = 0xFC
ALLLED_OFF_H = 0xFD
class Controller:
"""
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"):
l.append(Controller.from_row(db, row))
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=-1, cid=-1, from_db=False):
self.pwm_freq = pwm_freq
self.channels = channels
self.i2c_device = i2c_device
self.bus = smbus.SMBus(i2c_device)
self.address = address
self.id = cid
self.db = db
self.stripes = []
self.load_stripes()
if not from_db:
self.save_to_db()
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))
cur.close()
def __repr__(self):
return "<Controller stripes={} cid={}>".format(len(self.stripes), self.id)
def set_channel(self, channel, val):
self.bus.write_word_data(int(self.address, 16), LED0_OFF_L + 4 * channel, int(val * 4095))
self.bus.write_word_data(int(self.address, 16), LED0_ON_L + 4 * channel, 0)
def get_channel(self, channel):
return self.bus.read_word_data(self.address, LED0_OFF_L + 4 * channel)
def add_stripe(self, stripe):
self.stripes.append(stripe)
class Stripe:
"""
A stripe is the smallest controllable unit.
"""
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
self._color = Color()
self.gamma_correct = (2.8, 2.8, 2.8) # TODO: add to DB
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()
def read_color(self):
self._color.rgb = [self.controller.get_channel(channel) ** (1 / 2.8) for channel in self.channels]
@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.rgb):
self.controller.set_channel(channel, value ** gamma_correct)
def get_color(self):
return self._color
color = property(get_color, set_color)
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,
'i2c_device': o.i2c_device
}

285
ledd/daemon.py Normal file
View File

@@ -0,0 +1,285 @@
# 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 asyncore
import socket
import configparser
import json
import sqlite3
import os
import sys
import traceback
import time
import logging
from multiprocessing import Process
import nose
from ledd import controller, VERSION
from ledd.decorators import add_action
class Daemon:
daemonSection = 'daemon'
databaseSection = 'db'
instance = None
""":type : Daemon """
action_dict = {}
def __init__(self):
Daemon.instance = self
logging.basicConfig(level=logging.DEBUG,
format="[%(asctime)s] %(levelname)s [%(name)s.%(funcName)s:%(lineno)d] %(message)s",
datefmt="%H:%M:%S")
try:
self.config = configparser.ConfigParser()
try:
with open('ledd.config', 'w+') as f:
self.config.read_file(f)
except FileNotFoundError:
logging.info("No config file found!")
self.sqldb = sqlite3.connect(self.config.get(self.databaseSection, 'name', fallback='ledd.sqlite'))
self.sqldb.row_factory = sqlite3.Row
if not self.check_db():
self.init_db()
self.sqldb.commit()
self.controllers = controller.Controller.from_db(self.sqldb)
logging.debug(self.controllers)
server = self.SocketServer(self.config.get(self.daemonSection, 'host', fallback='0.0.0.0'),
self.config.get(self.daemonSection, 'port', fallback=1425))
asyncore.loop()
except (KeyboardInterrupt, SystemExit):
logging.info("Exiting")
self.sqldb.close()
sys.exit(0)
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()
c.close()
if db_version is not None:
logging.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()
@add_action(action_dict)
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: h,s,v
:param req_json: dict of request json
"""
# TODO: add adapter setting stripe with color here
logging.debug("recieved action: %s", req_json['action'])
@add_action(action_dict)
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
"""
logging.debug("recieved action: %s", req_json['action'])
try:
ncontroller = controller.Controller(Daemon.instance.sqldb, req_json['channels'],
req_json['i2c_dev'], req_json['address'])
except OSError as e:
logging.error("Error opening i2c device: %s", req_json['i2c_dev'])
rjson = {
'success': False,
'message': "Error while opening i2c device",
'message_detail': os.strerror(e.errno),
'ref': req_json['ref']
}
return json.dumps(rjson)
self.controllers.append(ncontroller)
rjson = {
'success': True,
'cid': ncontroller.id,
'ref': req_json['ref']
}
return json.dumps(rjson)
@add_action(action_dict)
def get_color(self, req_json):
"""
Part of the Color API. Used to get the currect color of an stripe.
Required JSON parameters: stripeid: sid
:param req_json: dict of request json
"""
logging.debug("recieved action: %s", req_json['action'])
# TODO: Add get color logic
@add_action(action_dict)
def add_stripes(self, req_json):
"""
Part of the Color API. Used to add stripes.
Required JSON parameters:
:param req_json: dict of request json
"""
logging.debug("recieved action: %s", req_json['action'])
if "stripes" in req_json:
for stripe in req_json['stripes']:
# TODO: add stripe here
logging.debug(len(req_json['stripes']))
@add_action(action_dict)
def get_controllers(self, req_json):
"""
Part of the Color API. Used to get all registered controllers known to the daemon.
Required JSON parameters: none
:param req_json: dict of request json
"""
logging.debug("recieved action: %s", req_json['action'])
rjson = {
'success': True,
'ccount': len(Daemon.instance.controllers),
'controller': Daemon.instance.controllers,
'ref': req_json['ref']
}
return json.dumps(rjson, cls=controller.ControllerEncoder)
@add_action(action_dict)
def connection_check(self, req_json):
"""
Part of the Color API. Used to query all channels on a specified controller.
Required JSON parameters: controller id: cid
:param req_json: dict of request json
"""
logging.debug("recieved action: %s", req_json['action'])
result = next(filter(lambda x: x.id == req_json['cid'], self.controllers), None)
""" :type : Controller """
if result is not None:
for i in range(result.channels):
logging.debug("set channel %d=%s", i, "1")
result.set_channel(i, 1)
time.sleep(10)
result.set_channel(i, 0)
rjson = {
'success': True,
'ref': req_json['ref']
}
return json.dumps(rjson)
@add_action(action_dict)
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
"""
logging.debug("recieved action: %s", req_json['action'])
rjson = {
'success': True,
'ref': req_json['ref'],
'version': VERSION
}
return json.dumps(rjson)
class ConnectionHandler(asyncore.dispatcher_with_send):
def handle_read(self):
data = self.recv(5120)
self.debug = True
def no_action_found(self, req_json):
rjson = {
'success': False,
'message': "No action found",
'ref': req_json['ref']
}
return json.dumps(rjson)
if data:
try:
json_decoded = json.loads(data.decode())
logging.debug(json.dumps(json_decoded, sort_keys=True))
if "action" in json_decoded and "ref" in json_decoded:
return_data = Daemon.instance.action_dict.get(json_decoded['action'], no_action_found)(
self=Daemon.instance,
req_json=json_decoded)
if return_data is not None:
self.send("{}\n".format(return_data).encode())
else:
logging.warning("no action or ref value found in JSON, ignoring")
except TypeError:
logging.error("No valid JSON found: %s", traceback.format_exc())
except ValueError:
logging.error("No valid JSON detected: %s", traceback.format_exc())
class SocketServer(asyncore.dispatcher):
def __init__(self, host, port):
asyncore.dispatcher.__init__(self)
self.create_socket(socket.AF_INET, socket.SOCK_STREAM)
self.set_reuse_addr()
self.bind((host, port))
self.listen(5)
p = Process(target=self.run_tests)
p.start()
@staticmethod
def run_tests():
nose.run()
def handle_accept(self):
pair = self.accept()
if pair is not None:
sock, addr = pair
logging.debug('Incoming connection from %s' % repr(addr))
handler = Daemon.ConnectionHandler(sock)

33
ledd/decorators.py Normal file
View File

@@ -0,0 +1,33 @@
# 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/>.
def add_action(actiondict):
"""
Decorator used to add functions to action dict
:param actiondict: dict to add to
:type actiondict: dict
"""
def wrap(f):
actiondict[f.__name__] = f
def wrapped_f(*args):
f(*args)
return wrapped_f
return wrap

15
ledd/effects/__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

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

21
ledd/sql/ledd.sql Normal file
View File

@@ -0,0 +1,21 @@
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
);
CREATE TABLE "meta" (
`option` TEXT,
`value` TEXT
);
INSERT INTO `meta` VALUES ('db_version','1');
CREATE TABLE "controller" (
`id` INTEGER PRIMARY KEY AUTOINCREMENT UNIQUE,
`address` TEXT,
`i2c_device` INTEGER,
`channels` INTEGER,
`pwm_freq` INTEGER
);

61
ledd/tests.py Normal file
View File

@@ -0,0 +1,61 @@
# 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 configparser
import socket
import json
import uuid
class TestDaemon:
s = None
""" :type : socket.socket """
@classmethod
def setup_class(cls):
daemon_section = 'daemon'
config = configparser.ConfigParser()
try:
with open('ledd.config', 'w+') as f:
config.read_file(f)
except FileNotFoundError:
pass
cls.s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
cls.s.settimeout(20)
cls.s.connect((config.get(daemon_section, 'host', fallback='0.0.0.0'),
config.get(daemon_section, 'port', fallback=1425)))
@classmethod
def teardown_class(cls):
cls.s.close()
def test_discover(self):
ref = uuid.uuid4().urn[9:]
sjson = {
"action": "discover",
"ref": ref
}
self.s.send(json.dumps(sjson).encode())
rstr = self.s.recv(1024).decode()
assert rstr is not None
rjson = json.loads(rstr)
assert rjson['ref'] == ref
assert rjson['version'] is not None