From 20c5d9c4ba6425139fb12f406e74f7d3253baf43 Mon Sep 17 00:00:00 2001 From: Andrea Mistrali Date: Mon, 1 May 2023 07:04:56 +0200 Subject: [PATCH] First version --- .gitignore | 8 +- build.sh | 9 ++ pulses/__init__.py | 0 pulses/pulses.py | 318 +++++++++++++++++++++++++++++++++++++++++++ pyproject.toml | 3 + requirements-dev.txt | 2 + requirements.txt | 3 + setup.cfg | 21 +++ 8 files changed, 361 insertions(+), 3 deletions(-) create mode 100755 build.sh create mode 100644 pulses/__init__.py create mode 100644 pulses/pulses.py create mode 100644 pyproject.toml create mode 100644 requirements-dev.txt create mode 100644 requirements.txt create mode 100644 setup.cfg diff --git a/.gitignore b/.gitignore index c71a10f..3b6abca 100644 --- a/.gitignore +++ b/.gitignore @@ -5,7 +5,8 @@ .LSOverride # Icon must end with two \r -Icon +Icon + # Thumbnails ._* @@ -224,8 +225,8 @@ tags crash.log # Exclude all .tfvars files, which are likely to contain sentitive data, such as -# password, private keys, and other secrets. These should not be part of version -# control as they are data points which are potentially sensitive and subject +# password, private keys, and other secrets. These should not be part of version +# control as they are data points which are potentially sensitive and subject # to change depending on the environment. # *.tfvars @@ -248,3 +249,4 @@ override.tf.json .terraformrc terraform.rc +.python-version diff --git a/build.sh b/build.sh new file mode 100755 index 0000000..ad90ac1 --- /dev/null +++ b/build.sh @@ -0,0 +1,9 @@ +#!/bin/sh + +if [ "$1" = "clean" ]; then + rm -rf build + rm -rf dist + rm -rf *.egg-info +fi + +python -m build . diff --git a/pulses/__init__.py b/pulses/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/pulses/pulses.py b/pulses/pulses.py new file mode 100644 index 0000000..0f77543 --- /dev/null +++ b/pulses/pulses.py @@ -0,0 +1,318 @@ +import logging +import sys +import math +import time +import threading + +if sys.platform == "linux": + import RPi.GPIO as GPIO + + +class ledWorker(threading.Thread): + """ + This is the thread that runs the main loop + and actually drives the LED + """ + + def __init__(self, parent): + super().__init__(name=parent.name, daemon=True) + self.log = logging.getLogger(self.__class__.__name__) + self.parent = parent + + self.changed = self.parent.changed + + self.stop_event = threading.Event() + + def run(self): + self.log.debug(f"running {self.name} main loop...") + self.parent.loop() + + def stop(self): + self.stop_event.set() + self.log.debug(f"stopping {self.name}...") + + +class ledPulse: + """ + There are 4 states of pulsing: + - on: steady light at value; + - off: turn off led; + - normal: pulse with delay; + - fast: pulse with delay/2; + """ + + delayModes = ['constant', 'sin', 'cos'] + valueModes = ['on', 'off', 'linear', 'sin', 'cos'] + modes = {'delay': {}, 'value': {}} + + defaultSet = { + 'min': 2, + 'max': 100, + 'delay': 0.01, + 'delayMode': 'constant', + 'initialMode': None, + 'valueMode': 'linear', + 'finalMode': None + } + + def __init__(self, gpio, name="led0"): + self.log = logging.getLogger(self.__class__.__name__) + self.gpio = gpio + self.name = name + + self.changed = threading.Event() + self.initialStep = 0 + self._set = False + + if sys.platform == "linux": + GPIO.setwarnings(False) # disable warnings + GPIO.setmode(GPIO.BCM) # set pin numbering system + # set GPIO for output + GPIO.setup(self.gpio, GPIO.OUT) + + # create PWM instance with frequency + self.pwm = GPIO.PWM(self.gpio, 120) + # self.setValue = self.pwm.ChangeDutyCycle + self.supported = True + # self.pwm.start(0) # start pwm with value 0 + else: + # Not supported, skip setValue + # self.setValue = print + self.supported = False + # self.setValue = lambda x: x + + self.log.info(f"platform '{sys.platform}' " + + f"support: {sys.platform == 'linux'}") + + for dm in self.delayModes: + self.register_delay_method(dm, eval(f"delay_{dm}")) + + for vm in self.valueModes: + self.register_value_method(vm, eval(f"value_{vm}")) + + # Add thread placeholder + self.worker = None + + # Set default values + for key, value in self.defaultSet.items(): + setattr(self, key, value) + + def setValue(self, value): + if self.supported: + self.pwm.ChangeDutyCycle(value) + else: + # on unsupported platforms, we just print the value + print(value) + + # def set(self, min=2, max=50, delay=1, + # delayMode='constant', + # valueMode='linear', + # initialMode=None, + # finalMode=None): + def set(self, **kwargs): + + # for key, value in self.defaultSet.items(): + # setattr(self, key, value) + + for key, value in kwargs.items(): + if key in self.defaultSet.keys(): + setattr(self, key, value) + self._set = True + else: + self.log.warning(f"set: unknown parameter '{key}'") + self.delta = self.max - self.min + + # self.min = min + # self.max = max + # self.delay = delay + # self.delayMode = delayMode + # self.valueMode = valueMode + # self.initialMode = initialMode + # self.finalMode = finalMode + + if self._set: + if self.worker and self.worker.is_alive(): + print("worker running, notify change") + self.worker.changed.set() + + + # VALUES CALCULATION + def calcValues(self, initialStep, finalStep): + + if not (self.delayMode in self.modes['delay'] + and self.valueMode in self.modes['value']): + self.log.error(f"Invalid parameters, delayMode:{self.delayMode}, " + f"valueMode:{self.valueMode}") + return + + self.log.debug(f"fill cycle values table, valueMode:{self.valueMode}, " + f"delayMode:{self.delayMode}, min:{self.min}, " + f"max:{self.max}, delay:{self.delay}") + + values = [] + for step in range(initialStep, finalStep): + try: + delay = self.modes['delay'][self.delayMode](self, step) + except NameError: + self.log.error(f"delay function for '{self.delayMode}' " + "not found") + return + try: + value = self.modes['value'][self.valueMode](self, step) + except NameError: + self.log.error(f"value function for '{self.valueMode}' not found") + return + values.append((value, delay)) + return values + + def cycleValues(self): + return self.calcValues(0, 100) + + def initialValues(self): + return self.calcValues(0, 50) + + def finalValues(self): + return self.calcValues(50, 100) + + def start(self): + if self.supported: + self.pwm.start(0) + self.worker = ledWorker(self) + self.worker.start() + + def stop(self): + self.worker.stop() + self.worker.join() + if self.supported: + self.pwm.stop() + self.worker = None + + def loop(self): + + if not self._set: + self.log.error("led modes not set, use .set() first") + return + + while True: + + # Initial + if self.initialMode: + self.log.debug('running initial step') + vals = self.initialValues() + for value, delay in vals: + self.setValue(value) + time.sleep(delay) + print('end initial step') + self.log.debug('end initial step') + + vals = self.cycleValues() + while True: + if self.worker.stop_event.is_set(): + # self.worker._started.clear() + self.worker.stop_event.clear() + self.log.debug("bailing out from infinite loop") + return + + if self.worker.changed.is_set(): + self.changed.clear() + self.log.debug("parameters changed, recalculate values...") + break + for step in range(len(vals)): + value, delay = vals[step] + self.setValue(value) + time.sleep(delay) + + # Final + if self.finalMode: + print('running final step') + self.log.debug('running final step') + vals = self.finalValues() + for value, delay in vals: + self.setValue(value) + time.sleep(delay) + print('end final step') + self.log.debug('end final step') + + ### + # PLUGINS METHODS + ### + def register_value_method(self, name, fn): + self.register_method('value', name, fn) + + def register_delay_method(self, name, fn): + self.register_method('delay', name, fn) + + def register_method(self, kind, name, fn): + if kind not in ['delay', 'value']: + self.log.warning(f"unknown kind {kind}") + return + + if name not in self.modes[kind]: + self.modes[kind][name] = fn + self.log.info(f"registered {kind} method '{name}'") + else: + self.log.warning(f"{kind} method '{name}' already defined") + + +#### +# Predefined functions +### +# Delays +### +def delay_sin(obj, step): + return math.sin(math.radians(1.8*step)) * obj.delay + + +def delay_cos(obj, step): + return abs(math.cos(math.radians(1.8*step))) * obj.delay + + +def delay_constant(obj, step): + return obj.delay + + +### +# Values +### +def value_on(obj, step): + """ + Always on at "max" brightness + """ + + return obj.max + + +def value_off(obj, step): + """ + Always off + """ + + return 0 + + +def value_linear(obj, step): + """ + Saw patterned value, 0-1-0 + """ + + if step < 50: + return obj.delta*step/50 + obj.min + else: + return obj.delta * (100-step)/50 + obj.min + + +def value_sin(obj, step): + """ + Sinusoidal values, 0-1-0 /\ + """ + + radians = math.radians(1.8 * step) + return obj.delta * math.sin(radians) + obj.min + + +def value_cos(obj, step): + """ + Absolute Cosinusoidal values, 1-0-1 \\// + """ + radians = math.radians(1.8 * step) + return obj.delta * abs(math.cos(radians)) + obj.min diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..9787c3b --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,3 @@ +[build-system] +requires = ["setuptools", "wheel"] +build-backend = "setuptools.build_meta" diff --git a/requirements-dev.txt b/requirements-dev.txt new file mode 100644 index 0000000..3ddafa3 --- /dev/null +++ b/requirements-dev.txt @@ -0,0 +1,2 @@ +build +ipython diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..053a674 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,3 @@ +build +PyYAML +rpi-backlight diff --git a/setup.cfg b/setup.cfg new file mode 100644 index 0000000..c0a7fe4 --- /dev/null +++ b/setup.cfg @@ -0,0 +1,21 @@ +[metadata] +name = mblight +version = 0.90 +author = Andrea Mistrali +author_email = akelge@gmail.com +platform = linux +description = RPI led(s) and display brightness +long_description = file: README.md +keywords = graylog, py3 +license = BSD 3-Clause License + +[options] +packages = mblight +python_requires = >=3 +include_package_data = True +install_requires = + PyYAML + +[options.package_data] +* = + config.yaml.example