From af6b9ca06068b28081eb33804cec753046916372 Mon Sep 17 00:00:00 2001 From: Driver Generator 2 Date: Thu, 19 Oct 2023 07:12:56 +0000 Subject: [PATCH] "Update SVM4x driver from SVM4X model version 2.0.0" --- .gitlab-ci.yml | 92 +++- CHANGELOG.rst | 2 +- LICENSE | 2 +- MANIFEST.in | 1 + README.md | 76 +-- ci/checkin_doc.sh | 18 +- conftest.py | 16 +- docs/api.rst | 12 +- docs/build-documentation.rst | 14 + docs/conf.py | 32 +- docs/development.rst | 9 + docs/execute-measurements.rst | 21 + docs/index.rst | 5 +- docs/installation.rst | 4 +- docs/quickstart.rst | 16 +- docs/requirements.txt | 4 +- docs/run-tests.rst | 22 + .../algorithm_tuning_example_uart_svm4x.py | 63 +++ examples/example_usage_uart_svm4x.py | 44 ++ metadata.yml | 7 + sensirion_uart_svm4x/__init__.py | 8 + sensirion_uart_svm4x/commands.py | 331 +++++++++++++ sensirion_uart_svm4x/device.py | 443 ++++++++++++++++++ sensirion_uart_svm4x/response_provider.py | 23 + sensirion_uart_svm4x/result_types.py | 74 +++ sensirion_uart_svm4x/version.py | 5 + setup.cfg | 2 +- setup.py | 25 +- tests/test_svm4x.py | 179 +++++++ 29 files changed, 1421 insertions(+), 129 deletions(-) create mode 100644 MANIFEST.in create mode 100644 docs/build-documentation.rst create mode 100644 docs/development.rst create mode 100644 docs/execute-measurements.rst create mode 100644 docs/run-tests.rst create mode 100644 examples/algorithm_tuning_example_uart_svm4x.py create mode 100644 examples/example_usage_uart_svm4x.py create mode 100644 metadata.yml create mode 100644 sensirion_uart_svm4x/__init__.py create mode 100644 sensirion_uart_svm4x/commands.py create mode 100644 sensirion_uart_svm4x/device.py create mode 100644 sensirion_uart_svm4x/response_provider.py create mode 100644 sensirion_uart_svm4x/result_types.py create mode 100644 sensirion_uart_svm4x/version.py create mode 100644 tests/test_svm4x.py diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index ff71833..c6ee5f5 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -1,9 +1,16 @@ -include: https://gitlab/Sensirion/Python/ci-config-python/raw/master/gitlab-ci-template-v2.yml +include: + - project: Sensirion/Python/ci-config-python + ref: master + file: gitlab-ci-template-v2.yml + stages: - prepare - build - deploy +variables: + YQ_URL: https://github.com/mikefarah/yq/releases/download/v4.33.3/yq_linux_amd64 + check_rst_syntax: stage: prepare extends: .check_rst_syntax_v2 @@ -12,29 +19,28 @@ check_editorconfig: stage: prepare extends: .check_editorconfig_v2 - py3p6_linux_build: stage: build extends: .py3p6_linux_build_v2 - variables: - PYTEST_ADDOPTS: '-m "not needs_device"' + +py3p8_linux_sdist: + stage: build + extends: .py3p8_linux_sdist_v2 py3p8_linux_build: stage: build extends: .py3p8_linux_build_v2 - variables: - PYTEST_ADDOPTS: '-m "not needs_device"' - py3p8_64bit_win_build: stage: build extends: .py3p8_64bit_win_docker_build_v2 - variables: - PYTEST_ADDOPTS: '-m "not needs_device"' build_docs: stage: build - extends: .build_docs_v2 + tags: [ linux, docker ] + image: registry.gitlab.sensirion.lokal/sensirion/docker/docker-python:3.8-20.04-2.11.0 + variables: + SPHINXOPTS: "-W --keep-going" script: - python setup.py install - pip install -r docs/requirements.txt @@ -43,7 +49,7 @@ build_docs: after_script: - mv docs/_build/html/ public/ # everything in public/ will get published artifacts: - paths: [public] + paths: [ public ] expire_in: 1 week when: always @@ -54,7 +60,7 @@ deploy_stable: extends: .deploy_stable_v2 environment: name: pypi.org - url: https://pypi.org/project/sensirion_shdlc_svm41/ + url: https://pypi.org/project/sensirion_uart_svm4x/ before_script: - pip install twine~=1.12.1 script: @@ -65,16 +71,11 @@ deploy_stable: deploy_docs: stage: deploy - tags: [linux, docker] - image: registry.gitlab.sensirion.lokal/sensirion/docker/docker-python:3.8-20.04-2.6.0 - dependencies: [] - only: [master, tags] + tags: [ linux, docker ] + image: registry.gitlab.sensirion.lokal/sensirion/docker/docker-python:3.8-20.04-2.11.0 + dependencies: [ build_docs ] + only: [ master, tags ] script: - - python setup.py install - - pip install -r docs/requirements.txt - - cd docs - - make html - after_script: - chmod 777 ./ci/set_git_config.sh - ./ci/set_git_config.sh - chmod 777 ./ci/checkin_doc.sh @@ -83,7 +84,52 @@ deploy_docs: TODO_check: stage: prepare image: - name: registry.gitlab.sensirion.lokal/mso-sw/drivers/docker-driver-generator:0.2.0 + name: registry.gitlab.sensirion.lokal/mso-sw/drivers/docker-driver-generator:0.2.0 + tags: [ linux, docker ] + script: + - '! grep -rnw --exclude=.gitlab-ci.yml --exclude-dir=.git . -e "TODO"' + +metadata_check: + stage: prepare + image: + name: registry.gitlab.sensirion.lokal/mso-sw/drivers/docker-driver-generator:0.2.0 tags: [linux, docker] + before_script: + - apt-get -qq update && apt-get -qq install -y wget + - if ! [ -d downloads/ ]; then mkdir downloads; fi + - if ! [ -e downloads/yq ]; then wget --no-verbose $YQ_URL -O downloads/yq; fi + - cp downloads/yq /usr/local/bin/yq && chmod +x /usr/local/bin/yq script: - - '! grep -rnw --exclude=.gitlab-ci.yml --exclude-dir=.git . -e "TODO"' \ No newline at end of file + # check if metadata.yml exists + - > + if ! [ -f "metadata.yml" ]; then + echo "metadata.yml file not found" + exit 1 + fi + # check that dg_status is 'released' + - export DG_STATUS=$(yq ".dg_status.[]" ./metadata.yml) + - > + if [ $DG_STATUS != "released" ]; then + echo "dg_status in metadata.yml has to be 'released', not '$DG_STATUS'" + exit 1 + fi + # check that last_generated is not older than timestamp of last non-merge commit (+ 3 minutes) + - export IS_MANUALLY_MODIFIED=$(yq ".is_manually_modified" ./metadata.yml) + - > + if [ $IS_MANUALLY_MODIFIED = false ]; then + export LAST_GENERATED_TS=$(yq ".last_generated" ./metadata.yml) + export LAST_GENERATED_TS_EPOCH=$(date -d "$LAST_GENERATED_TS" +%s) + export LAST_NON_MERGE_COMMIT_TS=$(git log --format=%ad --date=iso-strict --no-merges -1) + export COMMIT_TS_EPOCH=$(date -d "$LAST_NON_MERGE_COMMIT_TS" +%s) + if [ $(($LAST_GENERATED_TS_EPOCH + 180)) -lt $COMMIT_TS_EPOCH ]; then + echo "'last_generated' timestamp in metadata.yml is older than commit timestamp ($LAST_GENERATED_TS vs $LAST_NON_MERGE_COMMIT_TS)" + exit 1 + fi + fi + # check that 'is_manually_modified' is set to true if commit is not from driver generator + - export LAST_NON_MERGE_COMMIT_AUTHOR=$(git log --format=%an --no-merges -1) + - > + if ! [ "$LAST_NON_MERGE_COMMIT_AUTHOR" = "Driver Generator 2" ] && [ "$IS_MANUALLY_MODIFIED" = false ]; then + echo "Last commit is not from Driver Generator. Please update 'is_manually_modified' in metadata.yml" + exit 1 + fi \ No newline at end of file diff --git a/CHANGELOG.rst b/CHANGELOG.rst index d5b49f4..2fff05f 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -3,4 +3,4 @@ CHANGELOG 0.1.0 ::::: -- Initial release of SVM41 SHDLC driver +- Initial release of SVM4X SHDLC driver diff --git a/LICENSE b/LICENSE index e53c5ee..f95f5fe 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ BSD 3-Clause License -Copyright (c) 2022, Sensirion AG +Copyright (c) 2023, Sensirion AG All rights reserved. Redistribution and use in source and binary forms, with or without diff --git a/MANIFEST.in b/MANIFEST.in new file mode 100644 index 0000000..18e0683 --- /dev/null +++ b/MANIFEST.in @@ -0,0 +1 @@ +include CHANGELOG.rst diff --git a/README.md b/README.md index 51f28fa..b7e3adf 100644 --- a/README.md +++ b/README.md @@ -1,21 +1,54 @@ -# Python SHDLC Driver for Sensirion SVM41 +# Python UART Driver for Sensirion SVM4X -This repository contains the Python driver to communicate with a Sensirion SVM41 sensor over UART using the SHDLC protocol. +This repository contains the Python driver to communicate with a Sensirion SVM4X sensor over UART using the SHDLC protocol. +SVM4X picture -## Usage +Click [here](https://www.sensirion.com/my-sgp-ek/) to learn more about the Sensirion SVM4X sensor. -See user manual at -[https://sensirion.github.io](https://sensirion.github.io/python-uart-svm41). +unknown -## Development + + + +## Connect the sensor + +You can connect your sensor over the provided USB cable. +For special setups check out the sensor pinout in the section below. + +
Sensor pinout +

