diff --git a/.github/workflows/code-lint.yml b/.github/workflows/code-lint.yml index 374a544..ced90ee 100644 --- a/.github/workflows/code-lint.yml +++ b/.github/workflows/code-lint.yml @@ -8,8 +8,36 @@ jobs: steps: - uses: actions/checkout@v4 - run: pip3 install black - - run: | - black --version - black --check --diff . + - run: black --version + - run: black --check --diff . + flake8: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + - run: pip3 install flake8 + - run: flake8 --exclude .git,__pycache__ --max-line-length 88 python3 + isort: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + - run: pip3 install isort + - run: isort --version + - run: cd python3 && isort -c --diff --profile black . && cd .. + + pytest: + needs: [black, flake8, isort] + runs-on: ubuntu-latest + strategy: + matrix: + python-version: ['3.7', '3.8', '3.9', '3.10', 'pypy3.7', 'pypy3.8', 'pypy3.9'] + steps: + - uses: actions/checkout@v3 + - uses: actions/setup-python@v4 + with: + python-version: ${{matrix.python-version}} + cache: 'pip' + - run: pip install -r python3/requirements.txt + - run: python3 -m pytest python3 -vv + diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..ce8b0ea --- /dev/null +++ b/.gitignore @@ -0,0 +1,217 @@ +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +*$py.class + +# C extensions +*.so + +# Distribution / packaging +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +share/python-wheels/ +*.egg-info/ +.installed.cfg +*.egg +MANIFEST + +# PyInstaller +# Usually these files are written by a python script from a template +# before PyInstaller builds the exe, so as to inject date/other infos into it. +*.manifest +*.spec + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ +.nox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*.cover +*.py,cover +.hypothesis/ +.pytest_cache/ +cover/ + +# Translations +*.mo +*.pot + +# Django stuff: +*.log +local_settings.py +db.sqlite3 +db.sqlite3-journal + +# Flask stuff: +instance/ +.webassets-cache + +# Scrapy stuff: +.scrapy + +# Sphinx documentation +docs/_build/ + +# PyBuilder +.pybuilder/ +target/ + +# Jupyter Notebook +.ipynb_checkpoints + +# IPython +profile_default/ +ipython_config.py + +# pyenv +# For a library or package, you might want to ignore these files since the code is +# intended to run in multiple environments; otherwise, check them in: +# .python-version + +# pipenv +# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. +# However, in case of collaboration, if having platform-specific dependencies or dependencies +# having no cross-platform support, pipenv may install dependencies that don't work, or not +# install all needed dependencies. +#Pipfile.lock + +# poetry +# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. +# This is especially recommended for binary packages to ensure reproducibility, and is more +# commonly ignored for libraries. +# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control +#poetry.lock + +# pdm +# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control. +#pdm.lock +# pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it +# in version control. +# https://pdm.fming.dev/#use-with-ide +.pdm.toml + +# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm +__pypackages__/ + +# Celery stuff +celerybeat-schedule +celerybeat.pid + +# SageMath parsed files +*.sage.py + +# Environments +.env +.venv +env/ +venv/ +ENV/ +env.bak/ +venv.bak/ + +# Spyder project settings +.spyderproject +.spyproject + +# Rope project settings +.ropeproject + +# mkdocs documentation +/site + +# mypy +.mypy_cache/ +.dmypy.json +dmypy.json + +# Pyre type checker +.pyre/ + +# pytype static type analyzer +.pytype/ + +# Cython debug symbols +cython_debug/ + +# PyCharm +# JetBrains specific template is maintained in a separate JetBrains.gitignore that can +# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore +# and can be added to the global gitignore or merged into this file. For a more nuclear +# option (not recommended) you can uncomment the following to ignore the entire idea folder. +#.idea/ + + +# Prerequisites +*.d + +# Object files +*.o +*.ko +*.obj +*.elf + +# Linker output +*.ilk +*.map +*.exp + +# Precompiled Headers +*.gch +*.pch + +# Libraries +*.lib +*.a +*.la +*.lo + +# Shared objects (inc. Windows DLLs) +*.dll +*.so +*.so.* +*.dylib + +# Executables +*.exe +*.out +*.app +*.i*86 +*.x86_64 +*.hex + +# Debug files +*.dSYM/ +*.su +*.idb +*.pdb + +# Kernel Module Compile Results +*.mod* +*.cmd +.tmp_versions/ +modules.order +Module.symvers +Mkfile.old +dkms.conf + +tags +tags.* diff --git a/python3/controller_simulated.py b/python3/controller_simulated.py new file mode 100644 index 0000000..da5742f --- /dev/null +++ b/python3/controller_simulated.py @@ -0,0 +1,196 @@ +import logging +from typing import List + +import parse +from pydantic import BaseModel, Field + +logger = logging.getLogger(__name__) + + +class LED(BaseModel): + r: int = 0 + g: int = 0 + b: int = 0 + + +class Controller(BaseModel): + mode: int = 0 + step: int = 0 + atx: bool = True + echo: bool = False + uart5con: bool = False + timeout: int = 400000 + leds: List[LED] = Field(default=[LED()] * 432, max_items=432, min_items=432) + + +controller = Controller() + + +def decode_and_call(input: str, formatstring: str, callfunction): + decode = parse.parse(formatstring, input) + if decode is None: + return None + logger.debug(decode) + ret = callfunction(decode.named) + logger.debug(ret) + return ret + + +def decode(input: str) -> str: + ret = decode_and_call(input, "set mode {mode:d}", decode_mode) + if ret is not None: + return ret + ret = decode_and_call( + input, + "set limit {r:d} {g:d} {b:d} {offset:d} {length:d} {mode:d}", + decode_limit, + ) + if ret is not None: + return ret + ret = decode_and_call(input, "set step {step:d}", decode_step) + if ret is not None: + return ret + ret = decode_and_call(input, "set rgb {r:d} {g:d} {b:d}", decode_rgb) + if ret is not None: + return ret + ret = decode_and_call(input, "set timeout {timeout:d}", decode_timeout) + if ret is not None: + return ret + ret = decode_and_call(input, "set lights {state:w}", decode_lights) + if ret is not None: + return ret + ret = decode_and_call(input, "set atx {atx:w}", decode_atx) + if ret is not None: + return ret + ret = decode_and_call(input, "set uart5con {uart5con:w}", decode_uart5con) + if ret is not None: + return ret + + +def decode_mode(input): + global controller + if input["mode"] > 2 or input["mode"] < 0: + return "[ERROR]: Stepmode only valid between 0-2\n" + controller.mode = input["mode"] + return "[OK]: Set stepmode\n" + + +def decode_limit(input): + global controller + if ( + input["r"] < 0 + or input["r"] > 255 + or input["g"] < 0 + or input["g"] > 255 + or input["b"] < 0 + or input["b"] > 255 + or input["offset"] < 0 + or input["length"] < 0 + or input["offset"] + input["length"] > len(controller.leds) + or input["mode"] < 0 + or input["mode"] > 2 + ): + return """[ERROR]: Something went wrong! +[ERROR]: set limit r g b offset length mode\n""" + for i in range(input["length"], input["offset"] + input["length"]): + controller.leds[i].r = input["r"] + controller.leds[i].g = input["g"] + controller.leds[i].b = input["b"] + return "[OK]: set limit rgb done\n" + + +def decode_step(input): + global controller + if input["step"] < 0 or input["step"] >= 2 ^ 32: + return "[ERROR] Missing step argument\n" + controller.step = input["step"] + return "[OK]: Set step length\n" + + +def decode_rgb(input): + global controller + if "r" not in input: + return "[ERROR]: Argument to short, Red missing for setrgb\n" + if "g" not in input: + return "[ERROR]: Argument to short, Green missing for setrgb\n" + if "b" not in input: + return "[ERROR]: Argument to short, Blue missing for setrgb\n" + + if ( + input["r"] < 0 + or input["r"] >= 255 + or input["g"] < 0 + or input["g"] >= 255 + or input["b"] < 0 + or input["b"] >= 255 + ): + return "[ERROR]: Value Error\n" + for led in controller.leds: + led.r = input["r"] + led.g = input["g"] + led.b = input["b"] + return "[OK] set rgb values\n" + + +def decode_timeout(input): + global controller + if input["timeout"] < 0 or input["timeout"] > 2 ^ 32: + return "[ERROR]: Error in decoding timeout\n" + controller.timeout = input["timeout"] + return "[OK]: Set timeout\n" + + +def decode_lights(input): + global controller + if input["state"] == "on": + for led in controller.leds: + led.r = 0 + led.g = 0 + led.b = 0 + controller.atx = True + return "[OK]: lights on\n" + if input["state"] == "off": + for led in controller.leds: + led.r = 255 + led.g = 255 + led.b = 255 + controller.atx = False + return "[OK]: lights off\n" + return "[ERROR]: lights only takes on|off\n" + + +def decode_atx(input): + global controller + if input["atx"] == "on": + controller.atx = True + return "[ATX] Send endable signal\n[OK]: ATX on\n" + if input["atx"] == "off": + controller.atx = False + return "[OK]: remove enable signal\n[ATX]: Atx off\n" + return "" + + +def decode_uart5con(input): + global controller + if input["uart5con"] == "on": + controller.uart5con = True + return "[OK]: Enable uart5 consol\n" + if input["uart5con"] == "off": + controller.uart5con = False + return "[OK]: Disable uart5 consol\n" + return "[ERROR]: uart5con on or off\n" + + +def simulate_behavior(input: str) -> str: + mode = parse.compile("set mode {mode:d}") + ret = mode.parse(input) + if ret is None: + return ret + return ret.named + + +if __name__ == "__main__": + ret = simulate_behavior("set mode 0") + print(ret) + ret = simulate_behavior("set limit 0") + print(ret) diff --git a/python3/lightcontroller.py b/python3/lightcontroller.py index 8a4f95b..bfb58b7 100644 --- a/python3/lightcontroller.py +++ b/python3/lightcontroller.py @@ -1,11 +1,11 @@ #!/usr/bin/python3 -from threading import Thread -import time +import logging import os -import serial import queue +import time +from threading import Thread -import logging +import serial logger = logging.getLogger(__name__) @@ -153,7 +153,7 @@ def __init__( self.step = 4 self.timeout = 4 * 60 * 60 * 1000 # 4 hours in us message = self.returnqueue.get(timeout=20) - if not "ready" in message: + if "ready" not in message: raise ValueError("Backend took more than 20 seconds to initialise") logger.info("LightController Frontend initialised") @@ -185,7 +185,7 @@ def write(self, message, delay=0, retexpected=False): def abortprevious(self): self.abortqueue.put("abort") message = self.write("bla", delay=0, retexpected=True) - if not "ready" in message: + if "ready" not in message: raise ValueError("Backend not properly reset") def backgroundthreadalive(self): @@ -270,8 +270,6 @@ def set_lights(self, on: bool, delay=0): if __name__ == "__main__": - import time - # backend = SerialBackend('/dev/ttyUSB0') # backend.clearup() # time.sleep(1) diff --git a/python3/requirements.txt b/python3/requirements.txt new file mode 100644 index 0000000..5dd058d --- /dev/null +++ b/python3/requirements.txt @@ -0,0 +1,4 @@ +serial +parse +pydantic +pytest diff --git a/python3/test_controller_simulated.py b/python3/test_controller_simulated.py new file mode 100644 index 0000000..323b317 --- /dev/null +++ b/python3/test_controller_simulated.py @@ -0,0 +1,106 @@ +from controller_simulated import decode + + +def test_decode_mode(): + assert decode("set mode 0") == "[OK]: Set stepmode\n" + assert decode("set mode -1") == "[ERROR]: Stepmode only valid between 0-2\n" + assert decode("set mode 3") == "[ERROR]: Stepmode only valid between 0-2\n" + + +def test_decode_limit(): + assert decode("set limit 0 0 0 10 5 1") == "[OK]: set limit rgb done\n" + assert ( + decode("set limit -1 0 0 10 5 1") + == """[ERROR]: Something went wrong! +[ERROR]: set limit r g b offset length mode\n""" + ) + assert ( + decode("set limit 256 0 0 10 5 1") + == """[ERROR]: Something went wrong! +[ERROR]: set limit r g b offset length mode\n""" + ) + assert ( + decode("set limit 0 -1 0 10 5 1") + == """[ERROR]: Something went wrong! +[ERROR]: set limit r g b offset length mode\n""" + ) + assert ( + decode("set limit 0 256 0 10 5 1") + == """[ERROR]: Something went wrong! +[ERROR]: set limit r g b offset length mode\n""" + ) + assert ( + decode("set limit 0 0 -1 10 5 1") + == """[ERROR]: Something went wrong! +[ERROR]: set limit r g b offset length mode\n""" + ) + assert ( + decode("set limit 0 0 256 10 5 1") + == """[ERROR]: Something went wrong! +[ERROR]: set limit r g b offset length mode\n""" + ) + assert ( + decode("set limit 0 0 0 -10 5 1") + == """[ERROR]: Something went wrong! +[ERROR]: set limit r g b offset length mode\n""" + ) + assert ( + decode("set limit 0 0 0 10 -3 1") + == """[ERROR]: Something went wrong! +[ERROR]: set limit r g b offset length mode\n""" + ) + assert ( + decode("set limit 0 0 0 100 400 1") + == """[ERROR]: Something went wrong! +[ERROR]: set limit r g b offset length mode\n""" + ) + assert ( + decode("set limit 0 0 0 10 5 -1") + == """[ERROR]: Something went wrong! +[ERROR]: set limit r g b offset length mode\n""" + ) + assert ( + decode("set limit 0 0 0 10 5 3") + == """[ERROR]: Something went wrong! +[ERROR]: set limit r g b offset length mode\n""" + ) + + +def test_decode_step(): + assert decode("set step 1") == "[OK]: Set step length\n" + assert decode("set step -1") == "[ERROR] Missing step argument\n" + assert decode("set step 4394967296") == "[ERROR] Missing step argument\n" + + +def test_decode_rgb(): + assert decode("set rgb 1 1 1") == "[OK] set rgb values\n" + assert decode("set rgb -1 1 1") == "[ERROR]: Value Error\n" + assert decode("set rgb 256 1 1") == "[ERROR]: Value Error\n" + assert decode("set rgb 1 -1 1") == "[ERROR]: Value Error\n" + assert decode("set rgb 1 256 1") == "[ERROR]: Value Error\n" + assert decode("set rgb 1 1 -1") == "[ERROR]: Value Error\n" + assert decode("set rgb 1 1 256") == "[ERROR]: Value Error\n" + + +def test_timeout(): + assert decode("set timeout 5") == "[OK]: Set timeout\n" + assert decode("set timeout -5") == "[ERROR]: Error in decoding timeout\n" + assert decode("set timeout 4394967296") == "[ERROR]: Error in decoding timeout\n" + + +def test_lights(): + assert decode("set lights off") == "[OK]: lights off\n" + assert decode("set lights on") == "[OK]: lights on\n" + assert decode("set lights asdf") == "[ERROR]: lights only takes on|off\n" + + +def test_set_atx(): + assert decode("set atx on") == "[ATX] Send endable signal\n[OK]: ATX on\n" + assert decode("set atx off") == "[OK]: remove enable signal\n[ATX]: Atx off\n" + assert decode("set atx asdf") == "" + + +def test_set_uart5con(): + assert decode("set uart5con on") == "[OK]: Enable uart5 consol\n" + assert decode("set uart5con off") == "[OK]: Disable uart5 consol\n" + assert decode("set uart5con asdf") == "[ERROR]: uart5con on or off\n"