diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml new file mode 100644 index 0000000000000000000000000000000000000000..0269507939d6024efb50cd939dc44e61d5a6f822 --- /dev/null +++ b/.gitlab-ci.yml @@ -0,0 +1,67 @@ +# This file is a template, and might need editing before it works on your project. +# This is a sample GitLab CI/CD configuration file that should run without any modifications. +# It demonstrates a basic 3 stage CI/CD pipeline. Instead of real tests or scripts, +# it uses echo commands to simulate the pipeline execution. +# +# A pipeline is composed of independent jobs that run scripts, grouped into stages. +# Stages run in sequential order, but jobs within stages run in parallel. +# +# For more information, see: https://docs.gitlab.com/ee/ci/yaml/index.html#stages +# +# You can copy and paste this template into a new `.gitlab-ci.yml` file. +# You should not add this template to an existing `.gitlab-ci.yml` file by using the `include:` keyword. +# +# To contribute improvements to CI/CD templates, please follow the Development guide at: +# https://docs.gitlab.com/ee/development/cicd/templates.html +# This specific template is located at: +# https://gitlab.com/gitlab-org/gitlab/-/blob/master/lib/gitlab/ci/templates/Getting-Started.gitlab-ci.yml + +stages: # List of stages for jobs, and their order of execution + - lint + - deploy + +default: + image: + name: labinfo.ing.he-arc.ch:5050/igib/shared/ci-docker/poetry1.2.0-python3.10 + entrypoint: [ "" ] + +lint-black: + stage: lint + tags: + - docker + rules: + - if: $CI_PIPELINE_SOURCE == "merge_request_event" + when: always + allow_failure: false + script: + - poetry install --only lint + - poetry run black --check ms210/ + +lint-pylint: + stage: lint + tags: + - docker + rules: + - if: $CI_PIPELINE_SOURCE == "merge_request_event" + when: always + allow_failure: false + script: + - poetry install --only lint + - poetry run pylint --fail-under=8 ms210/ + +deploy-job: # This job runs only when the merge is accepted + stage: deploy # It only runs when *both* jobs in the test stage complete successfully. + tags: + - docker + environment: production + rules: + - if: $CI_COMMIT_BRANCH == "main" && $CI_PIPELINE_SOURCE == "push" + when: always + - when: never + + script: + - poetry install --only main + - poetry config repositories.gitlab https://labinfo.ing.he-arc.ch/gitlab/api/v4/projects/$CI_PROJECT_ID/packages/pypi + - poetry config http-basic.gitlab gitlab-ci-token "$CI_JOB_TOKEN" + - poetry build + - poetry publish --repository gitlab \ No newline at end of file diff --git a/README.md b/README.md index 3eb5efee50f95899d093970e3fd0be5e8448290f..f0a62e82d9e2e78ae20bc360474fd5571c7b1411 100644 --- a/README.md +++ b/README.md @@ -2,6 +2,14 @@ This repository contains a small python module to control the MS210 Channel Mixer by Advanced Illumination +> **NOTE**: +> +> For linux user add your user to the *dialout* group to access the serial port +> +> ```bash +> sudo usermod -aG dialout $USER +> ``` + ## Dependencies | Name | Version | @@ -9,13 +17,57 @@ This repository contains a small python module to control the MS210 Channel Mixe | Python | >= 3.10 | | Poetry | >= 1.2.0 | -## Add package +Install python dependencies: + +```bash +poetry install +``` + +## App + +A QT based graphical interface to control the **MS210** is available and can be run with the following command: + +```bash +poetry run controller +``` + +![UI](img/ms210_ui.png) + +## Add the MS210 package + +Instructions to add the MS210 package to your project -TODO: +### Poetry -## Linux -For linux user add your user to the *dialout* group to access the serial port +Add a secondary source to your poetry project ```bash -sudo usermod -aG dialout $USER +poetry source add -s igib https://labinfo.ing.he-arc.ch/gitlab/api/v4/projects/2417/packages/pypi/simple +``` + +Add **ms210** package + +```bash +poetry add ms210 --source igib +``` + +### PIP + +```bash +pip install pyrsvp --index-url https://labinfo.ing.he-arc.ch/gitlab/api/v4/projects/2417/packages/pypi/simple/ +``` + +## Usage + +```python +dev = MS210("/dev/ttyUSB0") + +# Set the red value +success, msg = dev.set_value("R", 500) +if not success: + print(msg) + +# Get the green value +green = dev.get_value("G") +print(green) ``` \ No newline at end of file diff --git a/app/ms210_controller.py b/app/ms210_controller.py index fdde68979a13d915953140773c4e1a62a54f16bf..6f88cc63239c88a0a800190ba1b818c5e01ec266 100644 --- a/app/ms210_controller.py +++ b/app/ms210_controller.py @@ -1,8 +1,9 @@ import sys from typing import Literal +from PyQt6.QtGui import QCloseEvent from PyQt6.QtWidgets import QApplication, QMainWindow, QMessageBox -from ms210_ui import Ui_MainWindow +from app.ms210_ui import Ui_MainWindow from ms210.driver import MS210, MS210InitFailed import serial.tools.list_ports @@ -139,11 +140,18 @@ class Window(QMainWindow, Ui_MainWindow): self.blue_slider.setValue(self.blue_sb.value()) def error_msg(self, msg : str): - QMessageBox.critical(None, "Error", msg) + QMessageBox.critical(None, "Error", msg) -if "__main__" == __name__: + def closeEvent(self, a0: QCloseEvent) -> None: + self._ms210 = None + return super().closeEvent(a0) + +def main(): app = QApplication(sys.argv) win = Window() win.show() - sys.exit(app.exec()) \ No newline at end of file + sys.exit(app.exec()) + +if "__main__" == __name__: + main() \ No newline at end of file diff --git a/img/ms210_ui.png b/img/ms210_ui.png new file mode 100644 index 0000000000000000000000000000000000000000..bb48fb3e25012a88d7ef7c39fe535a43147ef168 Binary files /dev/null and b/img/ms210_ui.png differ diff --git a/ms210/driver.py b/ms210/driver.py index 8d3c729cc07a6a2fd01846f0a3c4d1aa5d5ded4f..200b6de14e7423a9323d3d17f86a18fd21097f67 100644 --- a/ms210/driver.py +++ b/ms210/driver.py @@ -1,24 +1,31 @@ -# This module contains a class to control the MS210 device +"""This module contains a class to control the MS210 device +""" -import serial from typing import Literal from dataclasses import dataclass +import serial _BAUD = 19200 _CHANNELS = ["IR", "R", "B", "G"] _MAX_VALUE = 1000 _MIN_VALUE = 0 + @dataclass class Channel: - name : str - index : int - value : int + """Holds channel attributes""" + + name: str + index: int + value: int + class MS210InitFailed(Exception): - pass + """MS210 serial communication failed""" + class MS210: + """MS210 driver class""" def __init__(self, port: str = "/dev/ttyUSB0"): """Constructor of the MS210 object @@ -30,13 +37,13 @@ class MS210: Raises ------ - MS210InitFailed if device not found + MS210InitFailed if device not found """ - + self._ser = serial.Serial(timeout=2) self._ser.port = port self._ser.baudrate = _BAUD - + self.channels = [] for index, channel in enumerate(_CHANNELS): self.channels.append(Channel(channel, index, 0)) @@ -52,11 +59,10 @@ class MS210: raise MS210InitFailed(msg) def __del__(self): - """Destructor - """ + """Destructor""" for channel in _CHANNELS: self.set_value(channel, 0) - + self._ser.close() def __check_limits(self, value: int) -> bool: @@ -74,10 +80,10 @@ class MS210: if value > _MAX_VALUE: return False - + if value < _MIN_VALUE: return False - + return True def __get_current_values(self) -> tuple[bool, str]: @@ -90,7 +96,7 @@ class MS210: """ buf = "" - + for channel in self.channels: try: self._ser.write(f"R{channel.index}".encode()) @@ -108,7 +114,7 @@ class MS210: channel.value = int(buf[2:].decode().strip()) return (True, "OK") - + def get_value(self, channel: Literal["IR", "R", "B", "G"]) -> int: """Get the value of a channel @@ -124,7 +130,9 @@ class MS210: return [x for x in self.channels if x.name == channel][0].value - def set_value(self, channel: Literal["IR", "R", "B", "G"], value : int = 0) -> tuple[bool, str]: + def set_value( + self, channel: Literal["IR", "R", "B", "G"], value: int = 0 + ) -> tuple[bool, str]: """Set the value of a channel Parameters @@ -139,9 +147,12 @@ class MS210: tuple: bool, str success, an error msg in not success """ - + if not self.__check_limits(value): - return (False, f"Value {value} is out of bound. {_MIN_VALUE} < value < {_MAX_VALUE}") + return ( + False, + f"Value {value} is out of bound. {_MIN_VALUE} < value < {_MAX_VALUE}", + ) _channel = [x for x in self.channels if x.name == channel][0] if value == _channel.value: @@ -160,4 +171,4 @@ class MS210: _channel.value = int(buf[2:].decode().strip()) - return (True, "OK") \ No newline at end of file + return (True, "OK") diff --git a/poetry.lock b/poetry.lock index bad3dbe1c936adfc6a962e8f340bfa97b400c2f7..7328805a7cd11e8781f346d7bfd59c62a7e6f554 100644 --- a/poetry.lock +++ b/poetry.lock @@ -148,7 +148,7 @@ testutils = ["gitpython (>3)"] name = "PyQt6" version = "6.4.2" description = "Python bindings for the Qt cross platform application toolkit" -category = "main" +category = "dev" optional = false python-versions = ">=3.6.1" @@ -174,7 +174,7 @@ qt6-tools = ">=6.4.3.1.2,<6.4.3.2" name = "PyQt6-Qt6" version = "6.4.3" description = "The subset of a Qt installation needed by PyQt6." -category = "main" +category = "dev" optional = false python-versions = "*" @@ -182,7 +182,7 @@ python-versions = "*" name = "PyQt6-sip" version = "13.6.0" description = "The sip module support for PyQt6" -category = "main" +category = "dev" optional = false python-versions = ">=3.7" @@ -269,7 +269,7 @@ python-versions = ">=3.8" [metadata] lock-version = "1.1" python-versions = "^3.10" -content-hash = "4bbcc48f2001eebe9ea479fb0e898e9b60db95e30d73173cace2241b922edadf" +content-hash = "5c5b64c360238d047ead8cb0b9f4a25f0af1e8b4baaf737fad2838b3db4dc40f" [metadata.files] astroid = [ diff --git a/pyproject.toml b/pyproject.toml index d77d2353cabac165fd99223ee4139abce978ee05..69c8f13272648de783d78ff7c4a1854976b5fecf 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,15 +4,18 @@ version = "0.1.0" description = "Drive the MS210 device" authors = ["Mattia Gallacchi "] readme = "README.md" +packages = [ + {include = "app/"} +] [tool.poetry.dependencies] python = "^3.10" pyserial = "^3.5" -PyQt6 = "6.4.2" [tool.poetry.group.dev.dependencies] pyqt6-tools = "^6.4.2.3.3" +PyQt6 = "6.4.2" [tool.poetry.group.lint.dependencies] @@ -22,3 +25,6 @@ pylint = "^3.1.0" [build-system] requires = ["poetry-core"] build-backend = "poetry.core.masonry.api" + +[tool.poetry.scripts] +controller = "app.ms210_controller:main"