diff --git a/ledd.py b/ledd.py new file mode 100644 index 0000000..d92899d --- /dev/null +++ b/ledd.py @@ -0,0 +1,129 @@ +# 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 . + +"""LedD Daemon + +Usage: + ledd.py [--daemon] [-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) + -v --verbose Be verbose. + --daemon Run in daemon mode. +""" + +import logging +import sys +import os +from pkgutil import iter_modules + +from docopt import docopt + +import ledd.daemon +import ledd + +if "smbus" not in (name for loader, name, ispkg in iter_modules()): + print("smbus not found, installing replacement") + + + class SMBus: + def __init__(self, i2c_address): + self.i2c_address = i2c_address + self.channels = {} + + def write_word_data(self, addr, cmd, val): + if (cmd - 6) % 4 == 0: + self.channels[(cmd - 6) // 4] = val + + def read_word_data(self, addr, cmd): + if (cmd - 8) // 4 not in self.channels: + self.channels[(cmd - 8) // 4] = 0 + return self.channels[(cmd - 8) // 4] + + + class SMBusModule: + SMBus = SMBus + + + sys.modules['smbus'] = SMBusModule + sys.modules['smbus'].SMBus = SMBus + + +def pid_exists(processid): + if processid < 0: + return False + try: + os.kill(processid, 0) + except ProcessLookupError: + return False + except PermissionError: + return True + else: + return True + + +if __name__ == "__main__": + arguments = docopt(__doc__, version='LedD Daemon ' + ledd.VERSION) + lvl = logging.WARNING + + if arguments['--verbose']: + lvl = logging.INFO + + 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__) + + try: + with open('ledd.pid', 'r') as f: + spid = f.read() + if spid: + if pid_exists(int(spid)): + log.fatal("A instance of the program is already running, exiting...") + sys.exit(5) + else: + log.warning("Found stale pid file, assuming unclean shutdown.") + except FileNotFoundError: + pass + + if arguments['--daemon']: + wdir = os.path.dirname(os.path.realpath(__file__)) + try: + pid = os.fork() + if pid == 0: + os.setsid() + pid2 = os.fork() + if pid2 == 0: + os.umask(0) + os.chdir(wdir) + with open("ledd.pid", 'w') as pidf: + pidf.write(str(os.getpid()) + '\n') + daemon = ledd.daemon.Daemon() + else: + sys.exit() + else: + sys.exit() + except OSError as e: + log.fatal("Start failed: %s", e) + else: + daemon = ledd.daemon.Daemon() diff --git a/ledd/controller.py b/ledd/controller.py index 32cdf18..e477b93 100644 --- a/ledd/controller.py +++ b/ledd/controller.py @@ -15,6 +15,8 @@ # along with this program. If not, see . from json import JSONEncoder +import logging +import time import smbus @@ -27,7 +29,6 @@ PCA9685_SUBADR3 = 0x4 PCA9685_MODE1 = 0x00 PCA9685_MODE2 = 0x01 PCA9685_PRESCALE = 0xFE -PCA9685_RESET = 0xFE LED0_ON_L = 0x06 LED0_ON_H = 0x07 @@ -56,7 +57,8 @@ class Controller: l = [] cur = db.cursor() for row in cur.execute("select * from controller"): - l.append(Controller.from_row(db, row)) + c = Controller.from_row(db, row) + l.append(c) cur.close() return l @@ -64,24 +66,27 @@ class Controller: 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._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)) + (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 + def __init__(self, db, channels, i2c_device, address, pwm_freq=1526, cid=-1, from_db=False): + 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.load_stripes() + self._pwm_freq = None + self.pwm_freq = pwm_freq if not from_db: self.save_to_db() @@ -89,21 +94,56 @@ class Controller: 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() def __repr__(self): return "".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) + self.bus.write_word_data(self._address, LED0_OFF_L + 4 * channel, int(val * 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(int(self.address, 16), LED0_OFF_L + 4 * channel) / 4095 + return self.bus.read_word_data(self._address, LED0_OFF_L + 4 * channel) / 4095 def add_stripe(self, stripe): self.stripes.append(stripe) + def reset(self): + self.mode = int("0b00100001", 2) # MODE1 -> 0b00000001 + time.sleep(0.015) + self.mode = int("0b10100001", 2) + + @property + def mode(self): + self._mode = self.bus.read_byte_data(self._address, PCA9685_MODE1) + logging.getLogger(__name__).debug("Controller mode: %s", bin(self._mode)) + return self._mode + + @mode.setter + def mode(self, mode): + self.bus.write_byte_data(self._address, PCA9685_MODE1, mode) + self._mode = mode + logging.getLogger(__name__).debug("Controller mode: %s", bin(self._mode)) + + @property + def pwm_freq(self): + self._pwm_freq = (self.bus.read_byte_data(self._address, PCA9685_PRESCALE) + 1) / 4096 * 25000000 + return self._pwm_freq + + @pwm_freq.setter + def pwm_freq(self, value): + if value < 24 or value > 1526: + raise ValueError("PWM frequency must be 24Hz <= pwm_freq <= 1526Hz: {}".format(value)) + prescal = round((25000000.0 / (4096.0 * value))) - 1 + logging.getLogger(__name__).debug("Presacle value: %s", prescal) + + self.mode = int("0b00110001", 2) + self.bus.write_byte_data(self._address, PCA9685_PRESCALE, prescal) + self.reset() + self._pwm_freq = value + class ControllerEncoder(JSONEncoder): def default(self, o): @@ -114,5 +154,14 @@ class ControllerEncoder(JSONEncoder): 'channel': o.channels, 'address': o.address, 'stripes': o.stripes, - 'i2c_device': o.i2c_device + '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 } diff --git a/ledd/daemon.py b/ledd/daemon.py index 5e33a09..5b339dc 100644 --- a/ledd/daemon.py +++ b/ledd/daemon.py @@ -23,6 +23,8 @@ import sys import traceback import time import asyncio +import signal + import spectra from ledd import controller, VERSION @@ -54,6 +56,7 @@ class Daemon: self.config.read_file(f) except FileNotFoundError: log.info("No config file found!") + pass # SQL init self.sqldb = sqlite3.connect(self.config.get(self.databaseSection, 'name', fallback='ledd.sqlite')) @@ -67,7 +70,13 @@ class Daemon: # init controllers from db self.controllers = controller.Controller.from_db(self.sqldb) log.debug(self.controllers) - logging.getLogger("asyncio").setLevel(logging.DEBUG) + logging.getLogger("asyncio").setLevel(log.getEffectiveLevel()) + + # sigterm handler + def sigterm_handler(_signo, _stack_frame): + sys.exit(0) + + signal.signal(signal.SIGTERM, sigterm_handler) # main loop self.loop = asyncio.get_event_loop() @@ -78,6 +87,10 @@ class Daemon: 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()) @@ -208,7 +221,7 @@ class Daemon: def find_stripe(self, sid): """ Finds a given stripeid in the currently known controllers - :param jstripe: json containing sid + :param sid stripe id :return: stripe if found or none :rtype: ledd.Stripe | None """ @@ -298,6 +311,7 @@ class Daemon: if "stripes" in req_json: for stripe in req_json['stripes']: c = next((x for x in self.controllers if x.id == stripe['cid']), None) + """ :type c: ledd.controller.Controller """ if c is None: res_stripes.append({ @@ -310,6 +324,9 @@ class Daemon: s = Stripe(c, stripe['name'], stripe['rgb'], (stripe['map']['r'], stripe['map']['g'], stripe['map']['b'])) + c.stripes.append(s) + log.debug("Added stripe %s to controller %s; new len %s", c.id, s.id, len(c.stripes)) + res_stripes.append({ 'success': True, 'sid': s.id, @@ -325,9 +342,9 @@ class Daemon: return json.dumps(rjson) @ledd_protocol(protocol) - def get_controllers(self, req_json): + def get_stripes(self, req_json): """ - Part of the Color API. Used to get all registered controllers known to the daemon. + 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 """ @@ -398,17 +415,22 @@ class LedDProtocol(asyncio.Protocol): transport = None def connection_made(self, transport): - log.info("New connection from %s", transport.get_extra_info("peername")) + log.debug("New connection from %s", transport.get_extra_info("peername")) self.transport = transport def data_received(self, data): - log.info("Received: %s from: %s", data.decode(), self.transport.get_extra_info("peername")) - self.select_task(data) + try: + d_decoded = data.decode() + 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: - json_decoded = json.loads(data.decode()) + json_decoded = json.loads(data) if "action" in json_decoded and "ref" in json_decoded: return_data = Daemon.instance.protocol.get(json_decoded['action'], Daemon.no_action_found)( diff --git a/ledd/stripe.py b/ledd/stripe.py index 2e7b1e9..c07de75 100644 --- a/ledd/stripe.py +++ b/ledd/stripe.py @@ -49,6 +49,9 @@ class Stripe: c = Color("rgb", rc[0], rc[1], rc[2]) self._color = c.to("hsv") + def __repr__(self): + return "".format(self.id) + @classmethod def from_db(cls, controller, row): return cls(controller, name=row["name"], rgb=row["rgb"], diff --git a/setup.py b/setup.py index e89ed7d..6d4c2e9 100644 --- a/setup.py +++ b/setup.py @@ -21,10 +21,10 @@ setup(name='LedD', description='Providing control for led stripes.', url='https://github.com/LED-Freaks/LedD', author='IdleGandalf, Lauch', - author_email='539@idlegandalf.com', + author_email='ledd@idlegandalf.com', license='GPLv3', packages=['ledd'], install_requires=[ - 'nose', 'spectra', + 'nose', 'spectra', 'docopt', ], zip_safe=False) diff --git a/start.py b/start.py deleted file mode 100644 index cc16563..0000000 --- a/start.py +++ /dev/null @@ -1,71 +0,0 @@ -# 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 . - -import logging -import ledd.daemon -import sys -import os - -from pkgutil import iter_modules - -if "smbus" not in (name for loader, name, ispkg in iter_modules()): - print("smbus not found, installing replacement") - - - class SMBus: - def __init__(self, i2c_address): - self.i2c_address = i2c_address - self.channels = {} - - def write_word_data(self, addr, cmd, val): - if (cmd - 6) % 4 == 0: - self.channels[(cmd - 6) // 4] = val - - def read_word_data(self, addr, cmd): - if (cmd - 8) // 4 not in self.channels: - self.channels[(cmd - 8) // 4] = 0 - return self.channels[(cmd - 8) // 4] - - - class SMBusModule: - SMBus = SMBus - - - sys.modules['smbus'] = SMBusModule - sys.modules['smbus'].SMBus = SMBus - -if __name__ == "__main__": - logging.basicConfig(level=logging.DEBUG, - format="[%(asctime)s] %(levelname)s [%(name)s.%(funcName)s:%(lineno)d] %(message)s", - datefmt="%H:%M:%S") - log = logging.getLogger(__name__) - - wdir = os.path.dirname(os.path.realpath(__file__)) - try: - pid = os.fork() - if pid == 0: - os.setsid() - pid2 = os.fork() - if pid2 == 0: - os.umask(0) - os.chdir(wdir) - daemon = ledd.daemon.Daemon() - else: - sys.exit() - else: - sys.exit() - except OSError as e: - log.fatal("Start failed: %s", e)