+sensor wiring picture + +| *Pin* | *Cable Color* | *Name* | *Description* | *Comments* | +|-------|---------------|:------:|----------------|------------| +| 1 | red | VDD | Supply Voltage | 3.3 or 5V +| 2 | black | GND | Ground | +| 3 | green | RX | UART: Transmission pin for communication | +| 4 | yellow | TX | UART: Receiving pin for communication | +| 5 | blue | SEL | Interface select | Leave floating or pull to VDD to select UART +| 6 | purple | NC | Do not connect | + + +

+
+ +## Documentation & Quickstart + +See the [documentation page](https://sensirion.github.io/python-uart-svm4x) for an API description and a +[quickstart](https://sensirion.github.io/python-uart-svm4x/execute-measurements.html) example. + + +## Contributing We develop and test this driver using our company internal tools (version control, continuous integration, code review etc.) and automatically synchronize the `master` branch with GitHub. But this doesn't mean that we don't respond to issues or don't accept pull requests on GitHub. In fact, -you're very welcome to open issues or create pull requests :) +you're very welcome to open issues or create pull requests :-) ### Check coding style @@ -34,35 +67,6 @@ pip install editorconfig-checker==2.0.3 # Install requirements editorconfig-checker # Run check ``` -### Run tests - -Unit tests can be run with [`pytest`](https://pytest.org/): - -```bash -pip install -e .[test] # Install requirements -pytest -m "not needs_device" # Run tests without hardware -pytest # Run all tests -pytest -m "needs_device" # Run all tests for SVM41 - -``` - -The tests with the marker `needs_device` have following requirements: - -- The SVM41 sensor must be connected to a Serial Port of your developement machine. -- Pass the serial port where the sensor is connected with - `--serial-port`, e.g. `pytest --serial-port=COM7` - - -### Build documentation - -The documentation can be built with [Sphinx](http://www.sphinx-doc.org/): - -```bash -python setup.py install # Install package -pip install -r docs/requirements.txt # Install requirements -sphinx-versioning build docs docs/_build/html # Build documentation -``` - ## License See [LICENSE](LICENSE). diff --git a/ci/checkin_doc.sh b/ci/checkin_doc.sh index ac724b6..1031751 100644 --- a/ci/checkin_doc.sh +++ b/ci/checkin_doc.sh @@ -7,10 +7,20 @@ set -euv -o pipefail git clone "git@gitlab:${CI_PROJECT_PATH}.git" html cd html git checkout gh-pages -rm empty.txt +rm -f empty.txt +rm -f *.html +rm -f *.js +rm -rf _* cd .. -mv docs/_build/html/* html/ +# make sure to copy .nojekyll +cp -rf public/.[!.]* public/* html cd html git add . -git commit -m"Automatic doc update:${CI_COMMIT_SHORT_SHA}" -git push + +if git diff-index --quiet HEAD -- +then + exit 0 +fi + +git commit -m"Automatic doc update: ${CI_COMMIT_SHORT_SHA}" +git push \ No newline at end of file diff --git a/conftest.py b/conftest.py index a836da0..a1e0a5e 100644 --- a/conftest.py +++ b/conftest.py @@ -2,7 +2,9 @@ # (c) Copyright 2022 Sensirion AG, Switzerland import pytest -from sensirion_shdlc_driver import ShdlcSerialPort, ShdlcConnection +from sensirion_driver_adapters.mocks.mock_shdlc_channel_provider import ShdlcMockPortChannelProvider +from sensirion_driver_adapters.shdlc_adapter.shdlc_serial_channel_provider import ShdlcSerialPortChannelProvider +from sensirion_uart_svm4x.response_provider import Svm4xResponseProvider def pytest_addoption(parser): @@ -43,9 +45,11 @@ def pytest_report_header(config): @pytest.fixture(scope="session") -def shdlc_port(request): - serial_port = _get_serial_port(request.config, validate=True) +def channel_provider(request): + serial_port = _get_serial_port(request.config, validate=False) serial_bitrate = _get_serial_bitrate(request.config) - with ShdlcSerialPort(serial_port, serial_bitrate) as port: - yield port - + if serial_port is not None: + yield ShdlcSerialPortChannelProvider(serial_port=serial_port, + serial_baud_rate=serial_bitrate) + else: + yield ShdlcMockPortChannelProvider(response_provider=Svm4xResponseProvider()) diff --git a/docs/api.rst b/docs/api.rst index b1f1939..a634066 100644 --- a/docs/api.rst +++ b/docs/api.rst @@ -1,25 +1,25 @@ API Documentation ================= -SVM41 -===== +SVM4X +----- -Svm41Device +Svm4xDevice ~~~~~~~~~~~ -.. automodule:: sensirion_shdlc_svm41.device +.. automodule:: sensirion_uart_svm4x.device Commands ~~~~~~~~ -.. automodule:: sensirion_shdlc_svm41.commands +.. automodule:: sensirion_uart_svm4x.commands :exclude-members: __init__ Result Types ~~~~~~~~~~~~ -.. automodule:: sensirion_shdlc_svm41.result_types +.. automodule:: sensirion_uart_svm4x.result_types :exclude-members: __init__ diff --git a/docs/build-documentation.rst b/docs/build-documentation.rst new file mode 100644 index 0000000..8653cf1 --- /dev/null +++ b/docs/build-documentation.rst @@ -0,0 +1,14 @@ +Build documentation +=================== + +The documentation is built with `Sphinx `_: + +.. code-block:: bash + + python setup.py install # Install package + pip install -r docs/requirements.txt # Install requirements + cd docs # Change to docs folder + make html # Build documentation + + +The generated documentation is located in docs/_build/html. \ No newline at end of file diff --git a/docs/conf.py b/docs/conf.py index 442f46e..8930438 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -11,22 +11,22 @@ import pkg_resources import sphinx.ext.autodoc -import sensirion_shdlc_svm41 +import sensirion_uart_svm4x # Add project directory such that sphinx can detect the package. sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), '..'))) # -- Project information ----------------------------------------------------- -distribution = pkg_resources.get_distribution('sensirion_shdlc_svm41') +distribution = pkg_resources.get_distribution('sensirion_uart_svm4x') -project = u'sensirion_shdlc_svm41' +project = u'sensirion_uart_svm4x' copyright = u'{} Sensirion AG, Switzerland'.format(datetime.now().year) author = 'Sensirion AG' # The short X.Y version -version = sensirion_shdlc_svm41.__version__ +version = sensirion_uart_svm4x.__version__ # The full version, including alpha/beta/rc tags -release = sensirion_shdlc_svm41.__version__ +release = sensirion_uart_svm4x.__version__ # -- General configuration --------------------------------------------------- @@ -59,20 +59,15 @@ # html_theme = 'sphinx_rtd_theme' -# Workaround for weirdly formatted function parameters. See https://github.com/readthedocs/sphinx_rtd_theme/issues/766 -html4_writer = True - html_favicon = 'favicon.ico' # -- Extension configuration ------------------------------------------------- - -autodoc_member_order = 'bysource' - -autodoc_default_flags = [ - 'members', - 'special-members', # To see __init__() - 'inherited-members', # To see the methods from base classes -] +autodoc_default_options = { + 'members': None, + 'member-order': 'bysource', + 'special-members': '__init__', + 'undoc-members': True +} def autodoc_skip_member(app, what, name, obj, skip, options): @@ -85,11 +80,6 @@ def setup(app): app.connect('autodoc-skip-member', autodoc_skip_member) -# Workaround for "=None" documentation of instance attributes -# (see https://github.com/sphinx-doc/sphinx/issues/2044) -sphinx.ext.autodoc.InstanceAttributeDocumenter.add_directive_header = \ - sphinx.ext.autodoc.ClassLevelDocumenter.add_directive_header - scv_whitelist_branches = ('master',) scv_grm_exclude = ('.gitignore', '.nojekyll') diff --git a/docs/development.rst b/docs/development.rst new file mode 100644 index 0000000..d169708 --- /dev/null +++ b/docs/development.rst @@ -0,0 +1,9 @@ +Development +=========== + +.. toctree:: + + run-tests + build-documentation + + diff --git a/docs/execute-measurements.rst b/docs/execute-measurements.rst new file mode 100644 index 0000000..0906746 --- /dev/null +++ b/docs/execute-measurements.rst @@ -0,0 +1,21 @@ +Execute measurements +==================== + +The following steps shows how to use this driver to execute a simple measurement on a Windows system. + +1. Install the SVM4X sensor driver and all required packages as described in :ref:`Installation`. +2. Connect the SVM4X sensor to your system with the serial USB cable. +3. Run the example script from the root of the repository. + + By default the script assumes the sensor is connected to COM1 serial port. If this is different on your system, + pass the port in use with the :code:`--serial-port` parameter as outlined below. + + .. sourcecode:: bash + + python examples/example_usage_uart_svm4x.py --serial-port + +Example script +-------------- + +.. literalinclude:: ../examples/example_usage_uart_svm4x.py + :language: python \ No newline at end of file diff --git a/docs/index.rst b/docs/index.rst index 7f13f1f..79d8473 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -1,7 +1,7 @@ -Sensirion SVM41 SHDLC Python Driver +Sensirion SVM4X SHDLC Python Driver =================================== -This package contains the driver for the Sensirion SHDLC SVM41 sensor . +This package contains the driver for the Sensirion SHDLC SVM4X sensor. It provides a hardware-independent interface which works with various SHDLC transceivers. Contents @@ -11,4 +11,5 @@ Contents installation quickstart + development api diff --git a/docs/installation.rst b/docs/installation.rst index c4c4930..40ba2f8 100644 --- a/docs/installation.rst +++ b/docs/installation.rst @@ -1,3 +1,5 @@ +.. _installation: + Installation ============ @@ -5,6 +7,6 @@ The package can be installed with pip: .. sourcecode:: bash - pip install sensirion_shdlc_svm41 + pip install sensirion_uart_svm4x Recommended usage is within a virtualenv. diff --git a/docs/quickstart.rst b/docs/quickstart.rst index 6e96909..5dce52d 100644 --- a/docs/quickstart.rst +++ b/docs/quickstart.rst @@ -1,20 +1,8 @@ Quick Start =========== -Execute measurements --------------------- +.. toctree:: -Example script -~~~~~~~~~~~~~~ + execute-measurements -The following script shows how to use this driver on a Windows system that is attached to COM1 serial port -to execute a simple measurement. -.. literalinclude:: ../examples/example_uart_svm41.py - :language: python - -After installing all required packages the you can start it on the command line by typing. - -.. sourcecode:: bash - - python ../examples/example_serial_svm41.py --port diff --git a/docs/requirements.txt b/docs/requirements.txt index 20871f9..0c69c37 100644 --- a/docs/requirements.txt +++ b/docs/requirements.txt @@ -1,4 +1,4 @@ click==8.0.4 jinja2==3.0.1 -sphinx~=1.8.3 -sphinx_rtd_theme~=0.4.2 +sphinx~=4.2.0 +sphinx_rtd_theme~=1.0.0 diff --git a/docs/run-tests.rst b/docs/run-tests.rst new file mode 100644 index 0000000..c3c653e --- /dev/null +++ b/docs/run-tests.rst @@ -0,0 +1,22 @@ +Run tests +========= + +Unit tests can be run with `pytest `_: + +.. code-block:: bash + + pip install -e .[test] # Install test requirements + +We provide a mock implementation that allows you to execute the tests for SVM4X without hardware. + +.. code-block:: bash + + pytest # Run all tests for SVM4X using a driver mock + +To run the tests against real hardware, you must have your SVM4X sensor connected to a serial port of your +development machine. Pass the serial port used, e.g. COM1, as command line argument when running the tests: + +.. code-block:: bash + + pytest --serial-port=COM1 # Run all tests for SVM4X an the sensor attached to COM1 + diff --git a/examples/algorithm_tuning_example_uart_svm4x.py b/examples/algorithm_tuning_example_uart_svm4x.py new file mode 100644 index 0000000..6f0e099 --- /dev/null +++ b/examples/algorithm_tuning_example_uart_svm4x.py @@ -0,0 +1,63 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +# +# (c) Copyright 2023 Sensirion AG, Switzerland +# +# THIS FILE IS AUTOMATICALLY GENERATED! +# +# Generator: sensirion-driver-generator 0.32.0 +# Product: svm4x +# Model-Version: 2.0.0 +# + +import time +import argparse +from sensirion_shdlc_driver import ShdlcSerialPort +from sensirion_driver_adapters.shdlc_adapter.shdlc_channel import ShdlcChannel +from sensirion_uart_svm4x.device import Svm4xDevice + +parser = argparse.ArgumentParser() +parser.add_argument('--serial-port', '-p', default='COM1') +args = parser.parse_args() + +with ShdlcSerialPort(port=args.serial_port, baudrate=115200) as port: + channel = ShdlcChannel(port) + sensor = Svm4xDevice(channel) + sensor.device_reset() + time.sleep(2.0) + (voc_index_offset, learning_time_offset_hours, learning_time_gain_hours, gating_max_duration_minutes, std_initial, + gain_factor + ) = sensor.get_voc_tuning_parameters() + print(f"voc_index_offset: {voc_index_offset}; " + f"learning_time_offset_hours: {learning_time_offset_hours}; " + f"learning_time_gain_hours: {learning_time_gain_hours}; " + f"gating_max_duration_minutes: {gating_max_duration_minutes}; " + f"std_initial: {std_initial}; " + f"gain_factor: {gain_factor}; " + ) + (nox_index_offset, learning_time_offset_hours, learning_time_gain_hours, gating_max_duration_minutes, std_initial, + gain_factor + ) = sensor.get_nox_tuning_parameters() + print(f"nox_index_offset: {nox_index_offset}; " + f"learning_time_offset_hours: {learning_time_offset_hours}; " + f"learning_time_gain_hours: {learning_time_gain_hours}; " + f"gating_max_duration_minutes: {gating_max_duration_minutes}; " + f"std_initial: {std_initial}; " + f"gain_factor: {gain_factor}; " + ) + sensor.set_voc_tuning_parameters(100, 12, 12, 180, 50, 230) + sensor.set_nox_tuning_parameters(1, 12, 12, 720, 50, 230) + sensor.start_measurement() + for i in range(50): + try: + time.sleep(1.0) + (a_humidity, a_temperature, a_voc_index, a_nox_index + ) = sensor.read_measured_values() + print(f"a_humidity: {a_humidity}; " + f"a_temperature: {a_temperature}; " + f"a_voc_index: {a_voc_index}; " + f"a_nox_index: {a_nox_index}; " + ) + except BaseException: + continue + sensor.stop_measurement() diff --git a/examples/example_usage_uart_svm4x.py b/examples/example_usage_uart_svm4x.py new file mode 100644 index 0000000..ab38ee8 --- /dev/null +++ b/examples/example_usage_uart_svm4x.py @@ -0,0 +1,44 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +# +# (c) Copyright 2023 Sensirion AG, Switzerland +# +# THIS FILE IS AUTOMATICALLY GENERATED! +# +# Generator: sensirion-driver-generator 0.32.0 +# Product: svm4x +# Model-Version: 2.0.0 +# + +import time +import argparse +from sensirion_shdlc_driver import ShdlcSerialPort +from sensirion_driver_adapters.shdlc_adapter.shdlc_channel import ShdlcChannel +from sensirion_uart_svm4x.device import Svm4xDevice + +parser = argparse.ArgumentParser() +parser.add_argument('--serial-port', '-p', default='COM1') +args = parser.parse_args() + +with ShdlcSerialPort(port=args.serial_port, baudrate=115200) as port: + channel = ShdlcChannel(port) + sensor = Svm4xDevice(channel) + sensor.device_reset() + time.sleep(2.0) + serial_number = sensor.get_serial_number() + print(f"serial_number: {serial_number}; " + ) + sensor.start_measurement() + for i in range(50): + try: + time.sleep(1.0) + (a_humidity, a_temperature, a_voc_index, a_nox_index + ) = sensor.read_measured_values() + print(f"a_humidity: {a_humidity}; " + f"a_temperature: {a_temperature}; " + f"a_voc_index: {a_voc_index}; " + f"a_nox_index: {a_nox_index}; " + ) + except BaseException: + continue + sensor.stop_measurement() diff --git a/metadata.yml b/metadata.yml new file mode 100644 index 0000000..480dad7 --- /dev/null +++ b/metadata.yml @@ -0,0 +1,7 @@ +generator_version: 0.32.0 +model_version: 2.0.0 +dg_status: +- released +is_manually_modified: false +first_generated: '2022-09-27 17:04' +last_generated: '2023-10-19 07:12' diff --git a/sensirion_uart_svm4x/__init__.py b/sensirion_uart_svm4x/__init__.py new file mode 100644 index 0000000..dceeca0 --- /dev/null +++ b/sensirion_uart_svm4x/__init__.py @@ -0,0 +1,8 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +from __future__ import absolute_import, division, print_function +from .version import version as __version__ # noqa: F401 + +from sensirion_uart_svm4x.device import Svm4xDevice # noqa: F401 + +__all__ = ['Svm4xDevice'] diff --git a/sensirion_uart_svm4x/commands.py b/sensirion_uart_svm4x/commands.py new file mode 100644 index 0000000..48abdc3 --- /dev/null +++ b/sensirion_uart_svm4x/commands.py @@ -0,0 +1,331 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +# +# (c) Copyright 2023 Sensirion AG, Switzerland +# +# THIS FILE IS AUTOMATICALLY GENERATED! +# +# Generator: sensirion-driver-generator 0.32.0 +# Product: svm4x +# Model-Version: 2.0.0 +# +""" +The transfer classes specify the data that is transferred between host and sensor. The generated transfer classes +are used by the driver class and not intended for direct use. +""" + +from sensirion_driver_adapters.transfer import Transfer +from sensirion_driver_adapters.rx_tx_data import TxData, RxData + + +class GetProductType(Transfer): + """Gets the product type from the device.""" + + CMD_ID = 0xd0 + + def pack(self): + return self.tx_data.pack([0x0]) + + tx = TxData(CMD_ID, '>BB', device_busy_delay=0.05, slave_address=None, ignore_ack=False) + rx = RxData('>32s') + + +class GetProductName(Transfer): + """Gets the product name from the device.""" + + CMD_ID = 0xd0 + + def pack(self): + return self.tx_data.pack([0x1]) + + tx = TxData(CMD_ID, '>BB', device_busy_delay=0.05, slave_address=None, ignore_ack=False) + rx = RxData('>8s') + + +class GetSerialNumber(Transfer): + """Gets the serial number from the device.""" + + CMD_ID = 0xd0 + + def pack(self): + return self.tx_data.pack([0x3]) + + tx = TxData(CMD_ID, '>BB', device_busy_delay=0.05, slave_address=None, ignore_ack=False) + rx = RxData('>32s') + + +class GetVersion(Transfer): + """Gets the version information for the hardware, firmware and SHDLC protocol.""" + + CMD_ID = 0xd1 + + def pack(self): + return self.tx_data.pack([]) + + tx = TxData(CMD_ID, '>B', device_busy_delay=0.05, slave_address=None, ignore_ack=False) + rx = RxData('>BB?BBBB') + + +class DeviceReset(Transfer): + """Executs a reset on the device.""" + + CMD_ID = 0xd3 + + def pack(self): + return self.tx_data.pack([]) + + tx = TxData(CMD_ID, '>B', device_busy_delay=0.05, slave_address=None, ignore_ack=False) + post_processing_time = 0.1 + + +class GetSystemUpTime(Transfer): + """Get the system up time of the device.""" + + CMD_ID = 0x93 + + def pack(self): + return self.tx_data.pack([]) + + tx = TxData(CMD_ID, '>B', device_busy_delay=0.05, slave_address=None, ignore_ack=False) + rx = RxData('>I') + + +class StartMeasurement(Transfer): + """Starts measurement in polling mode.""" + + CMD_ID = 0x0 + + def pack(self): + return self.tx_data.pack([0x0]) + + tx = TxData(CMD_ID, '>BB', device_busy_delay=0.05, slave_address=None, ignore_ack=False) + + +class StopMeasurement(Transfer): + """Leaves the measurement mode and returns to the idle mode.""" + + CMD_ID = 0x1 + + def pack(self): + return self.tx_data.pack([]) + + tx = TxData(CMD_ID, '>B', device_busy_delay=0.05, slave_address=None, ignore_ack=False) + + +class ReadMeasuredValuesAsIntegers(Transfer): + """Returns the new measurement results as integers.""" + + CMD_ID = 0x3 + + def pack(self): + return self.tx_data.pack([0x10]) + + tx = TxData(CMD_ID, '>BB', device_busy_delay=0.05, slave_address=None, ignore_ack=False) + rx = RxData('>hhhh') + + +class ReadMeasuredRawValues(Transfer): + """Returns the measured raw values.""" + + CMD_ID = 0x3 + + def pack(self): + return self.tx_data.pack([0xd]) + + tx = TxData(CMD_ID, '>BB', device_busy_delay=0.05, slave_address=None, ignore_ack=False) + rx = RxData('>hhHH') + + +class GetTemperatureOffsetForRhtMeasurements(Transfer): + """Gets the T-Offset for the temperature compensation of the RHT algorithm.""" + + CMD_ID = 0x60 + + def pack(self): + return self.tx_data.pack([0x1]) + + tx = TxData(CMD_ID, '>BB', device_busy_delay=0.05, slave_address=None, ignore_ack=False) + rx = RxData('>h') + + +class GetVocTuningParameters(Transfer): + """Gets the currently set parameters for customizing the VOC algorithm.""" + + CMD_ID = 0x60 + + def pack(self): + return self.tx_data.pack([0xd]) + + tx = TxData(CMD_ID, '>BB', device_busy_delay=0.05, slave_address=None, ignore_ack=False) + rx = RxData('>hhhhhh') + + +class GetNoxTuningParameters(Transfer): + """Gets the currently set parameters for customizing the NOx algorithm.""" + + CMD_ID = 0x60 + + def pack(self): + return self.tx_data.pack([0xe]) + + tx = TxData(CMD_ID, '>BB', device_busy_delay=0.05, slave_address=None, ignore_ack=False) + rx = RxData('>hhhhhh') + + +class StoreNvData(Transfer): + """Stores all algorithm parameters to the non-volatile memory.""" + + CMD_ID = 0x60 + + def pack(self): + return self.tx_data.pack([0x80]) + + tx = TxData(CMD_ID, '>BB', device_busy_delay=0.05, slave_address=None, ignore_ack=False) + + +class SetTemperatureOffsetForRhtMeasurements(Transfer): + """Sets the T-Offset for the temperature compensation of the RHT algorithm.""" + + CMD_ID = 0x60 + + def __init__(self, t_offset): + self._t_offset = t_offset + + def pack(self): + return self.tx_data.pack([0x81, self._t_offset]) + + tx = TxData(CMD_ID, '>BBh', device_busy_delay=0.05, slave_address=None, ignore_ack=False) + + +class SetVocTuningParameters(Transfer): + """ + Sets parameters to customize the VOC algorithm. This command is only available + in idle mode. + """ + + CMD_ID = 0x60 + + def __init__(self, voc_index_offset, learning_time_offset_hours, learning_time_gain_hours, + gating_max_duration_minutes, std_initial, gain_factor): + self._voc_index_offset = voc_index_offset + self._learning_time_offset_hours = learning_time_offset_hours + self._learning_time_gain_hours = learning_time_gain_hours + self._gating_max_duration_minutes = gating_max_duration_minutes + self._std_initial = std_initial + self._gain_factor = gain_factor + + def pack(self): + return self.tx_data.pack([0x8d, self._voc_index_offset, self._learning_time_offset_hours, + self._learning_time_gain_hours, self._gating_max_duration_minutes, self._std_initial, + self._gain_factor]) + + tx = TxData(CMD_ID, '>BBhhhhhh', device_busy_delay=0.05, slave_address=None, ignore_ack=False) + + +class SetNoxTuningParameters(Transfer): + """ + Sets parameters to customize the NOx algorithm. This command is only available + in idle mode. + """ + + CMD_ID = 0x60 + + def __init__(self, nox_index_offset, learning_time_offset_hours, learning_time_gain_hours, + gating_max_duration_minutes, std_initial, gain_factor): + self._nox_index_offset = nox_index_offset + self._learning_time_offset_hours = learning_time_offset_hours + self._learning_time_gain_hours = learning_time_gain_hours + self._gating_max_duration_minutes = gating_max_duration_minutes + self._std_initial = std_initial + self._gain_factor = gain_factor + + def pack(self): + return self.tx_data.pack([0x8e, self._nox_index_offset, self._learning_time_offset_hours, + self._learning_time_gain_hours, self._gating_max_duration_minutes, self._std_initial, + self._gain_factor]) + + tx = TxData(CMD_ID, '>BBhhhhhh', device_busy_delay=0.05, slave_address=None, ignore_ack=False) + + +class GetVocState(Transfer): + """ + Gets the current VOC algorithm state. Retrieved values can be used to set the + VOC algorithm state to resume operation after a short interruption, skipping + initial learning phase. This command is only available during measurement mode. + """ + + CMD_ID = 0x61 + + def pack(self): + return self.tx_data.pack([0x8]) + + tx = TxData(CMD_ID, '>BB', device_busy_delay=0.05, slave_address=None, ignore_ack=False) + rx = RxData('>8B') + + +class GetNoxState(Transfer): + """ + Gets the current NOx algorithm state. Retrieved values can be used to set the + NOx algorithm state to resume operation after a short interruption, skipping + initial learning phase. This command is only available during measurement mode. + """ + + CMD_ID = 0x61 + + def pack(self): + return self.tx_data.pack([0x9]) + + tx = TxData(CMD_ID, '>BB', device_busy_delay=0.05, slave_address=None, ignore_ack=False) + rx = RxData('>8B') + + +class SetVocState(Transfer): + """ + Set previously retrieved VOC algorithm state to resume operation after a short + interruption, skipping initial learning phase. This command is only available in + idle mode. + """ + + CMD_ID = 0x61 + + def __init__(self, state): + self._state = state + + def pack(self): + return self.tx_data.pack([0x88, self._state]) + + tx = TxData(CMD_ID, '>BB8B', device_busy_delay=0.05, slave_address=None, ignore_ack=False) + + +class SetNoxState(Transfer): + """ + Set previously retrieved NOx algorithm state to resume operation after a short + interruption, skipping initial learning phase. This command is only available in + idle mode. + """ + + CMD_ID = 0x61 + + def __init__(self, state): + self._state = state + + def pack(self): + return self.tx_data.pack([0x89, self._state]) + + tx = TxData(CMD_ID, '>BB8B', device_busy_delay=0.05, slave_address=None, ignore_ack=False) + + +class EnterBootloader(Transfer): + """ + Command to enter into the bootloader mode. The device will reboot into + bootloader mode and wait until the new Firmware is received (start update + command expected). Even after a power reset, the device returns into bootloader + mode. The response frame is sent before the reset. + """ + + CMD_ID = 0xf3 + + def pack(self): + return self.tx_data.pack([]) + + tx = TxData(CMD_ID, '>B') diff --git a/sensirion_uart_svm4x/device.py b/sensirion_uart_svm4x/device.py new file mode 100644 index 0000000..5bd3425 --- /dev/null +++ b/sensirion_uart_svm4x/device.py @@ -0,0 +1,443 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +# +# (c) Copyright 2023 Sensirion AG, Switzerland +# +# THIS FILE IS AUTOMATICALLY GENERATED! +# +# Generator: sensirion-driver-generator 0.32.0 +# Product: svm4x +# Model-Version: 2.0.0 +# +""" +The class Svm4xDeviceBase implements the low level interface of the sensor. +The class Svm4xDevice extends the Svm4xDeviceBase. It provides additional functions to ease the use of the +sensor. +""" + +from sensirion_driver_adapters.transfer import execute_transfer +from sensirion_driver_support_types.mixin_access import MixinAccess +from sensirion_uart_svm4x.commands import (DeviceReset, EnterBootloader, GetNoxState, GetNoxTuningParameters, + GetProductName, GetProductType, GetSerialNumber, GetSystemUpTime, + GetTemperatureOffsetForRhtMeasurements, GetVersion, GetVocState, + GetVocTuningParameters, ReadMeasuredRawValues, ReadMeasuredValuesAsIntegers, + SetNoxState, SetNoxTuningParameters, SetTemperatureOffsetForRhtMeasurements, + SetVocState, SetVocTuningParameters, StartMeasurement, StopMeasurement, + StoreNvData) + +from sensirion_uart_svm4x.result_types import (SignalHumidity, SignalNoxIndex, SignalTemperature, SignalVocIndex) + + +class Svm4xDeviceBase: + """Low level API implementation of SVM4X""" + + def __init__(self, channel): + self._channel = channel + + @property + def channel(self): + return self._channel + + def get_product_type(self): + """ + Gets the product type from the device. + + :return product_type: + Character array containing the product type. + """ + transfer = GetProductType() + return execute_transfer(self._channel, transfer)[0] + + def get_product_name(self): + """ + Gets the product name from the device. + + :return product_name: + Character array containing the product name. + """ + transfer = GetProductName() + return execute_transfer(self._channel, transfer)[0] + + def get_serial_number(self): + """ + Gets the serial number from the device. + + :return serial_number: + Character array containing the serial number. + """ + transfer = GetSerialNumber() + return execute_transfer(self._channel, transfer)[0] + + def get_version(self): + """ + Gets the version information for the hardware, firmware and SHDLC protocol. + + :return firmware_major: + Firmware major version number. + :return firmware_minor: + Firmware minor version number. + :return firmware_debug: + Firmware debug state. If the debug state is set, the firmware is in development. + :return hardware_major: + Hardware major version number. + :return hardware_minor: + Hardware minor version number. + :return protocol_major: + Protocol major version number. + :return protocol_minor: + Protocol minor version number. + """ + transfer = GetVersion() + return execute_transfer(self._channel, transfer) + + def device_reset(self): + """ + Executs a reset on the device. + + .. note:: + The device will reply before executing the reset. If the command is send with + broadcast, the reset is done directly after the reception of the command. + """ + transfer = DeviceReset() + return execute_transfer(self._channel, transfer) + + def get_system_up_time(self): + """ + Get the system up time of the device. + + :return system_up_time: + The time since the last power-on or device reset in seconds. + """ + transfer = GetSystemUpTime() + return execute_transfer(self._channel, transfer)[0] + + def start_measurement(self): + """ + Starts measurement in polling mode. + + .. note:: + This command is only available in idle mode. + """ + transfer = StartMeasurement() + return execute_transfer(self._channel, transfer) + + def stop_measurement(self): + """ + Leaves the measurement mode and returns to the idle mode. + + .. note:: + This command is only available in measurement mode. + """ + transfer = StopMeasurement() + return execute_transfer(self._channel, transfer) + + def read_measured_values_as_integers(self): + """ + Returns the new measurement results as integers. + + :return humidity: + Compensated ambient humidity in % RH with a scaling factor of 100. + :return temperature: + Compensated ambient temperature in degrees celsius with a scaling of 200. + :return voc_index: + VOC index with a scaling value of 10. + :return nox_index: + NOx index with a scaling value of 10. + + .. note:: + This command is only available in measurement mode. The firmware updates the + measurement values every second. Polling data with a faster sampling rate will + return the same values. The first measurement is available 1 second after the + start measurement command is issued. Any readout prior to this will return zero + initialized values. + """ + transfer = ReadMeasuredValuesAsIntegers() + return execute_transfer(self._channel, transfer) + + def read_measured_raw_values(self): + """ + Returns the measured raw values. + + :return raw_humidity: + Uncompensated raw humidity in % RH as read from the SHT40 with a scaling factor + of 100. + :return raw_temperature: + Uncompensated raw temperature in degrees celsius as read from the SHT40 with a + scaling of 200. + :return raw_voc_ticks: + Raw VOC output ticks as read from the SGP sensor. + :return raw_nox_ticks: + Raw NOx output ticks as read from the SGP sensor. + + .. note:: + This command is only available in measurement mode. The firmware updates the + measurement values every second. Polling data with a faster sampling rate will + return the same values. The first measurement is available 1 second after the + start measurement command is issued. Any readout prior to this will return zero + initialized values. + """ + transfer = ReadMeasuredRawValues() + return execute_transfer(self._channel, transfer) + + def get_temperature_offset_for_rht_measurements(self): + """ + Gets the T-Offset for the temperature compensation of the RHT algorithm. + + :return t_offset: + Temperature offset which is used for the RHT measurements. Values are in degrees + celsius with a scaling of 200. Thus the received value must be divided by 200. + """ + transfer = GetTemperatureOffsetForRhtMeasurements() + return execute_transfer(self._channel, transfer)[0] + + def get_voc_tuning_parameters(self): + """ + Gets the currently set parameters for customizing the VOC algorithm. + + :return voc_index_offset: + VOC index representing typical (average) conditions. + :return learning_time_offset_hours: + Time constant to estimate the VOC algorithm offset from the history in hours. + Past events will be forgotten after about twice the learning time. + :return learning_time_gain_hours: + Time constant to estimate the VOC algorithm gain from the history in hours. Past + events will be forgotten after about twice the learning time. + :return gating_max_duration_minutes: + Maximum duration of gating in minutes (freeze of estimator during high VOC index + signal). Set to zero to disable the gating. + :return std_initial: + Initial estimate for standard deviation. Lower value boosts events during + initial learning period, but may result in larger device-to-device variations. + :return gain_factor: + Gain factor to amplify or to attenuate the VOC index output. + """ + transfer = GetVocTuningParameters() + return execute_transfer(self._channel, transfer) + + def get_nox_tuning_parameters(self): + """ + Gets the currently set parameters for customizing the NOx algorithm. + + :return nox_index_offset: + NOx index representing typical (average) conditions. + :return learning_time_offset_hours: + Time constant to estimate the NOx algorithm offset from the history in hours. + Past events will be forgotten after about twice the learning time. + :return learning_time_gain_hours: + The time constant to estimate the NOx algorithm gain from the history has no + impact for NOx. This parameter is still in place for consistency reasons with + the VOC tuning parameters command. This getter will always return the default + value. + :return gating_max_duration_minutes: + Maximum duration of gating in minutes (freeze of estimator during high NOx index + signal). Set to zero to disable the gating. + :return std_initial: + The initial estimate for standard deviation has no impact for NOx. This + parameter is still in place for consistency reasons with the VOC tuning + parameters command. This getter will always return the default value. + :return gain_factor: + Gain factor to amplify or to attenuate the NOx index output. + """ + transfer = GetNoxTuningParameters() + return execute_transfer(self._channel, transfer) + + def store_nv_data(self): + """Stores all algorithm parameters to the non-volatile memory.""" + transfer = StoreNvData() + return execute_transfer(self._channel, transfer) + + def set_temperature_offset_for_rht_measurements(self, t_offset): + """ + Sets the T-Offset for the temperature compensation of the RHT algorithm. + + :param t_offset: + Temperature offset in degrees celsius. Accepted data formats is a int16 value (2 + bytes). Values are in degrees celsius with a scaling of 200. Thus the provided + value must be multiplied by 200. + + .. note:: + Execute the store command after writing the parameter to store it in the non- + volatile memory of the device otherwise the parameter will be reset upton a + device reset. + """ + transfer = SetTemperatureOffsetForRhtMeasurements(t_offset) + return execute_transfer(self._channel, transfer) + + def set_voc_tuning_parameters(self, voc_index_offset, learning_time_offset_hours, learning_time_gain_hours, + gating_max_duration_minutes, std_initial, gain_factor): + """ + Sets parameters to customize the VOC algorithm. This command is only available + in idle mode. + + :param voc_index_offset: + VOC index representing typical (average) conditions. Allowed values are in range + 1..250. The default value is 100. + :param learning_time_offset_hours: + Time constant to estimate the VOC algorithm offset from the history in hours. + Past events will be forgotten after about twice the learning time. Allowed + values are in range 1..1000. The default value is 12 hours. + :param learning_time_gain_hours: + Time constant to estimate the VOC algorithm gain from the history in hours. Past + events will be forgotten after about twice the learning time. Allowed values are + in range 1..1000. The default value is 12 hours. + :param gating_max_duration_minutes: + Maximum duration of gating in minutes (freeze of estimator during high VOC index + signal). Set to zero to disable the gating. Allowed values are in range 0..3000. + The default value is 180 minutes. + :param std_initial: + Initial estimate for standard deviation. Lower value boosts events during + initial learning period, but may result in larger device-to-device variations. + Allowed values are in range 10..5000. The default value is 50. + :param gain_factor: + Gain factor to amplify or to attenuate the VOC index output. Allowed values are + in range 1..1000. The default value is 230. + + .. note:: + Execute the store command after writing the parameter to store it in the non- + volatile memory of the device otherwise the parameter will be reset upton a + device reset. + """ + transfer = SetVocTuningParameters(voc_index_offset, learning_time_offset_hours, learning_time_gain_hours, + gating_max_duration_minutes, std_initial, gain_factor) + return execute_transfer(self._channel, transfer) + + def set_nox_tuning_parameters(self, nox_index_offset, learning_time_offset_hours, learning_time_gain_hours, + gating_max_duration_minutes, std_initial, gain_factor): + """ + Sets parameters to customize the NOx algorithm. This command is only available + in idle mode. + + :param nox_index_offset: + NOx index representing typical (average) conditions. Allowed values are in range + 1..250. The default value is 1. + :param learning_time_offset_hours: + Time constant to estimate the NOx algorithm offset from the history in hours. + Past events will be forgotten after about twice the learning time. Allowed + values are in range 1..1000. The default value is 12 hours. + :param learning_time_gain_hours: + The time constant to estimate the NOx algorithm gain from the history has no + impact for the NOx algorithm. This parameter is still in place for consistency + reasons with the VOC tuning parameters command. This parameter must always be + set to 12 hours. + :param gating_max_duration_minutes: + Maximum duration of gating in minutes (freeze of estimator during high NOx index + signal). Set to zero to disable the gating. Allowed values are in range 0..3000. + The default value is 720 minutes. + :param std_initial: + The initial estimate for standard deviation parameter has no impact for the NOx + algorithm. This parameter is still in place for consistency reasons with the VOC + tuning parameters command. This parameter must always be set to 50. + :param gain_factor: + Gain factor to amplify or to attenuate the NOx index output. Allowed values are + in range 1..1000. The default value is 230. + + .. note:: + Execute the store command after writing the parameter to store it in the non- + volatile memory of the device otherwise the parameter will be reset upton a + device reset. + """ + transfer = SetNoxTuningParameters(nox_index_offset, learning_time_offset_hours, learning_time_gain_hours, + gating_max_duration_minutes, std_initial, gain_factor) + return execute_transfer(self._channel, transfer) + + def get_voc_state(self): + """ + Gets the current VOC algorithm state. Retrieved values can be used to set the + VOC algorithm state to resume operation after a short interruption, skipping + initial learning phase. This command is only available during measurement mode. + + :return state: + Current VOC algorithm state. + + .. note:: + This feature can only be used after at least 3 hours of continuous operation. + """ + transfer = GetVocState() + return execute_transfer(self._channel, transfer)[0] + + def get_nox_state(self): + """ + Gets the current NOx algorithm state. Retrieved values can be used to set the + NOx algorithm state to resume operation after a short interruption, skipping + initial learning phase. This command is only available during measurement mode. + + :return state: + Current NOx algorithm state. + + .. note:: + This feature can only be used after at least 3 hours of continuous operation. + """ + transfer = GetNoxState() + return execute_transfer(self._channel, transfer)[0] + + def set_voc_state(self, state): + """ + Set previously retrieved VOC algorithm state to resume operation after a short + interruption, skipping initial learning phase. This command is only available in + idle mode. + + :param state: + Current VOC algorithm state. + + .. note:: + This feature should not be used after interruptions of more than 10 minutes. + """ + transfer = SetVocState(state) + return execute_transfer(self._channel, transfer) + + def set_nox_state(self, state): + """ + Set previously retrieved NOx algorithm state to resume operation after a short + interruption, skipping initial learning phase. This command is only available in + idle mode. + + :param state: + Current NOx algorithm state. + + .. note:: + This feature should not be used after interruptions of more than 10 minutes. + """ + transfer = SetNoxState(state) + return execute_transfer(self._channel, transfer) + + def enter_bootloader(self): + """ + Command to enter into the bootloader mode. The device will reboot into + bootloader mode and wait until the new Firmware is received (start update + command expected). Even after a power reset, the device returns into bootloader + mode. The response frame is sent before the reset. + + .. note:: + After the response frame is received, the device will not accept new commands + until fully booted (wait at least 1 s). + """ + transfer = EnterBootloader() + return execute_transfer(self._channel, transfer) + + +class Svm4xDevice(Svm4xDeviceBase): + """Driver class implementation of SVM4X""" + + #: Access to base class + svm4x = MixinAccess() + + def __init__(self, channel): + super().__init__(channel) + + def read_measured_values(self): + """ + Read measurement data and apply appropriate scaling. + + :return a_humidity: + Measured humidity in %RH. The raw value is scaled appropriately. + :return a_temperature: + Measured temperature in degrees celsius. The raw value is scaled appropriately. + :return a_voc_index: + Measured VOC Index ticks. + :return a_nox_index: + Measured NOx Index ticks. + """ + (humidity_raw, temp_raw, voc_idx_raw, nox_idx_raw + ) = self.read_measured_values_as_integers() + return (SignalHumidity(humidity_raw), SignalTemperature(temp_raw), SignalVocIndex(voc_idx_raw), + SignalNoxIndex(nox_idx_raw)) diff --git a/sensirion_uart_svm4x/response_provider.py b/sensirion_uart_svm4x/response_provider.py new file mode 100644 index 0000000..2e2e5db --- /dev/null +++ b/sensirion_uart_svm4x/response_provider.py @@ -0,0 +1,23 @@ +import struct +import sensirion_driver_adapters.mocks.response_provider as rp + + +class Svm4xResponseProvider(rp.ResponseProvider): + + RESPONSE_MAP = {0xd0: {0: struct.pack('>32s', rp.random_ascii_string(32)), + 1: struct.pack('>8s', rp.random_ascii_string(8)), + 3: struct.pack('>32s', rp.random_ascii_string(32))} + } + + def get_id(self) -> str: + return 'Svm4xResponseProvider' + + def handle_command(self, cmd_id: int, data: bytes, response_length: int) -> bytes: + sub_cmd_map = self.RESPONSE_MAP.get(cmd_id, None) + if sub_cmd_map is None: + return rp.random_bytes(response_length) + if None in sub_cmd_map: + return sub_cmd_map[None] + assert len(data) >= 1, "No subcommand specified!" + actual_subcommand = data[0] + return sub_cmd_map.get(actual_subcommand, rp.random_bytes(response_length)) diff --git a/sensirion_uart_svm4x/result_types.py b/sensirion_uart_svm4x/result_types.py new file mode 100644 index 0000000..a0d5a2f --- /dev/null +++ b/sensirion_uart_svm4x/result_types.py @@ -0,0 +1,74 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +# +# (c) Copyright 2023 Sensirion AG, Switzerland +# +# THIS FILE IS AUTOMATICALLY GENERATED! +# +# Generator: sensirion-driver-generator 0.32.0 +# Product: svm4x +# Model-Version: 2.0.0 +# +""" +The signal classes specify transformations of the raw sensor signals into a meaningful units. +The generated signal types are used by the driver class and not intended for direct use. +""" + +from sensirion_driver_support_types.signals import AbstractSignal + + +class SignalTemperature(AbstractSignal): + """Measured temperature in degrees celsius. The raw value is scaled appropriately.""" + + def __init__(self, temperature_raw): + self._temperature = temperature_raw / 200.0 + + @property + def value(self): + return self._temperature + + def __str__(self): + return '{0:.2f}'.format(self.value) + + +class SignalHumidity(AbstractSignal): + """Measured humidity in %RH. The raw value is scaled appropriately.""" + + def __init__(self, humidity_raw): + self._humidity = humidity_raw / 100.0 + + @property + def value(self): + return self._humidity + + def __str__(self): + return '{0:.2f}'.format(self.value) + + +class SignalVocIndex(AbstractSignal): + """Measured VOC Index ticks.""" + + def __init__(self, voc_index_raw): + self._voc_index = voc_index_raw / 10.0 + + @property + def value(self): + return self._voc_index + + def __str__(self): + return '{0:.2f}'.format(self.value) + + +class SignalNoxIndex(AbstractSignal): + """Measured NOx Index ticks.""" + + def __init__(self, nox_index_raw): + self._nox_index = nox_index_raw / 10.0 + + @property + def value(self): + return self._nox_index + + def __str__(self): + return '{0:.2f}'.format(self.value) + diff --git a/sensirion_uart_svm4x/version.py b/sensirion_uart_svm4x/version.py new file mode 100644 index 0000000..338edf5 --- /dev/null +++ b/sensirion_uart_svm4x/version.py @@ -0,0 +1,5 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +from __future__ import absolute_import, division, print_function + +version = "0.1.0" diff --git a/setup.cfg b/setup.cfg index f3c283a..c8b5f3a 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,5 +1,5 @@ [tool:pytest] -addopts = --cov=sensirion_shdlc_svm41 +addopts = --cov=sensirion_uart_svm4x markers = needs_device: Marks tests which need any kind of hardware connected diff --git a/setup.py b/setup.py index 94351b7..71b79cf 100644 --- a/setup.py +++ b/setup.py @@ -12,8 +12,8 @@ # Packages that this package imports. List everything apart from standard lib packages. install_requires = [ 'sensirion-shdlc-driver~=0.1.5', - 'sensirion-driver-adapters~=2.1.2', - 'sensirion_driver_support_types~=0.1.0', + 'sensirion-driver-adapters>=2.1.8,<3.0', + 'sensirion-driver-support-types~=0.2.0', ] # Packages required for tests and docs @@ -32,7 +32,7 @@ } # Read version number from version.py -version_line = open("sensirion_shdlc_svm41/version.py", "rt").read() +version_line = open("sensirion_uart_svm4x/version.py", "rt").read() result = re.search(r"^version = ['\"]([^'\"]*)['\"]", version_line, re.M) if result: version_string = result.group(1) @@ -41,24 +41,27 @@ # Use README.rst and CHANGELOG.rst as package description root_path = os.path.dirname(__file__) -readme = open(os.path.join(root_path, 'README.rst')).read() -changelog = open(os.path.join(root_path, 'CHANGELOG.rst')).read() -long_description = readme.strip() + "\n\n" + changelog.strip() + "\n" +long_description = open(os.path.join(root_path, 'README.md')).read() setup( - name='sensirion_shdlc_svm41', + name='sensirion_uart_svm4x', version=version_string, author='Sensirion', author_email='info@sensirion.com', - description='SHDLC driver for the Sensirion SVM41 sensor family', + description='SHDLC driver for the Sensirion SVM4X sensor family', license='BSD', - keywords="""Sensirion SVM41 + keywords="""Sensirion SVM4X SHDLC UART - SVM41""", - url='https://sensirion.github.io/python-uart-svm41/', + SVM4x""", + project_urls={ + "Documentation": "https://sensirion.github.io/python-uart-svm4x", + "Repository": "https://github.com/Sensirion/python-uart-svm4x", + "Changelog": "https://github.com/Sensirion/python-uart-svm4x/blob/master/CHANGELOG.rst", + }, packages=find_packages(exclude=['tests', 'tests.*']), long_description=long_description, + long_description_content_type='text/markdown', python_requires=python_requires, install_requires=install_requires, extras_require=extras_require, diff --git a/tests/test_svm4x.py b/tests/test_svm4x.py new file mode 100644 index 0000000..320da83 --- /dev/null +++ b/tests/test_svm4x.py @@ -0,0 +1,179 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +# +# (c) Copyright 2023 Sensirion AG, Switzerland +# +# THIS FILE IS AUTOMATICALLY GENERATED! +# +# Generator: sensirion-driver-generator 0.32.0 +# Product: svm4x +# Model-Version: 2.0.0 +# + +import pytest +from sensirion_uart_svm4x.device import Svm4xDevice + + +@pytest.fixture +def sensor(channel_provider): + # Create Channel + with channel_provider: + channel = channel_provider.get_channel(0.05) + dev = Svm4xDevice(channel) + yield dev + + +def test_get_serial_number1(sensor): + serial_number = sensor.get_serial_number() + print(f"serial_number: {serial_number}; " + ) + + +def test_get_product_type1(sensor): + product_type = sensor.get_product_type() + print(f"product_type: {product_type}; " + ) + + +def test_get_product_name1(sensor): + product_name = sensor.get_product_name() + print(f"product_name: {product_name}; " + ) + + +def test_get_version1(sensor): + (firmware_major, firmware_minor, firmware_debug, hardware_major, hardware_minor, protocol_major, protocol_minor + ) = sensor.get_version() + print(f"firmware_major: {firmware_major}; " + f"firmware_minor: {firmware_minor}; " + f"firmware_debug: {firmware_debug}; " + f"hardware_major: {hardware_major}; " + f"hardware_minor: {hardware_minor}; " + f"protocol_major: {protocol_major}; " + f"protocol_minor: {protocol_minor}; " + ) + + +def test_device_reset1(sensor): + sensor.device_reset() + + +def test_get_system_up_time1(sensor): + system_up_time = sensor.get_system_up_time() + print(f"system_up_time: {system_up_time}; " + ) + + +def test_get_temperature_offset_for_rht_measurements1(sensor): + t_offset = sensor.get_temperature_offset_for_rht_measurements() + print(f"t_offset: {t_offset}; " + ) + + +def test_get_voc_tuning_parameters1(sensor): + (voc_index_offset, learning_time_offset_hours, learning_time_gain_hours, gating_max_duration_minutes, std_initial, + gain_factor + ) = sensor.get_voc_tuning_parameters() + print(f"voc_index_offset: {voc_index_offset}; " + f"learning_time_offset_hours: {learning_time_offset_hours}; " + f"learning_time_gain_hours: {learning_time_gain_hours}; " + f"gating_max_duration_minutes: {gating_max_duration_minutes}; " + f"std_initial: {std_initial}; " + f"gain_factor: {gain_factor}; " + ) + + +def test_get_nox_tuning_parameters1(sensor): + (nox_index_offset, learning_time_offset_hours, learning_time_gain_hours, gating_max_duration_minutes, std_initial, + gain_factor + ) = sensor.get_nox_tuning_parameters() + print(f"nox_index_offset: {nox_index_offset}; " + f"learning_time_offset_hours: {learning_time_offset_hours}; " + f"learning_time_gain_hours: {learning_time_gain_hours}; " + f"gating_max_duration_minutes: {gating_max_duration_minutes}; " + f"std_initial: {std_initial}; " + f"gain_factor: {gain_factor}; " + ) + + +def test_store_nv_data1(sensor): + sensor.store_nv_data() + + +def test_set_temperature_offset_for_rht_measurements1(sensor): + sensor.set_temperature_offset_for_rht_measurements(0) + + +def test_set_voc_tuning_parameters1(sensor): + sensor.set_voc_tuning_parameters(100, 12, 12, 180, 50, 230) + + +def test_set_nox_tuning_parameters1(sensor): + sensor.set_nox_tuning_parameters(1, 12, 12, 720, 50, 230) + + +def test_set_voc_state1(sensor): + sensor.set_voc_state((0, 0, 0, 0, 0, 0, 0, 0)) + + +def test_start_measurement1(sensor): + sensor.start_measurement() + (a_humidity, a_temperature, a_voc_index, a_nox_index + ) = sensor.read_measured_values() + print(f"a_humidity: {a_humidity}; " + f"a_temperature: {a_temperature}; " + f"a_voc_index: {a_voc_index}; " + f"a_nox_index: {a_nox_index}; " + ) + (humidity, temperature, voc_index, nox_index + ) = sensor.read_measured_values_as_integers() + print(f"humidity: {humidity}; " + f"temperature: {temperature}; " + f"voc_index: {voc_index}; " + f"nox_index: {nox_index}; " + ) + (raw_humidity, raw_temperature, raw_voc_ticks, raw_nox_ticks + ) = sensor.read_measured_raw_values() + print(f"raw_humidity: {raw_humidity}; " + f"raw_temperature: {raw_temperature}; " + f"raw_voc_ticks: {raw_voc_ticks}; " + f"raw_nox_ticks: {raw_nox_ticks}; " + ) + t_offset = sensor.get_temperature_offset_for_rht_measurements() + print(f"t_offset: {t_offset}; " + ) + (voc_index_offset, learning_time_offset_hours, learning_time_gain_hours, gating_max_duration_minutes, std_initial, + gain_factor + ) = sensor.get_voc_tuning_parameters() + print(f"voc_index_offset: {voc_index_offset}; " + f"learning_time_offset_hours: {learning_time_offset_hours}; " + f"learning_time_gain_hours: {learning_time_gain_hours}; " + f"gating_max_duration_minutes: {gating_max_duration_minutes}; " + f"std_initial: {std_initial}; " + f"gain_factor: {gain_factor}; " + ) + (nox_index_offset, learning_time_offset_hours, learning_time_gain_hours, gating_max_duration_minutes, std_initial, + gain_factor + ) = sensor.get_nox_tuning_parameters() + print(f"nox_index_offset: {nox_index_offset}; " + f"learning_time_offset_hours: {learning_time_offset_hours}; " + f"learning_time_gain_hours: {learning_time_gain_hours}; " + f"gating_max_duration_minutes: {gating_max_duration_minutes}; " + f"std_initial: {std_initial}; " + f"gain_factor: {gain_factor}; " + ) + state = sensor.get_voc_state() + print(f"state: {state}; " + ) + (firmware_major, firmware_minor, firmware_debug, hardware_major, hardware_minor, protocol_major, protocol_minor + ) = sensor.get_version() + print(f"firmware_major: {firmware_major}; " + f"firmware_minor: {firmware_minor}; " + f"firmware_debug: {firmware_debug}; " + f"hardware_major: {hardware_major}; " + f"hardware_minor: {hardware_minor}; " + f"protocol_major: {protocol_major}; " + f"protocol_minor: {protocol_minor}; " + ) + sensor.stop_measurement() +