diff --git a/.bandit b/.bandit new file mode 100644 index 00000000..46ed20c0 --- /dev/null +++ b/.bandit @@ -0,0 +1,3 @@ +# codacy: +# Don't flag valid Python "assert" statements +skips: ['B101'] diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml new file mode 100644 index 00000000..3c64fd09 --- /dev/null +++ b/.github/workflows/lint.yml @@ -0,0 +1,35 @@ +name: lint + +on: + - push + - pull_request + +jobs: + lint: + name: Lint + runs-on: ubuntu-latest + # The setup steps are duplicated between jobs. We should create a + # composite action for the setup, once composite actions support + # "uses". + steps: + - uses: actions/checkout@v3 + - uses: actions/setup-python@v4 + with: + python-version: ${{ matrix.python-version }} + - run: | + python -m pip install --upgrade pip + pip install tox tox-gh-actions + - run: tox -e lint + + docs: + name: Docs + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + - uses: actions/setup-python@v4 + with: + python-version: ${{ matrix.python-version }} + - run: | + python -m pip install --upgrade pip + pip install tox tox-gh-actions + - run: tox -e docs diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml new file mode 100644 index 00000000..3cab7dfb --- /dev/null +++ b/.github/workflows/publish.yml @@ -0,0 +1,26 @@ +name: Publish + +on: + release: + types: [published] + + +jobs: + pypi-publish: + runs-on: ubuntu-latest + + environment: pypi + + permissions: + id-token: write + + steps: + - uses: actions/checkout@v4 + + - name: build wheel and sdist + run: | + pip install "flit>=3.2.0,<4.0.0" + flit build + + - name: publish + uses: pypa/gh-action-pypi-publish@release/v1 diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 00000000..9bf389be --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,34 @@ +name: Draft release + +on: + push: + tags: + - '[0-9]+.[0-9]+.[0-9]+' + +jobs: + set-body: + runs-on: ubuntu-latest + + steps: + - run: sudo apt install pcregrep + + - uses: actions/checkout@v4 + + - name: Parse changelog + id: parse-changelog + run: | + tag='${{ github.ref_name }}' + re_current_tag="## \[$tag\].*\n\n" # Match, but do not capture, current version tag, then... + re_changes_body='((.|\n)+?)' # capture everything including newlines... + re_previous_tag='## \[[0-9]+.[0-9]+.[0-9]+\]' # until previous version tag. + re_full="${re_current_tag}${re_changes_body}${re_previous_tag}" + echo 'match<> $GITHUB_OUTPUT + # Match multiple lines, output capture group 1. + pcregrep -M -o1 "$re_full" ./CHANGELOG.md >> $GITHUB_OUTPUT + echo 'EOF' >> $GITHUB_OUTPUT + + - name: Set release body + uses: softprops/action-gh-release@v2 + with: + draft: true + body: ${{ steps.parse-changelog.outputs.match }} diff --git a/.gitignore b/.gitignore index 4d22ef99..73358ad8 100644 --- a/.gitignore +++ b/.gitignore @@ -1,8 +1,139 @@ -*~ -*.pyc -__pycache__ -__PYCACHE__ -build -_* -PSL.egg-info -.idea/ +# 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 + +# PEP 582; used by e.g. github.com/David-OConnor/pyflow +__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/ + diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index f4be74f6..00000000 --- a/.travis.yml +++ /dev/null @@ -1,17 +0,0 @@ -language: python -python: - - "2.6" - - "2.7" - - "3.2" - - "3.3" - - "3.4" - # - "3.5" - #- "3.5-dev" # 3.5 development branch - #- "nightly" # currently points to 3.6-dev -# command to install dependencies -#install: "pip install -r requirements.txt" -# command to run tests -script: nosetests - -notifications: - slack: fossasia:bqOzo4C9y6oI6dTF8kO8zdxp diff --git a/99-pslab.rules b/99-pslab.rules deleted file mode 100644 index 84ef45fb..00000000 --- a/99-pslab.rules +++ /dev/null @@ -1,4 +0,0 @@ -#Rules for TestBench - -SUBSYSTEM=="tty",ATTRS{idVendor}=="04d8", ATTRS{idProduct}=="00df", MODE="666",SYMLINK+="TestBench" -ATTRS{idVendor}=="04d8", ATTRS{idProduct}=="00df", ENV{ID_MM_DEVICE_IGNORE}="1" diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 00000000..5b7b3120 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,53 @@ +# Changelog + +## [4.0.1] - 2025-06-16 + +### Fixed + +- Fix UART passthrough ([`1346356`](https://github.com/fossasia/pslab-python/commit/1346356d3cf95691ea0e3e87be335659d5513ec8)) (Anashuman Singh) + +## [4.0.0] - 2025-02-19 + +### Changed + +- __Breaking__: Do not autoconnect on `SerialHandler` zero-arg instantiation ([`e70d01d`](https://github.com/fossasia/pslab-python/commit/e70d01d8761b7c0d8446994447849561450d5200)) (Alexander Bessman) +- __Breaking__: Deprecate `serial_handler` in favor of `connection` ([`bc53dd3`](https://github.com/fossasia/pslab-python/commit/bc53dd38830f3d70e908e2c4f1ae797a809231a6)) (Alexander Bessman) +- __Breaking__: Move `SerialHandler` to `connection` ([`e70d01d`](https://github.com/fossasia/pslab-python/commit/e70d01d8761b7c0d8446994447849561450d5200)) (Alexander Bessman) +- __Breaking__: Move `detect` to `connection` ([`e70d01d`](https://github.com/fossasia/pslab-python/commit/e70d01d8761b7c0d8446994447849561450d5200)) (Alexander Bessman) +- __Breaking__: Make `check_serial_access_permission` private ([`e70d01d`](https://github.com/fossasia/pslab-python/commit/e70d01d8761b7c0d8446994447849561450d5200)) (Alexander Bessman) +- __Breaking__: Move `ADCBufferMixin` to `instrument.buffer` ([`e70d01d`](https://github.com/fossasia/pslab-python/commit/e70d01d8761b7c0d8446994447849561450d5200)) (Alexander Bessman) + +### Added + +- Add common `connection` module for different control interfaces ([`e70d01d`](https://github.com/fossasia/pslab-python/commit/e70d01d8761b7c0d8446994447849561450d5200)) (Alexander Bessman) +- Add `WLANHandler` class for controlling the PSLab over WLAN ([`1316df4`](https://github.com/fossasia/pslab-python/commit/1316df452bff97106dc9313fe9458c93d7f954ab)) (Alexander Bessman) +- Add `ConnectionHandler` base class for `SerialHandler` and `WLANHandler` ([`e70d01d`](https://github.com/fossasia/pslab-python/commit/e70d01d8761b7c0d8446994447849561450d5200)) (Alexander Bessman) +- Add `connection.autoconnect` function ([`e70d01d`](https://github.com/fossasia/pslab-python/commit/e70d01d8761b7c0d8446994447849561450d5200)) (Alexander Bessman) +- Add `instrument.buffer` module ([`e70d01d`](https://github.com/fossasia/pslab-python/commit/e70d01d8761b7c0d8446994447849561450d5200)) (Alexander Bessman) + +### Removed + +- __Breaking__: Remove `SerialHandler.wait_for_data` ([`e70d01d`](https://github.com/fossasia/pslab-python/commit/e70d01d8761b7c0d8446994447849561450d5200)) (Alexander Bessman) + +### Fixed + +- Fix SPI configuration sending one byte too few ([`a3d88bb`](https://github.com/fossasia/pslab-python/commit/a3d88bbfeee8cdb012d033c6c80f40b971802851)) (Alexander Bessman) + +## [3.1.1] - 2025-01-05 + +### Changed + +- Raise `RuntimeError` if `_I2CPrimitive._start` is called on an already active peripheral ([`d86fbfa`](https://github.com/fossasia/pslab-python/commit/d86fbfa324b6671926a8548340221b40228c782c)) (Alexander Bessman) + +### Fixed + +- Fix I2C bus becomes unusable after device scan ([`05c135d`](https://github.com/fossasia/pslab-python/commit/05c135d8c59b5075a1d36e3af256022f5759a3a5)) (Alexander Bessman) + +## [3.1.0] - 2024-12-28 + +_Changelog added in following release._ + +[4.0.1]: https://github.com/fossasia/pslab-python/releases/tag/4.0.1 +[4.0.0]: https://github.com/fossasia/pslab-python/releases/tag/4.0.0 +[3.1.1]: https://github.com/fossasia/pslab-python/releases/tag/3.1.1 +[3.1.0]: https://github.com/fossasia/pslab-python/releases/tag/3.1.0 diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 00000000..3c3bc1a8 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,86 @@ +# Contributing to pslab-python + +Before opening a pull request, make sure that: + +1. The code builds. +2. The docs build. +3. The linters pass. +4. The tests pass. +5. Test coverage has not decreased. + +## Building & Installing + +The following assumes that the commands are executed from the root of the repository: + +The project can be built with: + + pip install wheel + python setup.py sdist bdist_wheel + +The project can be installed in editable mode with: + + pip install -e . + +The documentation can be built with: + + tox -e docs + +The linters can be run with: + + tox -e lint + +## Testing + +pslab-python tests are written to run against real hardware. The tests are integration tests, since they depend on the correct function of not just the Python code under test, but also the firmware and hardware of the connected PSLab device. The tests can be run with: + + tox -e integration + +When running the tests, the serial traffic to and from the PSLab can optionally be recorded by running: + + tox -e record + +The recorded traffic is played back when running the tests on Travis, since no real device is connected in that situation. The tests can be run with recorded traffic instead of against real hardware by running: + + tox -e playback + +### Writing new tests + +Tests are written in pytest. + +If the test requires multiple PSLab instruments to be connected together, this should be documented in the module docstring. + +Test coverage should be \>90%, but aim for 100%. + +## Code style + +### General + +- Black. +- When in doubt, refer to PEP8. +- Use type hints (PEP484). +- Maximum line length is 88 characters, but aim for less than 80. +- Maximum [cyclomatic complexity](https://en.wikipedia.org/wiki/Cyclomatic_complexity) is ten, but aim for five or lower. +- Blank lines before and after statements (for, if, return, \...), unless + - the statement comes at the beginning or end of another statement. + - the indentation level is five lines or fewer long. + +### Imports + +- All imports at the top of the module. +- Built-in imports come first, then third-party, and finally pslab, with a blank line between each group of imports. +- No relative imports. +- Within import groups, `import`-style imports come before `from`-style imports. +- One `import`-style import per line. +- All `from`-style imports from a specific package or module on the same line, unless that would violate the line length limit. + - In that case, strongly consider using `import`-style instead. + - If that is not possible, use one import per line. +- Imports are sorted alphabetically within groups. + +### Comments and docstrings + +- All public interfaces (modules, classes, methods) have Numpydoc-style docstrings. +- Blank line after module- and class-level docstrings, but not after method-level docstrings. +- Comments start with a capital letter and end with a period if they contain at least two words. +- Comments go on the same line as the code they explain, unless that would violate the line length limit. + - In that case, the comment goes immediately before the code it explains. +- Avoid multiline comments. diff --git a/Makefile b/Makefile deleted file mode 100644 index 1589e2de..00000000 --- a/Makefile +++ /dev/null @@ -1,31 +0,0 @@ -DESTDIR = -all: - #make -C docs html - #make -C docs/misc all - # make in subdirectory PSLab-apps-master if it is there - [ ! -d PSLab-apps-master ] || make -C PSLab-apps-master $@ DESTDIR=$(DESTDIR) - python setup.py build - -clean: - rm -rf docs/_* - # make in subdirectory PSLab-apps-master if it is there - [ ! -d PSLab-apps-master ] || make -C PSLab-apps-master $@ DESTDIR=$(DESTDIR) - rm -rf PSL.egg-info build - find . -name "*~" -o -name "*.pyc" -o -name "__pycache__" | xargs rm -rf - -IMAGEDIR=$(DESTDIR)/usr/share/doc/pslab-common/images - -install: - # make in subdirectory PSLab-apps-master if it is there - [ ! -d PSLab-apps-master ] || make -C PSLab-apps-master $@ DESTDIR=$(DESTDIR) - # install documents - install -d $(DESTDIR)/usr/share/doc/pslab - #cp -a docs/_build/html $(DESTDIR)/usr/share/doc/pslab - #cp docs/misc/build/*.html $(DESTDIR)/usr/share/doc/pslab/html - python setup.py install --install-layout=deb \ - --root=$(DESTDIR)/ --prefix=/usr - # rules for udev - mkdir -p $(DESTDIR)/lib/udev/rules.d - install -m 644 99-pslab.rules $(DESTDIR)/lib/udev/rules.d/99-pslab - # fix a few permissions - #find $(DESTDIR)/usr/share/pslab/psl_res -name auto.sh -exec chmod -x {} \; diff --git a/PSL/Peripherals.py b/PSL/Peripherals.py deleted file mode 100644 index 5574a8f2..00000000 --- a/PSL/Peripherals.py +++ /dev/null @@ -1,1592 +0,0 @@ -from __future__ import print_function -import commands_proto as CP -import numpy as np -import time, inspect - - -class I2C(): - """ - Methods to interact with the I2C port. An instance of Labtools.Packet_Handler must be passed to the init function - - - Example:: Read Values from an HMC5883L 3-axis Magnetometer(compass) [GY-273 sensor] connected to the I2C port - >>> ADDRESS = 0x1E - >>> from PSL import sciencelab - >>> I = sciencelab.connect() - #Alternately, you may skip using I2C as a child instance of Interface, - #and instead use I2C=PSL.Peripherals.I2C(PSL.packet_handler.Handler()) - - # writing to 0x1E, set gain(0x01) to smallest(0 : 1x) - >>> I.I2C.bulkWrite(ADDRESS,[0x01,0]) - - # writing to 0x1E, set mode conf(0x02), continuous measurement(0) - >>> I.I2C.bulkWrite(ADDRESS,[0x02,0]) - - # read 6 bytes from addr register on I2C device located at ADDRESS - >>> vals = I.I2C.bulkRead(ADDRESS,addr,6) - - >>> from numpy import int16 - #conversion to signed datatype - >>> x=int16((vals[0]<<8)|vals[1]) - >>> y=int16((vals[2]<<8)|vals[3]) - >>> z=int16((vals[4]<<8)|vals[5]) - >>> print (x,y,z) - - """ - samples = 0 - total_bytes=0 - channels = 0 - tg=100 - MAX_SAMPLES = 10000 - def __init__(self, H): - self.H = H - from PSL import sensorlist - self.SENSORS = sensorlist.sensors - self.buff = np.zeros(10000) - - def init(self): - try: - self.H.__sendByte__(CP.I2C_HEADER) - self.H.__sendByte__(CP.I2C_INIT) - self.H.__get_ack__() - except Exception as ex: - self.raiseException(ex, "Communication Error , Function : " + inspect.currentframe().f_code.co_name) - - def enable_smbus(self): - try: - self.H.__sendByte__(CP.I2C_HEADER) - self.H.__sendByte__(CP.I2C_ENABLE_SMBUS) - self.H.__get_ack__() - except Exception as ex: - self.raiseException(ex, "Communication Error , Function : " + inspect.currentframe().f_code.co_name) - - def pullSCLLow(self, uS): - """ - Hold SCL pin at 0V for a specified time period. Used by certain sensors such - as MLX90316 PIR for initializing. - - .. tabularcolumns:: |p{3cm}|p{11cm}| - - ================ ============================================================================================ - **Arguments** - ================ ============================================================================================ - uS Time(in uS) to hold SCL output at 0 Volts - ================ ============================================================================================ - - """ - try: - self.H.__sendByte__(CP.I2C_HEADER) - self.H.__sendByte__(CP.I2C_PULLDOWN_SCL) - self.H.__sendInt__(uS) - self.H.__get_ack__() - except Exception as ex: - self.raiseException(ex, "Communication Error , Function : " + inspect.currentframe().f_code.co_name) - - def config(self, freq, verbose=True): - """ - Sets frequency for I2C transactions - - .. tabularcolumns:: |p{3cm}|p{11cm}| - - ================ ============================================================================================ - **Arguments** - ================ ============================================================================================ - freq I2C frequency - ================ ============================================================================================ - """ - try: - self.H.__sendByte__(CP.I2C_HEADER) - self.H.__sendByte__(CP.I2C_CONFIG) - # freq=1/((BRGVAL+1.0)/64e6+1.0/1e7) - BRGVAL = int((1. / freq - 1. / 1e7) * 64e6 - 1) - if BRGVAL > 511: - BRGVAL = 511 - if verbose: print('Frequency too low. Setting to :', 1 / ((BRGVAL + 1.0) / 64e6 + 1.0 / 1e7)) - self.H.__sendInt__(BRGVAL) - self.H.__get_ack__() - except Exception as ex: - self.raiseException(ex, "Communication Error , Function : " + inspect.currentframe().f_code.co_name) - - def start(self, address, rw): - """ - Initiates I2C transfer to address via the I2C port - - .. tabularcolumns:: |p{3cm}|p{11cm}| - - ================ ============================================================================================ - **Arguments** - ================ ============================================================================================ - address I2C slave address\n - rw Read/write. - - 0 for writing - - 1 for reading. - ================ ============================================================================================ - """ - try: - self.H.__sendByte__(CP.I2C_HEADER) - self.H.__sendByte__(CP.I2C_START) - self.H.__sendByte__(((address << 1) | rw) & 0xFF) # address - return self.H.__get_ack__() >> 4 - except Exception as ex: - self.raiseException(ex, "Communication Error , Function : " + inspect.currentframe().f_code.co_name) - - def stop(self): - """ - stops I2C transfer - - :return: Nothing - """ - try: - self.H.__sendByte__(CP.I2C_HEADER) - self.H.__sendByte__(CP.I2C_STOP) - self.H.__get_ack__() - except Exception as ex: - self.raiseException(ex, "Communication Error , Function : " + inspect.currentframe().f_code.co_name) - - def wait(self): - """ - wait for I2C - - :return: Nothing - """ - try: - self.H.__sendByte__(CP.I2C_HEADER) - self.H.__sendByte__(CP.I2C_WAIT) - self.H.__get_ack__() - except Exception as ex: - self.raiseException(ex, "Communication Error , Function : " + inspect.currentframe().f_code.co_name) - - def send(self, data): - """ - SENDS data over I2C. - The I2C bus needs to be initialized and set to the correct slave address first. - Use I2C.start(address) for this. - - .. tabularcolumns:: |p{3cm}|p{11cm}| - - ================ ============================================================================================ - **Arguments** - ================ ============================================================================================ - data Sends data byte over I2C bus - ================ ============================================================================================ - - :return: Nothing - """ - try: - self.H.__sendByte__(CP.I2C_HEADER) - self.H.__sendByte__(CP.I2C_SEND) - self.H.__sendByte__(data) # data byte - return self.H.__get_ack__() >> 4 - except Exception as ex: - self.raiseException(ex, "Communication Error , Function : " + inspect.currentframe().f_code.co_name) - - def send_burst(self, data): - """ - SENDS data over I2C. The function does not wait for the I2C to finish before returning. - It is used for sending large packets quickly. - The I2C bus needs to be initialized and set to the correct slave address first. - Use start(address) for this. - - .. tabularcolumns:: |p{3cm}|p{11cm}| - - ================ ============================================================================================ - **Arguments** - ================ ============================================================================================ - data Sends data byte over I2C bus - ================ ============================================================================================ - - :return: Nothing - """ - try: - self.H.__sendByte__(CP.I2C_HEADER) - self.H.__sendByte__(CP.I2C_SEND_BURST) - self.H.__sendByte__(data) # data byte - except Exception as ex: - self.raiseException(ex, "Communication Error , Function : " + inspect.currentframe().f_code.co_name) - # No handshake. for the sake of speed. e.g. loading a frame buffer onto an I2C display such as ssd1306 - - def restart(self, address, rw): - """ - Initiates I2C transfer to address - - .. tabularcolumns:: |p{3cm}|p{11cm}| - - ================ ============================================================================================ - **Arguments** - ================ ============================================================================================ - address I2C slave address - rw Read/write. - * 0 for writing - * 1 for reading. - ================ ============================================================================================ - - """ - try: - self.H.__sendByte__(CP.I2C_HEADER) - self.H.__sendByte__(CP.I2C_RESTART) - self.H.__sendByte__(((address << 1) | rw) & 0xFF) # address - except Exception as ex: - self.raiseException(ex, "Communication Error , Function : " + inspect.currentframe().f_code.co_name) - return self.H.__get_ack__() >> 4 - - def simpleRead(self, addr, numbytes): - """ - Read bytes from I2C slave without first transmitting the read location. - - .. tabularcolumns:: |p{3cm}|p{11cm}| - - ================ ============================================================================================ - **Arguments** - ================ ============================================================================================ - addr Address of I2C slave - numbytes Total Bytes to read - ================ ============================================================================================ - """ - self.start(addr, 1) - vals = self.read(numbytes) - return vals - - def read(self, length): - """ - Reads a fixed number of data bytes from I2C device. Fetches length-1 bytes with acknowledge bits for each, +1 byte - with Nack. - - .. tabularcolumns:: |p{3cm}|p{11cm}| - - ================ ============================================================================================ - **Arguments** - ================ ============================================================================================ - length number of bytes to read from I2C bus - ================ ============================================================================================ - """ - data = [] - try: - for a in range(length - 1): - self.H.__sendByte__(CP.I2C_HEADER) - self.H.__sendByte__(CP.I2C_READ_MORE) - data.append(self.H.__getByte__()) - self.H.__get_ack__() - self.H.__sendByte__(CP.I2C_HEADER) - self.H.__sendByte__(CP.I2C_READ_END) - data.append(self.H.__getByte__()) - self.H.__get_ack__() - except Exception as ex: - self.raiseException(ex, "Communication Error , Function : " + inspect.currentframe().f_code.co_name) - return data - - def read_repeat(self): - try: - self.H.__sendByte__(CP.I2C_HEADER) - self.H.__sendByte__(CP.I2C_READ_MORE) - val = self.H.__getByte__() - self.H.__get_ack__() - return val - except Exception as ex: - self.raiseException(ex, "Communication Error , Function : " + inspect.currentframe().f_code.co_name) - - - def read_end(self): - try: - self.H.__sendByte__(CP.I2C_HEADER) - self.H.__sendByte__(CP.I2C_READ_END) - val = self.H.__getByte__() - self.H.__get_ack__() - return val - except Exception as ex: - self.raiseException(ex, "Communication Error , Function : " + inspect.currentframe().f_code.co_name) - - - def read_status(self): - try: - self.H.__sendByte__(CP.I2C_HEADER) - self.H.__sendByte__(CP.I2C_STATUS) - val = self.H.__getInt__() - self.H.__get_ack__() - return val - except Exception as ex: - self.raiseException(ex, "Communication Error , Function : " + inspect.currentframe().f_code.co_name) - - - def readBulk(self, device_address, register_address, bytes_to_read): - try: - self.H.__sendByte__(CP.I2C_HEADER) - self.H.__sendByte__(CP.I2C_READ_BULK) - self.H.__sendByte__(device_address) - self.H.__sendByte__(register_address) - self.H.__sendByte__(bytes_to_read) - data = self.H.fd.read(bytes_to_read) - self.H.__get_ack__() - try: - return [ord(a) for a in data] - except: - print('Transaction failed') - return False - except Exception as ex: - self.raiseException(ex, "Communication Error , Function : " + inspect.currentframe().f_code.co_name) - - def writeBulk(self, device_address, bytestream): - """ - write bytes to I2C slave - - .. tabularcolumns:: |p{3cm}|p{11cm}| - - ================ ============================================================================================ - **Arguments** - ================ ============================================================================================ - device_address Address of I2C slave - bytestream List of bytes to write - ================ ============================================================================================ - """ - try: - self.H.__sendByte__(CP.I2C_HEADER) - self.H.__sendByte__(CP.I2C_WRITE_BULK) - self.H.__sendByte__(device_address) - self.H.__sendByte__(len(bytestream)) - for a in bytestream: - self.H.__sendByte__(a) - self.H.__get_ack__() - except Exception as ex: - self.raiseException(ex, "Communication Error , Function : " + inspect.currentframe().f_code.co_name) - - def scan(self, frequency=100000, verbose=False): - """ - Scan I2C port for connected devices - - .. tabularcolumns:: |p{3cm}|p{11cm}| - - ================ ============================================================================================ - **Arguments** - ================ ============================================================================================ - Frequency I2C clock frequency - ================ ============================================================================================ - - :return: Array of addresses of connected I2C slave devices - - """ - - self.config(frequency, verbose) - addrs = [] - n = 0 - if verbose: - print('Scanning addresses 0-127...') - print('Address', '\t', 'Possible Devices') - for a in range(0, 128): - x = self.start(a, 0) - if x & 1 == 0: # ACK received - addrs.append(a) - if verbose: print(hex(a), '\t\t', self.SENSORS.get(a, 'None')) - n += 1 - self.stop() - return addrs - - def __captureStart__(self,address,location,sample_length,total_samples,tg): - """ - Blocking call that starts fetching data from I2C sensors like an oscilloscope fetches voltage readings - You will then have to call `__retrievebuffer__` to fetch this data, and `__dataProcessor` to process and return separate channels - refer to `capture` if you want a one-stop solution. - - .. tabularcolumns:: |p{3cm}|p{11cm}| - ================== ============================================================================================ - **Arguments** - ================== ============================================================================================ - address Address of the I2C sensor - location Address of the register to read from - sample_length Each sample can be made up of multiple bytes startng from . such as 3-axis data - total_samples Total samples to acquire. Total bytes fetched = total_samples*sample_length - tg timegap between samples (in uS) - ================== ============================================================================================ - - :return: Arrays X(timestamps),Y1,Y2 ... - - """ - if(tg<20):tg=20 - total_bytes = total_samples*sample_length - print ('total bytes calculated : ',total_bytes) - if(total_bytes>self.MAX_SAMPLES*2): - print ('Sample limit exceeded. 10,000 int / 20000 bytes total') - total_bytes = self.MAX_SAMPLES*2 - total_samples = total_bytes/sample_length #2* because sample array is in Integers, and we're using it to store bytes - - - print ('length of each channel : ',sample_length) - self.total_bytes = total_bytes - self.channels = sample_length - self.samples = total_samples - self.tg = tg - - self.H.__sendByte__(CP.I2C_HEADER) - self.H.__sendByte__(CP.I2C_START_SCOPE) - self.H.__sendByte__(address) - self.H.__sendByte__(location) - self.H.__sendByte__(sample_length) - self.H.__sendInt__(total_samples) #total number of samples to record - self.H.__sendInt__(tg) #Timegap between samples. 1MHz timer clock - self.H.__get_ack__() - return 1e-6*self.samples*self.tg+.01 - - def __retrievebuffer__(self): - ''' - Fetch data acquired by the I2C scope. refer to :func:`__captureStart__` - ''' - total_int_samples = self.total_bytes/2 - DATA_SPLITTING = 500 - print ('fetchin samples : ',total_int_samples,' split',DATA_SPLITTING) - data=b'' - for i in range(int(total_int_samples/DATA_SPLITTING)): - self.H.__sendByte__(CP.ADC) - self.H.__sendByte__(CP.GET_CAPTURE_CHANNEL) - self.H.__sendByte__(0) #starts with A0 on PIC - self.H.__sendInt__(DATA_SPLITTING) - self.H.__sendInt__(i*DATA_SPLITTING) - rem = DATA_SPLITTING*2+1 - for _ in range(200): - partial = self.H.fd.read(rem) #reading int by int sometimes causes a communication error. this works better. - rem -=len(partial) - data+=partial - #print ('partial: ',len(partial), end=",") - if rem<=0: - break - data=data[:-1] - #print ('Pass : len=',len(data), ' i = ',i) - - if total_int_samples%DATA_SPLITTING: - self.H.__sendByte__(CP.ADC) - self.H.__sendByte__(CP.GET_CAPTURE_CHANNEL) - self.H.__sendByte__(0) #starts with A0 on PIC - self.H.__sendInt__(total_int_samples%DATA_SPLITTING) - self.H.__sendInt__(total_int_samples-total_int_samples%DATA_SPLITTING) - rem = 2*(total_int_samples%DATA_SPLITTING)+1 - for _ in range(20): - partial = self.H.fd.read(rem) #reading int by int sometimes causes a communication error. this works better. - rem -=len(partial) - data+=partial - #print ('partial: ',len(partial), end="") - if rem<=0: - break - data=data[:-1] - print ('Final Pass : len=',len(data)) - return data - - def __dataProcessor__(self,data,*args): - ''' - Interpret data acquired by the I2C scope. refer to :func:`__retrievebuffer__` to fetch data - - ================== ============================================================================================ - **Arguments** - ================== ============================================================================================ - data byte array returned by :func:`__retrievebuffer__` - *args supply optional argument 'int' if consecutive bytes must be combined to form short integers - ================== ============================================================================================ - - ''' - - try: - data = [ord(a) for a in data] - if('int' in args): - for a in range(self.channels*self.samples/2): self.buff[a] = np.int16((data[a*2]<<8)|data[a*2+1]) - else: - for a in range(self.channels*self.samples): self.buff[a] = data[a] - - yield np.linspace(0,self.tg*(self.samples-1),self.samples) - for a in range(int(self.channels/2)): - yield self.buff[a:self.samples*self.channels/2][::self.channels/2] - except Exception as ex: - msg = "Incorrect number of bytes received",ex - raise RuntimeError(msg) - - - def capture(self, address, location, sample_length, total_samples, tg, *args): - """ - Blocking call that fetches data from I2C sensors like an oscilloscope fetches voltage readings - - .. tabularcolumns:: |p{3cm}|p{11cm}| - - ================== ============================================================================================ - **Arguments** - ================== ============================================================================================ - address Address of the I2C sensor - location Address of the register to read from - sample_length Each sample can be made up of multiple bytes startng from . such as 3-axis data - total_samples Total samples to acquire. Total bytes fetched = total_samples*sample_length - tg timegap between samples (in uS) - ================== ============================================================================================ - - Example - - >>> from pylab import * - >>> I=sciencelab.ScienceLab() - >>> x,y1,y2,y3,y4 = I.capture_multiple(800,1.75,'CH1','CH2','MIC','SEN') - >>> plot(x,y1) - >>> plot(x,y2) - >>> plot(x,y3) - >>> plot(x,y4) - >>> show() - - :return: Arrays X(timestamps),Y1,Y2 ... - - """ - t = self.__captureStart__(address,location,sample_length,total_samples,tg) - time.sleep(t) - data = self.__retrievebuffer__() - return self.__dataProcessor__(data,*args) - - -class SPI(): - """ - Methods to interact with the SPI port. An instance of Packet_Handler must be passed to the init function - - """ - - def __init__(self, H): - self.H = H - - def set_parameters(self, primary_prescaler=0, secondary_prescaler=2, CKE=1, CKP=0, SMP=1): - """ - sets SPI parameters. - - .. tabularcolumns:: |p{3cm}|p{11cm}| - - ================ ============================================================================================ - **Arguments** - ================ ============================================================================================ - primary_pres Primary Prescaler(0,1,2,3) for 64MHz clock->(64:1,16:1,4:1,1:1) - secondary_pres Secondary prescaler(0,1,..7)->(8:1,7:1,..1:1) - CKE CKE 0 or 1. - CKP CKP 0 or 1. - ================ ============================================================================================ - - """ - try: - self.H.__sendByte__(CP.SPI_HEADER) - self.H.__sendByte__(CP.SET_SPI_PARAMETERS) - # 0Bhgfedcba - > : modebit CKP,: modebit CKE, :primary pre,:secondary pre - self.H.__sendByte__(secondary_prescaler | (primary_prescaler << 3) | (CKE << 5) | (CKP << 6) | (SMP << 7)) - self.H.__get_ack__() - except Exception as ex: - self.raiseException(ex, "Communication Error , Function : " + inspect.currentframe().f_code.co_name) - - def start(self, channel): - """ - selects SPI channel to enable. - Basically lowers the relevant chip select pin . - - .. tabularcolumns:: |p{3cm}|p{11cm}| - - ================ ============================================================================================ - **Arguments** - ================ ============================================================================================ - channel 1-7 ->[PGA1 connected to CH1,PGA2,PGA3,PGA4,PGA5,external chip select 1,external chip select 2] - 8 -> sine1 - 9 -> sine2 - ================ ============================================================================================ - - """ - try: - self.H.__sendByte__(CP.SPI_HEADER) - self.H.__sendByte__(CP.START_SPI) - self.H.__sendByte__(channel) # value byte - # self.H.__get_ack__() - except Exception as ex: - self.raiseException(ex, "Communication Error , Function : " + inspect.currentframe().f_code.co_name) - - def set_cs(self, channel, state): - """ - Enable or disable a chip select - - .. tabularcolumns:: |p{3cm}|p{11cm}| - - ================ ============================================================================================ - **Arguments** - ================ ============================================================================================ - channel 'CS1','CS2' - state 1 for HIGH, 0 for LOW - ================ ============================================================================================ - - """ - try: - channel = channel.upper() - if channel in ['CS1', 'CS2']: - csnum = ['CS1', 'CS2'].index(channel) + 9 # chip select number 9=CSOUT1,10=CSOUT2 - self.H.__sendByte__(CP.SPI_HEADER) - if state: - self.H.__sendByte__(CP.STOP_SPI) - else: - self.H.__sendByte__(CP.START_SPI) - self.H.__sendByte__(csnum) - else: - print('Channel does not exist') - except Exception as ex: - self.raiseException(ex, "Communication Error , Function : " + inspect.currentframe().f_code.co_name) - - def stop(self, channel): - """ - selects SPI channel to disable. - Sets the relevant chip select pin to HIGH. - - .. tabularcolumns:: |p{3cm}|p{11cm}| - - ================ ============================================================================================ - **Arguments** - ================ ============================================================================================ - channel 1-7 ->[PGA1 connected to CH1,PGA2,PGA3,PGA4,PGA5,external chip select 1,external chip select 2] - ================ ============================================================================================ - - - """ - try: - self.H.__sendByte__(CP.SPI_HEADER) - self.H.__sendByte__(CP.STOP_SPI) - self.H.__sendByte__(channel) # value byte - except Exception as ex: - self.raiseException(ex, "Communication Error , Function : " + inspect.currentframe().f_code.co_name) - # self.H.__get_ack__() - - def send8(self, value): - """ - SENDS 8-bit data over SPI - - .. tabularcolumns:: |p{3cm}|p{11cm}| - - ================ ============================================================================================ - **Arguments** - ================ ============================================================================================ - value value to transmit - ================ ============================================================================================ - - :return: value returned by slave device - """ - try: - self.H.__sendByte__(CP.SPI_HEADER) - self.H.__sendByte__(CP.SEND_SPI8) - self.H.__sendByte__(value) # value byte - v = self.H.__getByte__() - self.H.__get_ack__() - return v - except Exception as ex: - self.raiseException(ex, "Communication Error , Function : " + inspect.currentframe().f_code.co_name) - - - def send16(self, value): - """ - SENDS 16-bit data over SPI - - .. tabularcolumns:: |p{3cm}|p{11cm}| - - ================ ============================================================================================ - **Arguments** - ================ ============================================================================================ - value value to transmit - ================ ============================================================================================ - - :return: value returned by slave device - :rtype: int - """ - try: - self.H.__sendByte__(CP.SPI_HEADER) - self.H.__sendByte__(CP.SEND_SPI16) - self.H.__sendInt__(value) # value byte - v = self.H.__getInt__() - self.H.__get_ack__() - return v - except Exception as ex: - self.raiseException(ex, "Communication Error , Function : " + inspect.currentframe().f_code.co_name) - - - def send8_burst(self, value): - """ - SENDS 8-bit data over SPI - No acknowledge/return value - - .. tabularcolumns:: |p{3cm}|p{11cm}| - - ================ ============================================================================================ - **Arguments** - ================ ============================================================================================ - value value to transmit - ================ ============================================================================================ - - :return: Nothing - """ - try: - self.H.__sendByte__(CP.SPI_HEADER) - self.H.__sendByte__(CP.SEND_SPI8_BURST) - self.H.__sendByte__(value) # value byte - except Exception as ex: - self.raiseException(ex, "Communication Error , Function : " + inspect.currentframe().f_code.co_name) - - def send16_burst(self, value): - """ - SENDS 16-bit data over SPI - no acknowledge/return value - - .. tabularcolumns:: |p{3cm}|p{11cm}| - - ============== ============================================================================================ - **Arguments** - ============== ============================================================================================ - value value to transmit - ============== ============================================================================================ - - :return: nothing - """ - try: - self.H.__sendByte__(CP.SPI_HEADER) - self.H.__sendByte__(CP.SEND_SPI16_BURST) - self.H.__sendInt__(value) # value byte - except Exception as ex: - self.raiseException(ex, "Communication Error , Function : " + inspect.currentframe().f_code.co_name) - - def xfer(self,chan,data): - self.start(chan) - reply=[] - for a in data: - reply.append(self.send8(a)) - self.stop(chan) - return reply - -class DACCHAN: - def __init__(self, name, span, channum, **kwargs): - self.name = name - self.channum = channum - self.VREF = kwargs.get('VREF', 0) - self.SwitchedOff = kwargs.get('STATE', 0) - self.range = span - slope = (span[1] - span[0]) - intercept = span[0] - self.VToCode = np.poly1d([4095. / slope, -4095. * intercept / slope]) - self.CodeToV = np.poly1d([slope / 4095., intercept]) - self.calibration_enabled = False - self.calibration_table = [] - self.slope = 1 - self.offset = 0 - - def load_calibration_table(self, table): - self.calibration_enabled = 'table' - self.calibration_table = table - - def load_calibration_twopoint(self, slope, offset): - self.calibration_enabled = 'twopoint' - self.slope = slope - self.offset = offset - - # print('########################',slope,offset) - - - def apply_calibration(self, v): - if self.calibration_enabled == 'table': # Each point is individually calibrated - return int(np.clip(v + self.calibration_table[v], 0, 4095)) - elif self.calibration_enabled == 'twopoint': # Overall slope and offset correction is applied - # print (self.slope,self.offset,v) - return int(np.clip(v * self.slope + self.offset, 0, 4095)) - else: - return v - - -class MCP4728: - defaultVDD = 3300 - RESET = 6 - WAKEUP = 9 - UPDATE = 8 - WRITEALL = 64 - WRITEONE = 88 - SEQWRITE = 80 - VREFWRITE = 128 - GAINWRITE = 192 - POWERDOWNWRITE = 160 - GENERALCALL = 0 - - # def __init__(self,I2C,vref=3.3,devid=0): - def __init__(self, H, vref=3.3, devid=0): - self.devid = devid - self.addr = 0x60 | self.devid # 0x60 is the base address - self.H = H - self.I2C = I2C(self.H) - self.SWITCHEDOFF = [0, 0, 0, 0] - self.VREFS = [0, 0, 0, 0] # 0=Vdd,1=Internal reference - self.CHANS = {'PCS': DACCHAN('PCS', [0, 3.3e-3], 0), 'PV3': DACCHAN('PV3', [0, 3.3], 1), - 'PV2': DACCHAN('PV2', [-3.3, 3.3], 2), 'PV1': DACCHAN('PV1', [-5., 5.], 3)} - self.CHANNEL_MAP = {0: 'PCS', 1: 'PV3', 2: 'PV2', 3: 'PV1'} - self.values = {'PV1': 0, 'PV2': 0, 'PV3': 0, 'PCS': 0} - - def __ignoreCalibration__(self, name): - self.CHANS[name].calibration_enabled = False - - def setVoltage(self, name, v): - chan = self.CHANS[name] - v = int(round(chan.VToCode(v))) - return self.__setRawVoltage__(name, v) - - def getVoltage(self, name): - return self.values[name] - - def setCurrent(self, v): - chan = self.CHANS['PCS'] - v = int(round(chan.VToCode(v))) - return self.__setRawVoltage__('PCS', v) - - def __setRawVoltage__(self, name, v): - v = int(np.clip(v, 0, 4095)) - CHAN = self.CHANS[name] - ''' - self.H.__sendByte__(CP.DAC) #DAC write coming through.(MCP4728) - self.H.__sendByte__(CP.SET_DAC) - self.H.__sendByte__(self.addr<<1) #I2C address - self.H.__sendByte__(CHAN.channum) #DAC channel - if self.calibration_enabled[name]: - val = v+self.calibration_tables[name][v] - #print (val,v,self.calibration_tables[name][v]) - self.H.__sendInt__((CHAN.VREF << 15) | (CHAN.SwitchedOff << 13) | (0 << 12) | (val) ) - else: - self.H.__sendInt__((CHAN.VREF << 15) | (CHAN.SwitchedOff << 13) | (0 << 12) | v ) - - self.H.__get_ack__() - ''' - val = self.CHANS[name].apply_calibration(v) - self.I2C.writeBulk(self.addr, [64 | (CHAN.channum << 1), (val >> 8) & 0x0F, val & 0xFF]) - self.values[name] = CHAN.CodeToV(v) - return self.values[name] - - def __writeall__(self, v1, v2, v3, v4): - self.I2C.start(self.addr, 0) - self.I2C.send((v1 >> 8) & 0xF) - self.I2C.send(v1 & 0xFF) - self.I2C.send((v2 >> 8) & 0xF) - self.I2C.send(v2 & 0xFF) - self.I2C.send((v3 >> 8) & 0xF) - self.I2C.send(v3 & 0xFF) - self.I2C.send((v4 >> 8) & 0xF) - self.I2C.send(v4 & 0xFF) - self.I2C.stop() - - def stat(self): - self.I2C.start(self.addr, 0) - self.I2C.send(0x0) # read raw values starting from address - self.I2C.restart(self.addr, 1) - vals = self.I2C.read(24) - self.I2C.stop() - print(vals) - - -class NRF24L01(): - # Commands - R_REG = 0x00 - W_REG = 0x20 - RX_PAYLOAD = 0x61 - TX_PAYLOAD = 0xA0 - ACK_PAYLOAD = 0xA8 - FLUSH_TX = 0xE1 - FLUSH_RX = 0xE2 - ACTIVATE = 0x50 - R_STATUS = 0xFF - - # Registers - NRF_CONFIG = 0x00 - EN_AA = 0x01 - EN_RXADDR = 0x02 - SETUP_AW = 0x03 - SETUP_RETR = 0x04 - RF_CH = 0x05 - RF_SETUP = 0x06 - NRF_STATUS = 0x07 - OBSERVE_TX = 0x08 - CD = 0x09 - RX_ADDR_P0 = 0x0A - RX_ADDR_P1 = 0x0B - RX_ADDR_P2 = 0x0C - RX_ADDR_P3 = 0x0D - RX_ADDR_P4 = 0x0E - RX_ADDR_P5 = 0x0F - TX_ADDR = 0x10 - RX_PW_P0 = 0x11 - RX_PW_P1 = 0x12 - RX_PW_P2 = 0x13 - RX_PW_P3 = 0x14 - RX_PW_P4 = 0x15 - RX_PW_P5 = 0x16 - R_RX_PL_WID = 0x60 - FIFO_STATUS = 0x17 - DYNPD = 0x1C - FEATURE = 0x1D - PAYLOAD_SIZE = 0 - ACK_PAYLOAD_SIZE = 0 - READ_PAYLOAD_SIZE = 0 - - ADC_COMMANDS = 1 - READ_ADC = 0 << 4 - - I2C_COMMANDS = 2 - I2C_TRANSACTION = 0 << 4 - I2C_WRITE = 1 << 4 - I2C_SCAN = 2 << 4 - PULL_SCL_LOW = 3 << 4 - I2C_CONFIG = 4 << 4 - I2C_READ = 5 << 4 - - NRF_COMMANDS = 3 - NRF_READ_REGISTER = 0 - NRF_WRITE_REGISTER = 1 << 4 - - CURRENT_ADDRESS = 0xAAAA01 - nodelist = {} - nodepos = 0 - NODELIST_MAXLENGTH = 15 - connected = False - - def __init__(self, H): - self.H = H - self.ready = False - self.sigs = {self.CURRENT_ADDRESS: 1} - if self.H.connected: - self.connected = self.init() - - """ - routines for the NRFL01 radio - """ - - def init(self): - try: - self.H.__sendByte__(CP.NRFL01) - self.H.__sendByte__(CP.NRF_SETUP) - self.H.__get_ack__() - time.sleep(0.015) # 15 mS settling time - stat = self.get_status() - if stat & 0x80: - print("Radio transceiver not installed/not found") - return False - else: - self.ready = True - self.selectAddress(self.CURRENT_ADDRESS) - # self.write_register(self.RF_SETUP,0x06) - self.rxmode() - time.sleep(0.1) - self.flush() - return True - except Exception as ex: - self.raiseException(ex, "Communication Error , Function : " + inspect.currentframe().f_code.co_name) - - def rxmode(self): - ''' - Puts the radio into listening mode. - ''' - try: - self.H.__sendByte__(CP.NRFL01) - self.H.__sendByte__(CP.NRF_RXMODE) - self.H.__get_ack__() - except Exception as ex: - self.raiseException(ex, "Communication Error , Function : " + inspect.currentframe().f_code.co_name) - - def txmode(self): - ''' - Puts the radio into transmit mode. - ''' - try: - self.H.__sendByte__(CP.NRFL01) - self.H.__sendByte__(CP.NRF_TXMODE) - self.H.__get_ack__() - except Exception as ex: - self.raiseException(ex, "Communication Error , Function : " + inspect.currentframe().f_code.co_name) - - def triggerAll(self, val): - self.txmode() - self.selectAddress(0x111111) - self.write_register(self.EN_AA, 0x00) - self.write_payload([val], True) - self.write_register(self.EN_AA, 0x01) - - def power_down(self): - try: - self.H.__sendByte__(CP.NRFL01) - self.H.__sendByte__(CP.NRF_POWER_DOWN) - self.H.__get_ack__() - except Exception as ex: - self.raiseException(ex, "Communication Error , Function : " + inspect.currentframe().f_code.co_name) - - def rxchar(self): - ''' - Receives a 1 Byte payload - ''' - try: - self.H.__sendByte__(CP.NRFL01) - self.H.__sendByte__(CP.NRF_RXCHAR) - value = self.H.__getByte__() - self.H.__get_ack__() - return value - except Exception as ex: - self.raiseException(ex, "Communication Error , Function : " + inspect.currentframe().f_code.co_name) - - - def txchar(self, char): - ''' - Transmits a single character - ''' - try: - self.H.__sendByte__(CP.NRFL01) - self.H.__sendByte__(CP.NRF_TXCHAR) - self.H.__sendByte__(char) - return self.H.__get_ack__() >> 4 - except Exception as ex: - self.raiseException(ex, "Communication Error , Function : " + inspect.currentframe().f_code.co_name) - - def hasData(self): - ''' - Check if the RX FIFO contains data - ''' - try: - self.H.__sendByte__(CP.NRFL01) - self.H.__sendByte__(CP.NRF_HASDATA) - value = self.H.__getByte__() - self.H.__get_ack__() - return value - except Exception as ex: - self.raiseException(ex, "Communication Error , Function : " + inspect.currentframe().f_code.co_name) - - - def flush(self): - ''' - Flushes the TX and RX FIFOs - ''' - try: - self.H.__sendByte__(CP.NRFL01) - self.H.__sendByte__(CP.NRF_FLUSH) - self.H.__get_ack__() - except Exception as ex: - self.raiseException(ex, "Communication Error , Function : " + inspect.currentframe().f_code.co_name) - - def write_register(self, address, value): - ''' - write a byte to any of the configuration registers on the Radio. - address byte can either be located in the NRF24L01+ manual, or chosen - from some of the constants defined in this module. - ''' - # print ('writing',address,value) - try: - self.H.__sendByte__(CP.NRFL01) - self.H.__sendByte__(CP.NRF_WRITEREG) - self.H.__sendByte__(address) - self.H.__sendByte__(value) - self.H.__get_ack__() - except Exception as ex: - self.raiseException(ex, "Communication Error , Function : " + inspect.currentframe().f_code.co_name) - - def read_register(self, address): - ''' - Read the value of any of the configuration registers on the radio module. - - ''' - try: - self.H.__sendByte__(CP.NRFL01) - self.H.__sendByte__(CP.NRF_READREG) - self.H.__sendByte__(address) - val = self.H.__getByte__() - self.H.__get_ack__() - return val - except Exception as ex: - self.raiseException(ex, "Communication Error , Function : " + inspect.currentframe().f_code.co_name) - - - def get_status(self): - ''' - Returns a byte representing the STATUS register on the radio. - Refer to NRF24L01+ documentation for further details - ''' - try: - self.H.__sendByte__(CP.NRFL01) - self.H.__sendByte__(CP.NRF_GETSTATUS) - val = self.H.__getByte__() - self.H.__get_ack__() - return val - except Exception as ex: - self.raiseException(ex, "Communication Error , Function : " + inspect.currentframe().f_code.co_name) - - def write_command(self, cmd): - try: - self.H.__sendByte__(CP.NRFL01) - self.H.__sendByte__(CP.NRF_WRITECOMMAND) - self.H.__sendByte__(cmd) - self.H.__get_ack__() - except Exception as ex: - self.raiseException(ex, "Communication Error , Function : " + inspect.currentframe().f_code.co_name) - - def write_address(self, register, address): - ''' - register can be TX_ADDR, RX_ADDR_P0 -> RX_ADDR_P5 - 3 byte address. eg 0xFFABXX . XX cannot be FF - if RX_ADDR_P1 needs to be used along with any of the pipes - from P2 to P5, then RX_ADDR_P1 must be updated last. - Addresses from P1-P5 must share the first two bytes. - ''' - try: - self.H.__sendByte__(CP.NRFL01) - self.H.__sendByte__(CP.NRF_WRITEADDRESS) - self.H.__sendByte__(register) - self.H.__sendByte__(address & 0xFF) - self.H.__sendByte__((address >> 8) & 0xFF) - self.H.__sendByte__((address >> 16) & 0xFF) - self.H.__get_ack__() - except Exception as ex: - self.raiseException(ex, "Communication Error , Function : " + inspect.currentframe().f_code.co_name) - - def selectAddress(self, address): - ''' - Sets RX_ADDR_P0 and TX_ADDR to the specified address. - - ''' - try: - self.H.__sendByte__(CP.NRFL01) - self.H.__sendByte__(CP.NRF_WRITEADDRESSES) - self.H.__sendByte__(address & 0xFF) - self.H.__sendByte__((address >> 8) & 0xFF) - self.H.__sendByte__((address >> 16) & 0xFF) - self.H.__get_ack__() - self.CURRENT_ADDRESS = address - if address not in self.sigs: - self.sigs[address] = 1 - except Exception as ex: - self.raiseException(ex, "Communication Error , Function : " + inspect.currentframe().f_code.co_name) - - def read_payload(self, numbytes): - try: - self.H.__sendByte__(CP.NRFL01) - self.H.__sendByte__(CP.NRF_READPAYLOAD) - self.H.__sendByte__(numbytes) - data = self.H.fd.read(numbytes) - self.H.__get_ack__() - return [ord(a) for a in data] - except Exception as ex: - self.raiseException(ex, "Communication Error , Function : " + inspect.currentframe().f_code.co_name) - - def write_payload(self, data, verbose=False, **args): - try: - self.H.__sendByte__(CP.NRFL01) - self.H.__sendByte__(CP.NRF_WRITEPAYLOAD) - numbytes = len( - data) | 0x80 # 0x80 implies transmit immediately. Otherwise it will simply load the TX FIFO ( used by ACK_payload) - if (args.get('rxmode', False)): numbytes |= 0x40 - self.H.__sendByte__(numbytes) - self.H.__sendByte__(self.TX_PAYLOAD) - for a in data: - self.H.__sendByte__(a) - val = self.H.__get_ack__() >> 4 - if (verbose): - if val & 0x2: - print(' NRF radio not found. Connect one to the add-on port') - elif val & 0x1: - print(' Node probably dead/out of range. It failed to acknowledge') - return - return val - except Exception as ex: - self.raiseException(ex, "Communication Error , Function : " + inspect.currentframe().f_code.co_name) - - def I2C_scan(self): - ''' - Scans the I2C bus and returns a list of live addresses - ''' - x = self.transaction([self.I2C_COMMANDS | self.I2C_SCAN | 0x80], timeout=500) - if not x: return [] - if not sum(x): return [] - addrs = [] - for a in range(16): - if (x[a] ^ 255): - for b in range(8): - if x[a] & (0x80 >> b) == 0: - addr = 8 * a + b - addrs.append(addr) - return addrs - - def GuessingScan(self): - ''' - Scans the I2C bus and also prints the possible devices associated with each found address - ''' - from PSL import sensorlist - print('Scanning addresses 0-127...') - x = self.transaction([self.I2C_COMMANDS | self.I2C_SCAN | 0x80], timeout=500) - if not x: return [] - if not sum(x): return [] - addrs = [] - print('Address', '\t', 'Possible Devices') - - for a in range(16): - if (x[a] ^ 255): - for b in range(8): - if x[a] & (0x80 >> b) == 0: - addr = 8 * a + b - addrs.append(addr) - print(hex(addr), '\t\t', sensorlist.sensors.get(addr, 'None')) - - return addrs - - def transaction(self, data, **args): - st = time.time() - try: - self.H.__sendByte__(CP.NRFL01) - self.H.__sendByte__(CP.NRF_TRANSACTION) - self.H.__sendByte__(len(data)) # total Data bytes coming through - if 'listen' not in args: args['listen'] = True - if args.get('listen', False): data[0] |= 0x80 # You need this if hardware must wait for a reply - timeout = args.get('timeout', 200) - verbose = args.get('verbose', False) - self.H.__sendInt__(timeout) # timeout. - for a in data: - self.H.__sendByte__(a) - - # print ('dt send',time.time()-st,timeout,data[0]&0x80,data) - numbytes = self.H.__getByte__() - # print ('byte 1 in',time.time()-st) - if numbytes: - data = self.H.fd.read(numbytes) - else: - data = [] - val = self.H.__get_ack__() >> 4 - if (verbose): - if val & 0x1: print(time.time(), '%s Err. Node not found' % (hex(self.CURRENT_ADDRESS))) - if val & 0x2: print(time.time(), - '%s Err. NRF on-board transmitter not found' % (hex(self.CURRENT_ADDRESS))) - if val & 0x4 and args['listen']: print(time.time(), - '%s Err. Node received command but did not reply' % ( - hex(self.CURRENT_ADDRESS))) - if val & 0x7: # Something didn't go right. - self.flush() - self.sigs[self.CURRENT_ADDRESS] = self.sigs[self.CURRENT_ADDRESS] * 50 / 51. - return False - - self.sigs[self.CURRENT_ADDRESS] = (self.sigs[self.CURRENT_ADDRESS] * 50 + 1) / 51. - return [ord(a) for a in data] - except Exception as ex: - self.raiseException(ex, "Communication Error , Function : " + inspect.currentframe().f_code.co_name) - - def transactionWithRetries(self, data, **args): - retries = args.get('retries', 5) - reply = False - while retries > 0: - reply = self.transaction(data, verbose=(retries == 1), **args) - if reply: - break - retries -= 1 - return reply - - def write_ack_payload(self, data, pipe): - if (len(data) != self.ACK_PAYLOAD_SIZE): - self.ACK_PAYLOAD_SIZE = len(data) - if self.ACK_PAYLOAD_SIZE > 15: - print('too large. truncating.') - self.ACK_PAYLOAD_SIZE = 15 - data = data[:15] - else: - print('ack payload size:', self.ACK_PAYLOAD_SIZE) - try: - self.H.__sendByte__(CP.NRFL01) - self.H.__sendByte__(CP.NRF_WRITEPAYLOAD) - self.H.__sendByte__(len(data)) - self.H.__sendByte__(self.ACK_PAYLOAD | pipe) - for a in data: - self.H.__sendByte__(a) - return self.H.__get_ack__() >> 4 - except Exception as ex: - self.raiseException(ex, "Communication Error , Function : " + inspect.currentframe().f_code.co_name) - - def start_token_manager(self): - ''' - ''' - try: - self.H.__sendByte__(CP.NRFL01) - self.H.__sendByte__(CP.NRF_START_TOKEN_MANAGER) - self.H.__get_ack__() - except Exception as ex: - self.raiseException(ex, "Communication Error , Function : " + inspect.currentframe().f_code.co_name) - - def stop_token_manager(self): - ''' - ''' - try: - self.H.__sendByte__(CP.NRFL01) - self.H.__sendByte__(CP.NRF_STOP_TOKEN_MANAGER) - self.H.__get_ack__() - except Exception as ex: - self.raiseException(ex, "Communication Error , Function : " + inspect.currentframe().f_code.co_name) - - def total_tokens(self): - ''' - ''' - try: - self.H.__sendByte__(CP.NRFL01) - self.H.__sendByte__(CP.NRF_TOTAL_TOKENS) - x = self.H.__getByte__() - self.H.__get_ack__() - return x - except Exception as ex: - self.raiseException(ex, "Communication Error , Function : " + inspect.currentframe().f_code.co_name) - - def fetch_report(self, num): - ''' - ''' - try: - self.H.__sendByte__(CP.NRFL01) - self.H.__sendByte__(CP.NRF_REPORTS) - self.H.__sendByte__(num) - data = [self.H.__getByte__() for a in range(20)] - self.H.__get_ack__() - return data - except Exception as ex: - self.raiseException(ex, "Communication Error , Function : " + inspect.currentframe().f_code.co_name) - - def __decode_I2C_list__(self, data): - lst = [] - if sum(data) == 0: - return lst - for a in range(len(data)): - if (data[a] ^ 255): - for b in range(8): - if data[a] & (0x80 >> b) == 0: - addr = 8 * a + b - lst.append(addr) - return lst - - def get_nodelist(self): - ''' - Refer to the variable 'nodelist' if you simply want a list of nodes that either registered while your code was - running , or were loaded from the firmware buffer(max 15 entries) - - If you plan to use more than 15 nodes, and wish to register their addresses without having to feed them manually, - then this function must be called each time before the buffer resets. - - The dictionary object returned by this function [addresses paired with arrays containing their registered sensors] - is filtered by checking with each node if they are alive. - - ''' - - total = self.total_tokens() - if self.nodepos != total: - for nm in range(self.NODELIST_MAXLENGTH): - dat = self.fetch_report(nm) - txrx = (dat[0]) | (dat[1] << 8) | (dat[2] << 16) - if not txrx: continue - self.nodelist[txrx] = self.__decode_I2C_list__(dat[3:19]) - self.nodepos = total - # else: - # self.__delete_registered_node__(nm) - - filtered_lst = {} - for a in self.nodelist: - if self.isAlive(a): filtered_lst[a] = self.nodelist[a] - - return filtered_lst - - def __delete_registered_node__(self, num): - try: - self.H.__sendByte__(CP.NRFL01) - self.H.__sendByte__(CP.NRF_DELETE_REPORT_ROW) - self.H.__sendByte__(num) - self.H.__get_ack__() - except Exception as ex: - self.raiseException(ex, "Communication Error , Function : " + inspect.currentframe().f_code.co_name) - - def __delete_all_registered_nodes__(self): - while self.total_tokens(): - print('-') - self.__delete_registered_node__(0) - - def isAlive(self, addr): - self.selectAddress(addr) - return self.transaction([self.NRF_COMMANDS | self.NRF_READ_REGISTER] + [self.R_STATUS], timeout=100, - verbose=False) - - def init_shockburst_transmitter(self, **args): - ''' - Puts the radio into transmit mode. - Dynamic Payload with auto acknowledge is enabled. - upto 5 retransmits with 1ms delay between each in case a node doesn't respond in time - Receivers must acknowledge payloads - ''' - self.PAYLOAD_SIZE = args.get('PAYLOAD_SIZE', self.PAYLOAD_SIZE) - myaddr = args.get('myaddr', 0xAAAA01) - sendaddr = args.get('sendaddr', 0xAAAA01) - - self.init() - # shockburst - self.write_address(self.RX_ADDR_P0, myaddr) # transmitter's address - self.write_address(self.TX_ADDR, sendaddr) # send to node with this address - self.write_register(self.RX_PW_P0, self.PAYLOAD_SIZE) - self.rxmode() - time.sleep(0.1) - self.flush() - - def init_shockburst_receiver(self, **args): - ''' - Puts the radio into receive mode. - Dynamic Payload with auto acknowledge is enabled. - ''' - self.PAYLOAD_SIZE = args.get('PAYLOAD_SIZE', self.PAYLOAD_SIZE) - if 'myaddr0' not in args: - args['myaddr0'] = 0xA523B5 - # if 'sendaddr' non in args: - # args['sendaddr']=0xA523B5 - print(args) - self.init() - self.write_register(self.RF_SETUP, 0x26) # 2MBPS speed - - # self.write_address(self.TX_ADDR,sendaddr) #send to node with this address - # self.write_address(self.RX_ADDR_P0,myaddr) #will receive the ACK Payload from that node - enabled_pipes = 0 # pipes to be enabled - for a in range(0, 6): - x = args.get('myaddr' + str(a), None) - if x: - print(hex(x), hex(self.RX_ADDR_P0 + a)) - enabled_pipes |= (1 << a) - self.write_address(self.RX_ADDR_P0 + a, x) - P15_base_address = args.get('myaddr1', None) - if P15_base_address: self.write_address(self.RX_ADDR_P1, P15_base_address) - - self.write_register(self.EN_RXADDR, enabled_pipes) # enable pipes - self.write_register(self.EN_AA, enabled_pipes) # enable auto Acknowledge on all pipes - self.write_register(self.DYNPD, enabled_pipes) # enable dynamic payload on Data pipes - self.write_register(self.FEATURE, 0x06) # enable dynamic payload length - # self.write_register(self.RX_PW_P0,self.PAYLOAD_SIZE) - - self.rxmode() - time.sleep(0.1) - self.flush() - - -class RadioLink(): - ADC_COMMANDS = 1 - READ_ADC = 0 << 4 - - I2C_COMMANDS = 2 - I2C_TRANSACTION = 0 << 4 - I2C_WRITE = 1 << 4 - SCAN_I2C = 2 << 4 - PULL_SCL_LOW = 3 << 4 - I2C_CONFIG = 4 << 4 - I2C_READ = 5 << 4 - - NRF_COMMANDS = 3 - NRF_READ_REGISTER = 0 << 4 - NRF_WRITE_REGISTER = 1 << 4 - - MISC_COMMANDS = 4 - WS2812B_CMD = 0 << 4 - - def __init__(self, NRF, **args): - self.NRF = NRF - if 'address' in args: - self.ADDRESS = args.get('address', False) - else: - print('Address not specified. Add "address=0x....." argument while instantiating') - self.ADDRESS = 0x010101 - - def __selectMe__(self): - if self.NRF.CURRENT_ADDRESS != self.ADDRESS: - self.NRF.selectAddress(self.ADDRESS) - - def I2C_scan(self): - self.__selectMe__() - from PSL import sensorlist - print('Scanning addresses 0-127...') - x = self.NRF.transaction([self.I2C_COMMANDS | self.SCAN_I2C | 0x80], timeout=500) - if not x: return [] - if not sum(x): return [] - addrs = [] - print('Address', '\t', 'Possible Devices') - - for a in range(16): - if (x[a] ^ 255): - for b in range(8): - if x[a] & (0x80 >> b) == 0: - addr = 8 * a + b - addrs.append(addr) - print(hex(addr), '\t\t', sensorlist.sensors.get(addr, 'None')) - - return addrs - - def __decode_I2C_list__(self, data): - lst = [] - if sum(data) == 0: - return lst - for a in range(len(data)): - if (data[a] ^ 255): - for b in range(8): - if data[a] & (0x80 >> b) == 0: - addr = 8 * a + b - lst.append(addr) - return lst - - def writeI2C(self, I2C_addr, regaddress, bytes): - self.__selectMe__() - return self.NRF.transaction([self.I2C_COMMANDS | self.I2C_WRITE] + [I2C_addr] + [regaddress] + bytes) - - def readI2C(self, I2C_addr, regaddress, numbytes): - self.__selectMe__() - return self.NRF.transaction([self.I2C_COMMANDS | self.I2C_TRANSACTION] + [I2C_addr] + [regaddress] + [numbytes]) - - def writeBulk(self, I2C_addr, bytes): - self.__selectMe__() - return self.NRF.transaction([self.I2C_COMMANDS | self.I2C_WRITE] + [I2C_addr] + bytes) - - def readBulk(self, I2C_addr, regaddress, numbytes): - self.__selectMe__() - return self.NRF.transactionWithRetries( - [self.I2C_COMMANDS | self.I2C_TRANSACTION] + [I2C_addr] + [regaddress] + [numbytes]) - - def simpleRead(self, I2C_addr, numbytes): - self.__selectMe__() - return self.NRF.transactionWithRetries([self.I2C_COMMANDS | self.I2C_READ] + [I2C_addr] + [numbytes]) - - def readADC(self, channel): - self.__selectMe__() - return self.NRF.transaction([self.ADC_COMMANDS | self.READ_ADC] + [channel]) - - def pullSCLLow(self, t_ms): - self.__selectMe__() - dat = self.NRF.transaction([self.I2C_COMMANDS | self.PULL_SCL_LOW] + [t_ms]) - if dat: - return self.__decode_I2C_list__(dat) - else: - return [] - - def configI2C(self, freq): - self.__selectMe__() - brgval = int(32e6 / freq / 4 - 1) - print(brgval) - return self.NRF.transaction([self.I2C_COMMANDS | self.I2C_CONFIG] + [brgval], listen=False) - - def write_register(self, reg, val): - self.__selectMe__() - # print ('writing to ',reg,val) - return self.NRF.transaction([self.NRF_COMMANDS | self.NRF_WRITE_REGISTER] + [reg, val], listen=False) - - def WS2812B(self, cols): - """ - set shade of WS2182 LED on CS1/RC0 - - .. tabularcolumns:: |p{3cm}|p{11cm}| - - ============== ============================================================================================ - **Arguments** - ============== ============================================================================================ - cols 2Darray [[R,G,B],[R2,G2,B2],[R3,G3,B3]...] - brightness of R,G,B ( 0-255 ) - ============== ============================================================================================ - - example:: - - >>> WS2812B([[10,0,0],[0,10,10],[10,0,10]]) - #sets red, cyan, magenta to three daisy chained LEDs - - """ - self.__selectMe__() - colarray = [] - for a in cols: - colarray.append(int('{:08b}'.format(int(a[1]))[::-1], 2)) - colarray.append(int('{:08b}'.format(int(a[0]))[::-1], 2)) - colarray.append(int('{:08b}'.format(int(a[2]))[::-1], 2)) - - res = self.NRF.transaction([self.MISC_COMMANDS | self.WS2812B_CMD] + colarray, listen=False) - return res - - def read_register(self, reg): - self.__selectMe__() - x = self.NRF.transaction([self.NRF_COMMANDS | self.NRF_READ_REGISTER] + [reg]) - if x: - return x[0] - else: - return False diff --git a/PSL/SENSORS/ADS1115.py b/PSL/SENSORS/ADS1115.py deleted file mode 100644 index 82216476..00000000 --- a/PSL/SENSORS/ADS1115.py +++ /dev/null @@ -1,194 +0,0 @@ -# -*- coding: utf-8; mode: python; indent-tabs-mode: t; tab-width:4 -*- -from __future__ import print_function -from numpy import int16 -import time -try: - from collections import OrderedDict -except ImportError: - # fallback: try to use the ordereddict backport when using python 2.6 - from ordereddict import OrderedDict - - -def connect(route,**args): - return ADS1115(route,**args) - -class ADS1115: - ADDRESS = 0x48 #addr pin grounded. floating - - REG_POINTER_MASK = 0x3 - REG_POINTER_CONVERT = 0 - REG_POINTER_CONFIG = 1 - REG_POINTER_LOWTHRESH=2 - REG_POINTER_HITHRESH =3 - - REG_CONFIG_OS_MASK =0x8000 - REG_CONFIG_OS_SINGLE =0x8000 - REG_CONFIG_OS_BUSY =0x0000 - REG_CONFIG_OS_NOTBUSY =0x8000 - - REG_CONFIG_MUX_MASK =0x7000 - REG_CONFIG_MUX_DIFF_0_1 =0x0000 # Differential P = AIN0, N = AIN1 =default) - REG_CONFIG_MUX_DIFF_0_3 =0x1000 # Differential P = AIN0, N = AIN3 - REG_CONFIG_MUX_DIFF_1_3 =0x2000 # Differential P = AIN1, N = AIN3 - REG_CONFIG_MUX_DIFF_2_3 =0x3000 # Differential P = AIN2, N = AIN3 - REG_CONFIG_MUX_SINGLE_0 =0x4000 # Single-ended AIN0 - REG_CONFIG_MUX_SINGLE_1 =0x5000 # Single-ended AIN1 - REG_CONFIG_MUX_SINGLE_2 =0x6000 # Single-ended AIN2 - REG_CONFIG_MUX_SINGLE_3 =0x7000 # Single-ended AIN3 - - REG_CONFIG_PGA_MASK =0x0E00 #bits 11:9 - REG_CONFIG_PGA_6_144V =(0<<9) # +/-6.144V range = Gain 2/3 - REG_CONFIG_PGA_4_096V =(1<<9) # +/-4.096V range = Gain 1 - REG_CONFIG_PGA_2_048V =(2<<9) # +/-2.048V range = Gain 2 =default) - REG_CONFIG_PGA_1_024V =(3<<9) # +/-1.024V range = Gain 4 - REG_CONFIG_PGA_0_512V =(4<<9) # +/-0.512V range = Gain 8 - REG_CONFIG_PGA_0_256V =(5<<9) # +/-0.256V range = Gain 16 - - REG_CONFIG_MODE_MASK =0x0100 #bit 8 - REG_CONFIG_MODE_CONTIN =(0<<8) # Continuous conversion mode - REG_CONFIG_MODE_SINGLE =(1<<8) # Power-down single-shot mode =default) - - REG_CONFIG_DR_MASK =0x00E0 - REG_CONFIG_DR_8SPS =(0<<5) #8 SPS - REG_CONFIG_DR_16SPS =(1<<5) #16 SPS - REG_CONFIG_DR_32SPS =(2<<5) #32 SPS - REG_CONFIG_DR_64SPS =(3<<5) #64 SPS - REG_CONFIG_DR_128SPS =(4<<5) #128 SPS - REG_CONFIG_DR_250SPS =(5<<5) #260 SPS - REG_CONFIG_DR_475SPS =(6<<5) #475 SPS - REG_CONFIG_DR_860SPS =(7<<5) #860 SPS - - REG_CONFIG_CMODE_MASK =0x0010 - REG_CONFIG_CMODE_TRAD =0x0000 - REG_CONFIG_CMODE_WINDOW =0x0010 - - REG_CONFIG_CPOL_MASK =0x0008 - REG_CONFIG_CPOL_ACTVLOW =0x0000 - REG_CONFIG_CPOL_ACTVHI =0x0008 - - REG_CONFIG_CLAT_MASK =0x0004 - REG_CONFIG_CLAT_NONLAT =0x0000 - REG_CONFIG_CLAT_LATCH =0x0004 - - REG_CONFIG_CQUE_MASK =0x0003 - REG_CONFIG_CQUE_1CONV =0x0000 - REG_CONFIG_CQUE_2CONV =0x0001 - REG_CONFIG_CQUE_4CONV =0x0002 - REG_CONFIG_CQUE_NONE =0x0003 - gains = OrderedDict([('GAIN_TWOTHIRDS',REG_CONFIG_PGA_6_144V),('GAIN_ONE',REG_CONFIG_PGA_4_096V),('GAIN_TWO',REG_CONFIG_PGA_2_048V),('GAIN_FOUR',REG_CONFIG_PGA_1_024V),('GAIN_EIGHT',REG_CONFIG_PGA_0_512V),('GAIN_SIXTEEN',REG_CONFIG_PGA_0_256V)]) - gain_scaling = OrderedDict([('GAIN_TWOTHIRDS',0.1875),('GAIN_ONE',0.125),('GAIN_TWO',0.0625),('GAIN_FOUR',0.03125),('GAIN_EIGHT',0.015625),('GAIN_SIXTEEN',0.0078125)]) - type_selection = OrderedDict([('UNI_0',0),('UNI_1',1),('UNI_2',2),('UNI_3',3),('DIFF_01','01'),('DIFF_23','23')]) - sdr_selection = OrderedDict([(8,REG_CONFIG_DR_8SPS),(16,REG_CONFIG_DR_16SPS),(32,REG_CONFIG_DR_32SPS),(64,REG_CONFIG_DR_64SPS),(128,REG_CONFIG_DR_128SPS),(250,REG_CONFIG_DR_250SPS),(475,REG_CONFIG_DR_475SPS),(860,REG_CONFIG_DR_860SPS)]) #sampling data rate - - NUMPLOTS=1 - PLOTNAMES = ['mV'] - def __init__(self,I2C,**args): - self.ADDRESS = args.get('address',self.ADDRESS) - self.I2C = I2C - self.channel = 'UNI_0' - self.gain = 'GAIN_ONE' - self.rate = 128 - - self.setGain('GAIN_ONE') - self.setChannel('UNI_0') - self.setDataRate(128) - self.conversionDelay = 8 - self.name = 'ADS1115 16-bit ADC' - self.params={'setGain':self.gains.keys(),'setChannel':self.type_selection.keys(),'setDataRate':self.sdr_selection.keys()} - - - - - def __readInt__(self,addr): - return int16(self.__readUInt__(addr)) - - def __readUInt__(self,addr): - vals = self.I2C.readBulk(self.ADDRESS,addr,2) - v=1.*((vals[0]<<8)|vals[1]) - return v - - def initTemperature(self): - self.I2C.writeBulk(self.ADDRESS,[self.REG_CONTROL,self.CMD_TEMP]) - time.sleep(0.005) - - def readRegister(self,register): - vals = self.I2C.readBulk(self.ADDRESS,register,2) - return (vals[0]<<8)|vals[1] - - def writeRegister(self,reg,value): - self.I2C.writeBulk(self.ADDRESS,[reg,(value>>8)&0xFF,value&0xFF]) - - def setGain(self,gain): - ''' - options : 'GAIN_TWOTHIRDS','GAIN_ONE','GAIN_TWO','GAIN_FOUR','GAIN_EIGHT','GAIN_SIXTEEN' - ''' - self.gain = gain - - def setChannel(self,channel): - ''' - options 'UNI_0','UNI_1','UNI_2','UNI_3','DIFF_01','DIFF_23' - ''' - self.channel = channel - - def setDataRate(self,rate): - ''' - data rate options 8,16,32,64,128,250,475,860 SPS - ''' - self.rate = rate - - - def readADC_SingleEnded(self,chan): - if chan>3:return None - #start with default values - config = (self.REG_CONFIG_CQUE_NONE # Disable the comparator (default val) - |self.REG_CONFIG_CLAT_NONLAT # Non-latching (default val) - |self.REG_CONFIG_CPOL_ACTVLOW #Alert/Rdy active low (default val) - |self.REG_CONFIG_CMODE_TRAD # Traditional comparator (default val) - |self.sdr_selection[self.rate] # 1600 samples per second (default) - |self.REG_CONFIG_MODE_SINGLE) # Single-shot mode (default) - - #Set PGA/voltage range - config |= self.gains[self.gain] - - if chan == 0 : config |= self.REG_CONFIG_MUX_SINGLE_0 - elif chan == 1 : config |= self.REG_CONFIG_MUX_SINGLE_1 - elif chan == 2 : config |= self.REG_CONFIG_MUX_SINGLE_2 - elif chan == 3 : config |= self.REG_CONFIG_MUX_SINGLE_3 - #Set 'start single-conversion' bit - config |= self.REG_CONFIG_OS_SINGLE - self.writeRegister(self.REG_POINTER_CONFIG, config); - time.sleep(1./self.rate+.002) #convert to mS to S - return self.readRegister(self.REG_POINTER_CONVERT)*self.gain_scaling[self.gain] - - def readADC_Differential(self,chan = '01'): - #start with default values - config = (self.REG_CONFIG_CQUE_NONE # Disable the comparator (default val) - |self.REG_CONFIG_CLAT_NONLAT # Non-latching (default val) - |self.REG_CONFIG_CPOL_ACTVLOW #Alert/Rdy active low (default val) - |self.REG_CONFIG_CMODE_TRAD # Traditional comparator (default val) - |self.sdr_selection[self.rate] # samples per second - |self.REG_CONFIG_MODE_SINGLE) # Single-shot mode (default) - - #Set PGA/voltage range - config |= self.gains[self.gain] - if chan == '01':config |= self.REG_CONFIG_MUX_DIFF_0_1 - elif chan == '23':config |= self.REG_CONFIG_MUX_DIFF_2_3 - #Set 'start single-conversion' bit - config |= self.REG_CONFIG_OS_SINGLE - self.writeRegister(self.REG_POINTER_CONFIG, config); - time.sleep(1./self.rate+.002) #convert to mS to S - return int16(self.readRegister(self.REG_POINTER_CONVERT))*self.gain_scaling[self.gain] - - def getLastResults(self): - return int16(self.readRegister(self.REG_POINTER_CONVERT))*self.gain_scaling[self.gain] - - def getRaw(self): - ''' - return values in mV - ''' - chan = self.type_selection[self.channel] - if self.channel[:3]=='UNI': - return [self.readADC_SingleEnded(chan)] - elif self.channel[:3]=='DIF': - return [self.readADC_Differential(chan)] - diff --git a/PSL/SENSORS/BMP180.py b/PSL/SENSORS/BMP180.py deleted file mode 100644 index a422f50f..00000000 --- a/PSL/SENSORS/BMP180.py +++ /dev/null @@ -1,116 +0,0 @@ -from __future__ import print_function -from numpy import int16 -import time - - -def connect(route, **args): - return BMP180(route, **args) - - -class BMP180: - ADDRESS = 0x77 - REG_CONTROL = 0xF4 - REG_RESULT = 0xF6 - CMD_TEMP = 0x2E - CMD_P0 = 0x34 - CMD_P1 = 0x74 - CMD_P2 = 0xB4 - CMD_P3 = 0xF4 - oversampling = 0 - NUMPLOTS = 3 - PLOTNAMES = ['Temperature', 'Pressure', 'Altitude'] - name = 'Altimeter BMP180' - - def __init__(self, I2C, **args): - self.ADDRESS = args.get('address', self.ADDRESS) - - self.I2C = I2C - - self.MB = self.__readInt__(0xBA) - - self.c3 = 160.0 * pow(2, -15) * self.__readInt__(0xAE) - self.c4 = pow(10, -3) * pow(2, -15) * self.__readUInt__(0xB0) - self.b1 = pow(160, 2) * pow(2, -30) * self.__readInt__(0xB6) - self.c5 = (pow(2, -15) / 160) * self.__readUInt__(0xB2) - self.c6 = self.__readUInt__(0xB4) - self.mc = (pow(2, 11) / pow(160, 2)) * self.__readInt__(0xBC) - self.md = self.__readInt__(0xBE) / 160.0 - self.x0 = self.__readInt__(0xAA) - self.x1 = 160.0 * pow(2, -13) * self.__readInt__(0xAC) - self.x2 = pow(160, 2) * pow(2, -25) * self.__readInt__(0xB8) - self.y0 = self.c4 * pow(2, 15) - self.y1 = self.c4 * self.c3 - self.y2 = self.c4 * self.b1 - self.p0 = (3791.0 - 8.0) / 1600.0 - self.p1 = 1.0 - 7357.0 * pow(2, -20) - self.p2 = 3038.0 * 100.0 * pow(2, -36) - self.T = 25 - print('calib:', self.c3, self.c4, self.b1, self.c5, self.c6, self.mc, self.md, self.x0, self.x1, self.x2, - self.y0, self.y1, self.p0, self.p1, self.p2) - self.params = {'setOversampling': [0, 1, 2, 3]} - self.name = 'BMP180 Altimeter' - self.initTemperature() - self.readTemperature() - self.initPressure() - self.baseline = self.readPressure() - - def __readInt__(self, addr): - return int16(self.__readUInt__(addr)) - - def __readUInt__(self, addr): - vals = self.I2C.readBulk(self.ADDRESS, addr, 2) - v = 1. * ((vals[0] << 8) | vals[1]) - return v - - def initTemperature(self): - self.I2C.writeBulk(self.ADDRESS, [self.REG_CONTROL, self.CMD_TEMP]) - time.sleep(0.005) - - def readTemperature(self): - vals = self.I2C.readBulk(self.ADDRESS, self.REG_RESULT, 2) - if len(vals) == 2: - T = (vals[0] << 8) + vals[1] - a = self.c5 * (T - self.c6) - self.T = a + (self.mc / (a + self.md)) - return self.T - else: - return False - - def setOversampling(self, num): - self.oversampling = num - - def initPressure(self): - os = [0x34, 0x74, 0xb4, 0xf4] - delays = [0.005, 0.008, 0.014, 0.026] - self.I2C.writeBulk(self.ADDRESS, [self.REG_CONTROL, os[self.oversampling]]) - time.sleep(delays[self.oversampling]) - - def readPressure(self): - vals = self.I2C.readBulk(self.ADDRESS, self.REG_RESULT, 3) - if len(vals) == 3: - P = 1. * (vals[0] << 8) + vals[1] + (vals[2] / 256.0) - s = self.T - 25.0 - x = (self.x2 * pow(s, 2)) + (self.x1 * s) + self.x0 - y = (self.y2 * pow(s, 2)) + (self.y1 * s) + self.y0 - z = (P - x) / y - self.P = (self.p2 * pow(z, 2)) + (self.p1 * z) + self.p0 - return self.P - else: - return False - - def altitude(self): - # baseline pressure needs to be provided - return (44330.0 * (1 - pow(self.P / self.baseline, 1 / 5.255))) - - def sealevel(self, P, A): - ''' - given a calculated pressure and altitude, return the sealevel - ''' - return (P / pow(1 - (A / 44330.0), 5.255)) - - def getRaw(self): - self.initTemperature() - self.readTemperature() - self.initPressure() - self.readPressure() - return [self.T, self.P, self.altitude()] diff --git a/PSL/SENSORS/MPU925x.py b/PSL/SENSORS/MPU925x.py deleted file mode 100644 index 7996a624..00000000 --- a/PSL/SENSORS/MPU925x.py +++ /dev/null @@ -1,189 +0,0 @@ -# -*- coding: utf-8; mode: python; indent-tabs-mode: t; tab-width:4 -*- -from numpy import int16,std -from Kalman import KalmanFilter - -def connect(route,**args): - return MPU925x(route,**args) - -class MPU925x(): - ''' - Mandatory members: - GetRaw : Function called by Graphical apps. Must return values stored in a list - NUMPLOTS : length of list returned by GetRaw. Even single datapoints need to be stored in a list before returning - PLOTNAMES : a list of strings describing each element in the list returned by GetRaw. len(PLOTNAMES) = NUMPLOTS - name : the name of the sensor shown to the user - params: - A dictionary of function calls(single arguments only) paired with list of valid argument values. (Primitive. I know.) - These calls can be used for one time configuration settings - - ''' - INT_PIN_CFG = 0x37 - GYRO_CONFIG = 0x1B - ACCEL_CONFIG = 0x1C - GYRO_SCALING= [131,65.5,32.8,16.4] - ACCEL_SCALING=[16384,8192,4096,2048] - AR=3 - GR=3 - NUMPLOTS=7 - PLOTNAMES = ['Ax','Ay','Az','Temp','Gx','Gy','Gz'] - ADDRESS = 0x68 - AK8963_ADDRESS =0x0C - AK8963_CNTL = 0x0A - name = 'Accel/gyro' - def __init__(self,I2C,**args): - self.I2C=I2C - self.ADDRESS = args.get('address',self.ADDRESS) - self.name = 'Accel/gyro' - self.params={'powerUp':None,'setGyroRange':[250,500,1000,2000],'setAccelRange':[2,4,8,16],'KalmanFilter':[.01,.1,1,10,100,1000,10000,'OFF']} - self.setGyroRange(2000) - self.setAccelRange(16) - self.powerUp() - self.K=None - - def KalmanFilter(self,opt): - if opt=='OFF': - self.K=None - return - noise=[[]]*self.NUMPLOTS - for a in range(500): - vals=self.getRaw() - for b in range(self.NUMPLOTS):noise[b].append(vals[b]) - - self.K=[None]*7 - for a in range(self.NUMPLOTS): - sd = std(noise[a]) - self.K[a] = KalmanFilter(1./opt, sd**2) - - def getVals(self,addr,numbytes): - return self.I2C.readBulk(self.ADDRESS,addr,numbytes) - - def powerUp(self): - self.I2C.writeBulk(self.ADDRESS,[0x6B,0]) - - def setGyroRange(self,rs): - self.GR=self.params['setGyroRange'].index(rs) - self.I2C.writeBulk(self.ADDRESS,[self.GYRO_CONFIG,self.GR<<3]) - - def setAccelRange(self,rs): - self.AR=self.params['setAccelRange'].index(rs) - self.I2C.writeBulk(self.ADDRESS,[self.ACCEL_CONFIG,self.AR<<3]) - - def getRaw(self): - ''' - This method must be defined if you want GUIs to use this class to generate plots on the fly. - It must return a set of different values read from the sensor. such as X,Y,Z acceleration. - The length of this list must not change, and must be defined in the variable NUMPLOTS. - - GUIs will generate as many plots, and the data returned from this method will be appended appropriately - ''' - vals=self.getVals(0x3B,14) - if vals: - if len(vals)==14: - raw=[0]*7 - for a in range(3):raw[a] = 1.*int16(vals[a*2]<<8|vals[a*2+1])/self.ACCEL_SCALING[self.AR] - for a in range(4,7):raw[a] = 1.*int16(vals[a*2]<<8|vals[a*2+1])/self.GYRO_SCALING[self.GR] - raw[3] = int16(vals[6]<<8|vals[7])/340. + 36.53 - if not self.K: - return raw - else: - for b in range(self.NUMPLOTS): - self.K[b].input_latest_noisy_measurement(raw[b]) - raw[b]=self.K[b].get_latest_estimated_measurement() - return raw - - else: - return False - else: - return False - - def getAccel(self): - ''' - Return a list of 3 values for acceleration vector - - ''' - vals=self.getVals(0x3B,6) - ax=int16(vals[0]<<8|vals[1]) - ay=int16(vals[2]<<8|vals[3]) - az=int16(vals[4]<<8|vals[5]) - return [ax/65535.,ay/65535.,az/65535.] - - def getTemp(self): - ''' - Return temperature - ''' - vals=self.getVals(0x41,6) - t=int16(vals[0]<<8|vals[1]) - return t/65535. - - def getGyro(self): - ''' - Return a list of 3 values for angular velocity vector - - ''' - vals=self.getVals(0x43,6) - ax=int16(vals[0]<<8|vals[1]) - ay=int16(vals[2]<<8|vals[3]) - az=int16(vals[4]<<8|vals[5]) - return [ax/65535.,ay/65535.,az/65535.] - - def getMag(self): - ''' - Return a list of 3 values for magnetic field vector - - ''' - vals=self.I2C.readBulk(self.AK8963_ADDRESS,0x03,7) #6+1 . 1(ST2) should not have bit 4 (0x8) true. It's ideally 16 . overflow bit - ax=int16(vals[0]<<8|vals[1]) - ay=int16(vals[2]<<8|vals[3]) - az=int16(vals[4]<<8|vals[5]) - if not vals[6]&0x08:return [ax/65535.,ay/65535.,az/65535.] - else: return None - - - def WhoAmI(self): - ''' - Returns the ID. - It is 71 for MPU9250. - ''' - v = self.I2C.readBulk(self.ADDRESS,0x75,1)[0] - if v not in [0x71,0x73]:return 'Error %s'%hex(v) - - - if v==0x73:return 'MPU9255 %s'%hex(v) - elif v==0x71:return 'MPU9250 %s'%hex(v) - - - def WhoAmI_AK8963(self): - ''' - Returns the ID fo magnetometer AK8963 if found. - It should be 0x48. - ''' - self.initMagnetometer() - v= self.I2C.readBulk(self.AK8963_ADDRESS,0,1) [0] - if v==0x48:return 'AK8963 at %s'%hex(v) - else: return 'AK8963 not found. returned :%s'%hex(v) - - def initMagnetometer(self): - ''' - For MPU925x with integrated magnetometer. - It's called a 10 DoF sensor, but technically speaking , - the 3-axis Accel , 3-Axis Gyro, temperature sensor are integrated in one IC, and the 3-axis magnetometer is implemented in a - separate IC which can be accessed via an I2C passthrough. - Therefore , in order to detect the magnetometer via an I2C scan, the passthrough must first be enabled on IC#1 (Accel,gyro,temp) - ''' - self.I2C.writeBulk(self.ADDRESS,[self.INT_PIN_CFG,0x22]) #I2C passthrough - self.I2C.writeBulk(self.AK8963_ADDRESS,[self.AK8963_CNTL,0]) #power down mag - self.I2C.writeBulk(self.AK8963_ADDRESS,[self.AK8963_CNTL,(1<<4)|6]) #mode (0=14bits,1=16bits) <<4 | (2=8Hz , 6=100Hz) - - - -if __name__ == "__main__": - from PSL import sciencelab - I = sciencelab.connect() - A = connect(I.I2C) - t,x,y,z = I.I2C.capture(A.ADDRESS,0x43,6,5000,1000,'int') - #print (t,x,y,z) - from pylab import * - plot(t,x) - plot(t,y) - plot(t,z) - show() diff --git a/PSL/SENSORS/SSD1306.py b/PSL/SENSORS/SSD1306.py deleted file mode 100644 index 9c40ff5e..00000000 --- a/PSL/SENSORS/SSD1306.py +++ /dev/null @@ -1,500 +0,0 @@ -''' -Adapted into Python from Adafruit's oled.cpp -Original license text: - -Software License Agreement (BSD License) - -Copyright (c) 2012, Adafruit Industries -All rights reserved. - -Redistribution and use in source and binary forms, with or without -modification, are permitted provided that the following conditions are met: -1. Redistributions of source code must retain the above copyright -notice, this list of conditions and the following disclaimer. -2. Redistributions in binary form must reproduce the above copyright -notice, this list of conditions and the following disclaimer in the -documentation and/or other materials provided with the distribution. -3. Neither the name of the copyright holders nor the -names of its contributors may be used to endorse or promote products -derived from this software without specific prior written permission. - -THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS ''AS IS'' AND ANY -EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED -WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE -DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER BE LIABLE FOR ANY -DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES -(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; -LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND -ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT -(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS -SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - -''' -from __future__ import print_function -from numpy import int16 -import time - - -def connect(route, **args): - return SSD1306(route, **args) - - -class SSD1306(): - ADDRESS = 0x3C - - # --------------Parameters-------------------- - # This must be defined in order to let GUIs automatically create menus - # for changing various options of this sensor - # It's a dictionary of the string representations of functions matched with an array - # of options that each one can accept - params = {'load': ['logo'], - 'scroll': ['left', 'right', 'topright', 'topleft', 'bottomleft', 'bottomright', 'stop'] - } - - NUMPLOTS = 0 - PLOTNAMES = [''] - name = 'OLED Display' - _width = 128 - WIDTH = 128 - _height = 64 - HEIGHT = 64 - - rotation = 0 - cursor_y = 0 - cursor_x = 0 - textsize = 1 - textcolor = 1 - textbgcolor = 0 - wrap = True - - SSD1306_128_64 = 1 - SSD1306_128_32 = 2 - SSD1306_96_16 = 3 - - # -----------------------------------------------------------------------*/ - DISPLAY_TYPE = SSD1306_96_16 - ## self.SSD1306_128_32 - # /*=========================================================================*/ - - SSD1306_LCDWIDTH = 128 - SSD1306_LCDHEIGHT = 64 - - SSD1306_SETCONTRAST = 0x81 - SSD1306_DISPLAYALLON_RESUME = 0xA4 - SSD1306_DISPLAYALLON = 0xA5 - SSD1306_NORMALDISPLAY = 0xA6 - SSD1306_INVERTDISPLAY = 0xA7 - SSD1306_DISPLAYOFF = 0xAE - SSD1306_DISPLAYON = 0xAF - - SSD1306_SETDISPLAYOFFSET = 0xD3 - SSD1306_SETCOMPINS = 0xDA - - SSD1306_SETVCOMDETECT = 0xDB - - SSD1306_SETDISPLAYCLOCKDIV = 0xD5 - SSD1306_SETPRECHARGE = 0xD9 - - SSD1306_SETMULTIPLEX = 0xA8 - - SSD1306_SETLOWCOLUMN = 0x00 - SSD1306_SETHIGHCOLUMN = 0x10 - - SSD1306_SETSTARTLINE = 0x40 - - SSD1306_MEMORYMODE = 0x20 - - SSD1306_COMSCANINC = 0xC0 - SSD1306_COMSCANDEC = 0xC8 - - SSD1306_SEGREMAP = 0xA0 - - SSD1306_CHARGEPUMP = 0x8D - - SSD1306_EXTERNALVCC = 0x1 - SSD1306_SWITCHCAPVCC = 0x2 - - logobuff = [255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 127, 127, 63, 63, 159, 159, - 223, 223, 207, 207, 207, 239, 239, 47, 47, 39, 39, 7, 7, 67, 67, 83, 131, 135, 7, 7, 15, 15, 31, 191, - 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, - 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, - 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, - 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, - 255, 255, 255, 255, 255, 255, 63, 31, 15, 199, 99, 17, 25, 12, 4, 2, 3, 7, 63, 255, 255, 255, 255, 255, - 255, 255, 255, 254, 252, 240, 224, 224, 224, 192, 192, 128, 128, 128, 128, 129, 128, 0, 0, 0, 0, 0, 3, - 3, 7, 31, 127, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, - 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, - 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, - 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 127, 15, - 3, 192, 120, 134, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, - 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 254, 254, 254, 252, 252, - 249, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, - 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, - 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, - 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 143, 0, 0, 124, 199, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 128, 240, 252, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, - 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, - 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, - 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, - 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, - 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 240, 128, 0, 7, 56, 96, 128, 0, 0, 0, - 0, 0, 0, 0, 12, 63, 255, 127, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, - 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 31, 7, 227, 243, 249, 249, 249, - 249, 249, 249, 243, 255, 255, 199, 131, 49, 57, 57, 57, 121, 115, 255, 255, 255, 255, 15, 15, 159, 207, - 207, 207, 143, 31, 63, 255, 255, 159, 207, 207, 207, 143, 31, 63, 255, 255, 255, 15, 15, 159, 207, 207, - 207, 255, 255, 0, 0, 255, 127, 63, 159, 207, 239, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, - 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 254, 248, 240, 224, 129, 2, 4, 8, 16, 32, 96, 64, 128, - 128, 135, 30, 115, 207, 159, 255, 255, 255, 255, 127, 63, 31, 31, 31, 31, 31, 31, 31, 7, 7, 7, 127, 127, - 127, 127, 127, 127, 255, 255, 255, 255, 252, 240, 227, 231, 207, 207, 207, 207, 207, 207, 231, 255, 255, - 231, 207, 207, 207, 207, 207, 198, 224, 240, 255, 255, 255, 0, 0, 231, 207, 207, 207, 199, 224, 240, - 255, 225, 193, 204, 204, 204, 228, 192, 192, 255, 255, 255, 192, 192, 255, 255, 255, 255, 255, 255, 192, - 192, 252, 248, 243, 231, 207, 223, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, - 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 254, 252, 248, 248, 240, 240, 224, - 225, 225, 193, 193, 195, 195, 195, 195, 195, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 62, 62, 62, 62, 62, 62, - 255, 243, 3, 3, 51, 51, 51, 19, 135, 239, 255, 255, 63, 63, 159, 159, 159, 159, 63, 127, 255, 255, 255, - 63, 31, 159, 159, 159, 31, 252, 252, 255, 63, 63, 159, 159, 159, 159, 63, 127, 255, 255, 255, 223, 159, - 159, 159, 31, 127, 255, 255, 255, 255, 223, 31, 31, 191, 159, 159, 159, 255, 255, 127, 63, 159, 159, - 159, 159, 31, 31, 255, 255, 247, 3, 7, 159, 159, 159, 31, 127, 255, 255, 255, 255, 255, 255, 255, 255, - 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, - 255, 254, 252, 252, 252, 252, 252, 252, 252, 252, 224, 224, 224, 255, 255, 255, 255, 255, 255, 255, 243, - 240, 240, 247, 255, 254, 252, 248, 243, 255, 255, 248, 248, 242, 242, 242, 242, 242, 250, 255, 255, 255, - 241, 242, 242, 242, 242, 248, 253, 255, 255, 248, 248, 242, 242, 242, 242, 242, 250, 255, 255, 249, 240, - 242, 242, 242, 240, 240, 255, 255, 255, 255, 243, 240, 240, 243, 243, 255, 255, 255, 255, 252, 248, 243, - 243, 243, 243, 243, 255, 255, 255, 247, 240, 240, 247, 255, 247, 240, 240, 247, 255] - font = [0x00, 0x00, 0x00, 0x00, 0x00, 0x3E, 0x5B, 0x4F, 0x5B, 0x3E, 0x3E, 0x6B, 0x4F, 0x6B, 0x3E, - 0x1C, 0x3E, 0x7C, 0x3E, 0x1C, 0x18, 0x3C, 0x7E, 0x3C, 0x18, 0x1C, 0x57, 0x7D, 0x57, 0x1C, - 0x1C, 0x5E, 0x7F, 0x5E, 0x1C, 0x00, 0x18, 0x3C, 0x18, 0x00, 0xFF, 0xE7, 0xC3, 0xE7, 0xFF, - 0x00, 0x18, 0x24, 0x18, 0x00, 0xFF, 0xE7, 0xDB, 0xE7, 0xFF, 0x30, 0x48, 0x3A, 0x06, 0x0E, - 0x26, 0x29, 0x79, 0x29, 0x26, 0x40, 0x7F, 0x05, 0x05, 0x07, 0x40, 0x7F, 0x05, 0x25, 0x3F, - 0x5A, 0x3C, 0xE7, 0x3C, 0x5A, 0x7F, 0x3E, 0x1C, 0x1C, 0x08, 0x08, 0x1C, 0x1C, 0x3E, 0x7F, - 0x14, 0x22, 0x7F, 0x22, 0x14, 0x5F, 0x5F, 0x00, 0x5F, 0x5F, 0x06, 0x09, 0x7F, 0x01, 0x7F, - 0x00, 0x66, 0x89, 0x95, 0x6A, 0x60, 0x60, 0x60, 0x60, 0x60, 0x94, 0xA2, 0xFF, 0xA2, 0x94, - 0x08, 0x04, 0x7E, 0x04, 0x08, 0x10, 0x20, 0x7E, 0x20, 0x10, 0x08, 0x08, 0x2A, 0x1C, 0x08, - 0x08, 0x1C, 0x2A, 0x08, 0x08, 0x1E, 0x10, 0x10, 0x10, 0x10, 0x0C, 0x1E, 0x0C, 0x1E, 0x0C, - 0x30, 0x38, 0x3E, 0x38, 0x30, 0x06, 0x0E, 0x3E, 0x0E, 0x06, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x5F, 0x00, 0x00, 0x00, 0x07, 0x00, 0x07, 0x00, 0x14, 0x7F, 0x14, 0x7F, 0x14, - 0x24, 0x2A, 0x7F, 0x2A, 0x12, 0x23, 0x13, 0x08, 0x64, 0x62, 0x36, 0x49, 0x56, 0x20, 0x50, - 0x00, 0x08, 0x07, 0x03, 0x00, 0x00, 0x1C, 0x22, 0x41, 0x00, 0x00, 0x41, 0x22, 0x1C, 0x00, - 0x2A, 0x1C, 0x7F, 0x1C, 0x2A, 0x08, 0x08, 0x3E, 0x08, 0x08, 0x00, 0x80, 0x70, 0x30, 0x00, - 0x08, 0x08, 0x08, 0x08, 0x08, 0x00, 0x00, 0x60, 0x60, 0x00, 0x20, 0x10, 0x08, 0x04, 0x02, - 0x3E, 0x51, 0x49, 0x45, 0x3E, 0x00, 0x42, 0x7F, 0x40, 0x00, 0x72, 0x49, 0x49, 0x49, 0x46, - 0x21, 0x41, 0x49, 0x4D, 0x33, 0x18, 0x14, 0x12, 0x7F, 0x10, 0x27, 0x45, 0x45, 0x45, 0x39, - 0x3C, 0x4A, 0x49, 0x49, 0x31, 0x41, 0x21, 0x11, 0x09, 0x07, 0x36, 0x49, 0x49, 0x49, 0x36, - 0x46, 0x49, 0x49, 0x29, 0x1E, 0x00, 0x00, 0x14, 0x00, 0x00, 0x00, 0x40, 0x34, 0x00, 0x00, - 0x00, 0x08, 0x14, 0x22, 0x41, 0x14, 0x14, 0x14, 0x14, 0x14, 0x00, 0x41, 0x22, 0x14, 0x08, - 0x02, 0x01, 0x59, 0x09, 0x06, 0x3E, 0x41, 0x5D, 0x59, 0x4E, 0x7C, 0x12, 0x11, 0x12, 0x7C, - 0x7F, 0x49, 0x49, 0x49, 0x36, 0x3E, 0x41, 0x41, 0x41, 0x22, 0x7F, 0x41, 0x41, 0x41, 0x3E, - 0x7F, 0x49, 0x49, 0x49, 0x41, 0x7F, 0x09, 0x09, 0x09, 0x01, 0x3E, 0x41, 0x41, 0x51, 0x73, - 0x7F, 0x08, 0x08, 0x08, 0x7F, 0x00, 0x41, 0x7F, 0x41, 0x00, 0x20, 0x40, 0x41, 0x3F, 0x01, - 0x7F, 0x08, 0x14, 0x22, 0x41, 0x7F, 0x40, 0x40, 0x40, 0x40, 0x7F, 0x02, 0x1C, 0x02, 0x7F, - 0x7F, 0x04, 0x08, 0x10, 0x7F, 0x3E, 0x41, 0x41, 0x41, 0x3E, 0x7F, 0x09, 0x09, 0x09, 0x06, - 0x3E, 0x41, 0x51, 0x21, 0x5E, 0x7F, 0x09, 0x19, 0x29, 0x46, 0x26, 0x49, 0x49, 0x49, 0x32, - 0x03, 0x01, 0x7F, 0x01, 0x03, 0x3F, 0x40, 0x40, 0x40, 0x3F, 0x1F, 0x20, 0x40, 0x20, 0x1F, - 0x3F, 0x40, 0x38, 0x40, 0x3F, 0x63, 0x14, 0x08, 0x14, 0x63, 0x03, 0x04, 0x78, 0x04, 0x03, - 0x61, 0x59, 0x49, 0x4D, 0x43, 0x00, 0x7F, 0x41, 0x41, 0x41, 0x02, 0x04, 0x08, 0x10, 0x20, - 0x00, 0x41, 0x41, 0x41, 0x7F, 0x04, 0x02, 0x01, 0x02, 0x04, 0x40, 0x40, 0x40, 0x40, 0x40, - 0x00, 0x03, 0x07, 0x08, 0x00, 0x20, 0x54, 0x54, 0x78, 0x40, 0x7F, 0x28, 0x44, 0x44, 0x38, - 0x38, 0x44, 0x44, 0x44, 0x28, 0x38, 0x44, 0x44, 0x28, 0x7F, 0x38, 0x54, 0x54, 0x54, 0x18, - 0x00, 0x08, 0x7E, 0x09, 0x02, 0x18, 0xA4, 0xA4, 0x9C, 0x78, 0x7F, 0x08, 0x04, 0x04, 0x78, - 0x00, 0x44, 0x7D, 0x40, 0x00, 0x20, 0x40, 0x40, 0x3D, 0x00, 0x7F, 0x10, 0x28, 0x44, 0x00, - 0x00, 0x41, 0x7F, 0x40, 0x00, 0x7C, 0x04, 0x78, 0x04, 0x78, 0x7C, 0x08, 0x04, 0x04, 0x78, - 0x38, 0x44, 0x44, 0x44, 0x38, 0xFC, 0x18, 0x24, 0x24, 0x18, 0x18, 0x24, 0x24, 0x18, 0xFC, - 0x7C, 0x08, 0x04, 0x04, 0x08, 0x48, 0x54, 0x54, 0x54, 0x24, 0x04, 0x04, 0x3F, 0x44, 0x24, - 0x3C, 0x40, 0x40, 0x20, 0x7C, 0x1C, 0x20, 0x40, 0x20, 0x1C, 0x3C, 0x40, 0x30, 0x40, 0x3C, - 0x44, 0x28, 0x10, 0x28, 0x44, 0x4C, 0x90, 0x90, 0x90, 0x7C, 0x44, 0x64, 0x54, 0x4C, 0x44, - 0x00, 0x08, 0x36, 0x41, 0x00, 0x00, 0x00, 0x77, 0x00, 0x00, 0x00, 0x41, 0x36, 0x08, 0x00, - 0x02, 0x01, 0x02, 0x04, 0x02, 0x3C, 0x26, 0x23, 0x26, 0x3C, 0x1E, 0xA1, 0xA1, 0x61, 0x12, - 0x3A, 0x40, 0x40, 0x20, 0x7A, 0x38, 0x54, 0x54, 0x55, 0x59, 0x21, 0x55, 0x55, 0x79, 0x41, - 0x21, 0x54, 0x54, 0x78, 0x41, 0x21, 0x55, 0x54, 0x78, 0x40, 0x20, 0x54, 0x55, 0x79, 0x40, - 0x0C, 0x1E, 0x52, 0x72, 0x12, 0x39, 0x55, 0x55, 0x55, 0x59, 0x39, 0x54, 0x54, 0x54, 0x59, - 0x39, 0x55, 0x54, 0x54, 0x58, 0x00, 0x00, 0x45, 0x7C, 0x41, 0x00, 0x02, 0x45, 0x7D, 0x42, - 0x00, 0x01, 0x45, 0x7C, 0x40, 0xF0, 0x29, 0x24, 0x29, 0xF0, 0xF0, 0x28, 0x25, 0x28, 0xF0, - 0x7C, 0x54, 0x55, 0x45, 0x00, 0x20, 0x54, 0x54, 0x7C, 0x54, 0x7C, 0x0A, 0x09, 0x7F, 0x49, - 0x32, 0x49, 0x49, 0x49, 0x32, 0x32, 0x48, 0x48, 0x48, 0x32, 0x32, 0x4A, 0x48, 0x48, 0x30, - 0x3A, 0x41, 0x41, 0x21, 0x7A, 0x3A, 0x42, 0x40, 0x20, 0x78, 0x00, 0x9D, 0xA0, 0xA0, 0x7D, - 0x39, 0x44, 0x44, 0x44, 0x39, 0x3D, 0x40, 0x40, 0x40, 0x3D, 0x3C, 0x24, 0xFF, 0x24, 0x24, - 0x48, 0x7E, 0x49, 0x43, 0x66, 0x2B, 0x2F, 0xFC, 0x2F, 0x2B, 0xFF, 0x09, 0x29, 0xF6, 0x20, - 0xC0, 0x88, 0x7E, 0x09, 0x03, 0x20, 0x54, 0x54, 0x79, 0x41, 0x00, 0x00, 0x44, 0x7D, 0x41, - 0x30, 0x48, 0x48, 0x4A, 0x32, 0x38, 0x40, 0x40, 0x22, 0x7A, 0x00, 0x7A, 0x0A, 0x0A, 0x72, - 0x7D, 0x0D, 0x19, 0x31, 0x7D, 0x26, 0x29, 0x29, 0x2F, 0x28, 0x26, 0x29, 0x29, 0x29, 0x26, - 0x30, 0x48, 0x4D, 0x40, 0x20, 0x38, 0x08, 0x08, 0x08, 0x08, 0x08, 0x08, 0x08, 0x08, 0x38, - 0x2F, 0x10, 0xC8, 0xAC, 0xBA, 0x2F, 0x10, 0x28, 0x34, 0xFA, 0x00, 0x00, 0x7B, 0x00, 0x00, - 0x08, 0x14, 0x2A, 0x14, 0x22, 0x22, 0x14, 0x2A, 0x14, 0x08, 0xAA, 0x00, 0x55, 0x00, 0xAA, - 0xAA, 0x55, 0xAA, 0x55, 0xAA, 0x00, 0x00, 0x00, 0xFF, 0x00, 0x10, 0x10, 0x10, 0xFF, 0x00, - 0x14, 0x14, 0x14, 0xFF, 0x00, 0x10, 0x10, 0xFF, 0x00, 0xFF, 0x10, 0x10, 0xF0, 0x10, 0xF0, - 0x14, 0x14, 0x14, 0xFC, 0x00, 0x14, 0x14, 0xF7, 0x00, 0xFF, 0x00, 0x00, 0xFF, 0x00, 0xFF, - 0x14, 0x14, 0xF4, 0x04, 0xFC, 0x14, 0x14, 0x17, 0x10, 0x1F, 0x10, 0x10, 0x1F, 0x10, 0x1F, - 0x14, 0x14, 0x14, 0x1F, 0x00, 0x10, 0x10, 0x10, 0xF0, 0x00, 0x00, 0x00, 0x00, 0x1F, 0x10, - 0x10, 0x10, 0x10, 0x1F, 0x10, 0x10, 0x10, 0x10, 0xF0, 0x10, 0x00, 0x00, 0x00, 0xFF, 0x10, - 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0xFF, 0x10, 0x00, 0x00, 0x00, 0xFF, 0x14, - 0x00, 0x00, 0xFF, 0x00, 0xFF, 0x00, 0x00, 0x1F, 0x10, 0x17, 0x00, 0x00, 0xFC, 0x04, 0xF4, - 0x14, 0x14, 0x17, 0x10, 0x17, 0x14, 0x14, 0xF4, 0x04, 0xF4, 0x00, 0x00, 0xFF, 0x00, 0xF7, - 0x14, 0x14, 0x14, 0x14, 0x14, 0x14, 0x14, 0xF7, 0x00, 0xF7, 0x14, 0x14, 0x14, 0x17, 0x14, - 0x10, 0x10, 0x1F, 0x10, 0x1F, 0x14, 0x14, 0x14, 0xF4, 0x14, 0x10, 0x10, 0xF0, 0x10, 0xF0, - 0x00, 0x00, 0x1F, 0x10, 0x1F, 0x00, 0x00, 0x00, 0x1F, 0x14, 0x00, 0x00, 0x00, 0xFC, 0x14, - 0x00, 0x00, 0xF0, 0x10, 0xF0, 0x10, 0x10, 0xFF, 0x10, 0xFF, 0x14, 0x14, 0x14, 0xFF, 0x14, - 0x10, 0x10, 0x10, 0x1F, 0x00, 0x00, 0x00, 0x00, 0xF0, 0x10, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, - 0xF0, 0xF0, 0xF0, 0xF0, 0xF0, 0xFF, 0xFF, 0xFF, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFF, 0xFF, - 0x0F, 0x0F, 0x0F, 0x0F, 0x0F, 0x38, 0x44, 0x44, 0x38, 0x44, 0x7C, 0x2A, 0x2A, 0x3E, 0x14, - 0x7E, 0x02, 0x02, 0x06, 0x06, 0x02, 0x7E, 0x02, 0x7E, 0x02, 0x63, 0x55, 0x49, 0x41, 0x63, - 0x38, 0x44, 0x44, 0x3C, 0x04, 0x40, 0x7E, 0x20, 0x1E, 0x20, 0x06, 0x02, 0x7E, 0x02, 0x02, - 0x99, 0xA5, 0xE7, 0xA5, 0x99, 0x1C, 0x2A, 0x49, 0x2A, 0x1C, 0x4C, 0x72, 0x01, 0x72, 0x4C, - 0x30, 0x4A, 0x4D, 0x4D, 0x30, 0x30, 0x48, 0x78, 0x48, 0x30, 0xBC, 0x62, 0x5A, 0x46, 0x3D, - 0x3E, 0x49, 0x49, 0x49, 0x00, 0x7E, 0x01, 0x01, 0x01, 0x7E, 0x2A, 0x2A, 0x2A, 0x2A, 0x2A, - 0x44, 0x44, 0x5F, 0x44, 0x44, 0x40, 0x51, 0x4A, 0x44, 0x40, 0x40, 0x44, 0x4A, 0x51, 0x40, - 0x00, 0x00, 0xFF, 0x01, 0x03, 0xE0, 0x80, 0xFF, 0x00, 0x00, 0x08, 0x08, 0x6B, 0x6B, 0x08, - 0x36, 0x12, 0x36, 0x24, 0x36, 0x06, 0x0F, 0x09, 0x0F, 0x06, 0x00, 0x00, 0x18, 0x18, 0x00, - 0x00, 0x00, 0x10, 0x10, 0x00, 0x30, 0x40, 0xFF, 0x01, 0x01, 0x00, 0x1F, 0x01, 0x01, 0x1E, - 0x00, 0x19, 0x1D, 0x17, 0x12, 0x00, 0x3C, 0x3C, 0x3C, 0x3C, 0x00, 0x00, 0x00, 0x00, 0x00] # ascii fonts - - def __init__(self, I2C, **args): - self.buff = [0 for a in range(1024)] - self.ADDRESS = args.get('address', self.ADDRESS) - self.I2C = I2C - self.SSD1306_command(self.SSD1306_DISPLAYOFF) # 0xAE - self.SSD1306_command(self.SSD1306_SETDISPLAYCLOCKDIV) # 0xD5 - self.SSD1306_command(0x80) # the suggested ratio 0x80 - self.SSD1306_command(self.SSD1306_SETMULTIPLEX) # 0xA8 - self.SSD1306_command(0x3F) - self.SSD1306_command(self.SSD1306_SETDISPLAYOFFSET) # 0xD3 - self.SSD1306_command(0x0) # no offset - self.SSD1306_command(self.SSD1306_SETSTARTLINE | 0x0) # line #0 - self.SSD1306_command(self.SSD1306_CHARGEPUMP) # 0x8D - self.SSD1306_command(0x14) # vccstate = self.SSD1306_SWITCHCAPVCC - self.SSD1306_command(self.SSD1306_MEMORYMODE) # 0x20 - self.SSD1306_command(0x00) # 0x0 act like ks0108 - self.SSD1306_command(self.SSD1306_SEGREMAP | 0x1) - self.SSD1306_command(self.SSD1306_COMSCANDEC) - self.SSD1306_command(self.SSD1306_SETCOMPINS) # 0xDA - self.SSD1306_command(0x12) - self.SSD1306_command(self.SSD1306_SETCONTRAST) # 0x81 - self.SSD1306_command(0xFF) # vccstate = self.SSD1306_SWITCHCAPVCC - self.SSD1306_command(self.SSD1306_SETPRECHARGE) # 0xd9 - self.SSD1306_command(0xF1) # vccstate = self.SSD1306_SWITCHCAPVCC - self.SSD1306_command(self.SSD1306_SETVCOMDETECT) # 0xDB - self.SSD1306_command(0x40) - self.SSD1306_command(self.SSD1306_DISPLAYALLON_RESUME) # 0xA4 - self.SSD1306_command(self.SSD1306_NORMALDISPLAY) # 0xA6 - self.SSD1306_command(self.SSD1306_DISPLAYON) # --turn on oled panel - - def load(self, arg): - self.scroll('stop') - if arg == 'logo': - self.clearDisplay() - for a in range(1024): - self.buff[a] = self.logobuff[a] - self.displayOLED() - - def SSD1306_command(self, cmd): - self.I2C.writeBulk(self.ADDRESS, [0x00, cmd]) - - def SSD1306_data(self, data): - self.I2C.writeBulk(self.ADDRESS, [0x40, data]) - - def clearDisplay(self): - self.setCursor(0, 0) - for a in range(self.SSD1306_LCDWIDTH * self.SSD1306_LCDHEIGHT / 8): - self.buff[a] = 0 - - def displayOLED(self): - self.SSD1306_command(self.SSD1306_SETLOWCOLUMN | 0x0) # low col = 0 - self.SSD1306_command(self.SSD1306_SETHIGHCOLUMN | 0x0) # hi col = 0 - self.SSD1306_command(self.SSD1306_SETSTARTLINE | 0x0) # line #0 - a = 0 - while (a < self.SSD1306_LCDWIDTH * self.SSD1306_LCDHEIGHT / 8): - self.I2C.writeBulk(self.ADDRESS, [0x40] + self.buff[a:a + 16]) - a += 16 - - def setContrast(self, i): - self.SSD1306_command(self.SSD1306_SETCONTRAST) - self.SSD1306_command(i) - - def drawPixel(self, x, y, color): - if (color == 1): - self.buff[x + (y / 8) * self.SSD1306_LCDWIDTH] |= (1 << (y % 8)) - else: - self.buff[x + (y / 8) * self.SSD1306_LCDWIDTH] &= ~(1 << (y % 8)) - - def drawCircle(self, x0, y0, r, color): - f = 1 - r - ddF_x = 1 - ddF_y = -2 * r - x = 0 - y = r - self.drawPixel(x0, y0 + r, color) - self.drawPixel(x0, y0 - r, color) - self.drawPixel(x0 + r, y0, color) - self.drawPixel(x0 - r, y0, color) - while (x < y): - if (f >= 0): - y -= 1 - ddF_y += 2 - f += ddF_y - x += 1 - ddF_x += 2 - f += ddF_x - self.drawPixel(x0 + x, y0 + y, color) - self.drawPixel(x0 - x, y0 + y, color) - self.drawPixel(x0 + x, y0 - y, color) - self.drawPixel(x0 - x, y0 - y, color) - self.drawPixel(x0 + y, y0 + x, color) - self.drawPixel(x0 - y, y0 + x, color) - self.drawPixel(x0 + y, y0 - x, color) - self.drawPixel(x0 - y, y0 - x, color) - - def drawLine(self, x0, y0, x1, y1, color): - steep = abs(y1 - y0) > abs(x1 - x0) - if (steep): - tmp = y0 - y0 = x0 - x0 = tmp - tmp = y1 - y1 = x1 - x1 = tmp - if (x0 > x1): - tmp = x1 - x1 = x0 - x0 = tmp - tmp = y1 - y1 = y0 - y0 = tmp - - dx = x1 - x0 - dy = abs(y1 - y0) - err = dx / 2 - - if (y0 < y1): - ystep = 1 - else: - ystep = -1 - - while (x0 <= x1): - if (steep): - self.drawPixel(y0, x0, color) - else: - self.drawPixel(x0, y0, color) - err -= dy - if (err < 0): - y0 += ystep - err += dx - x0 += 1 - - def drawRect(self, x, y, w, h, color): - self.drawFastHLine(x, y, w, color) - self.drawFastHLine(x, y + h - 1, w, color) - self.drawFastVLine(x, y, h, color) - self.drawFastVLine(x + w - 1, y, h, color) - - def drawFastVLine(self, x, y, h, color): - self.drawLine(x, y, x, y + h - 1, color) - - def drawFastHLine(self, x, y, w, color): - self.drawLine(x, y, x + w - 1, y, color) - - def fillRect(self, x, y, w, h, color): - for i in range(x, x + w): - self.drawFastVLine(i, y, h, color) - - def writeString(self, s): - for a in s: self.writeChar(ord(a)) - - def writeChar(self, c): - if (c == '\n'): - self.cursor_y += self.textsize * 8 - self.cursor_x = 0 - elif (c == '\r'): - pass - else: - self.drawChar(self.cursor_x, self.cursor_y, c, self.textcolor, self.textbgcolor, self.textsize) - self.cursor_x += self.textsize * 6 - if (self.wrap and (self.cursor_x > (self._width - self.textsize * 6))): - self.cursor_y += self.textsize * 8 - self.cursor_x = 0 - - def drawChar(self, x, y, c, color, bg, size): - if ((x >= self._width) or (y >= self._height) or ((x + 5 * size - 1) < 0) or ((y + 8 * size - 1) < 0)): - return - for i in range(6): - if (i == 5): - line = 0x0 - else: - line = self.font[c * 5 + i] - for j in range(8): - if (line & 0x1): - if (size == 1): - self.drawPixel(x + i, y + j, color) - else: - self.fillRect(x + (i * size), y + (j * size), size, size, color) - elif (bg != color): - if (size == 1): - self.drawPixel(x + i, y + j, bg) - else: - self.fillRect(x + i * size, y + j * size, size, size, bg) - line >>= 1 - - def setCursor(self, x, y): - self.cursor_x = x - self.cursor_y = y - - def setTextSize(self, s): - self.textsize = s if (s > 0) else 1 - - def setTextColor(self, c, b): - self.textcolor = c - self.textbgcolor = b - - def setTextWrap(self, w): - self.wrap = w - - def scroll(self, arg): - if arg == 'left': - self.SSD1306_command(0x27) # up-0x29 ,2A left-0x27 right0x26 - if arg == 'right': - self.SSD1306_command(0x26) # up-0x29 ,2A left-0x27 right0x26 - if arg in ['topright', 'bottomright']: - self.SSD1306_command(0x29) # up-0x29 ,2A left-0x27 right0x26 - if arg in ['topleft', 'bottomleft']: - self.SSD1306_command(0x2A) # up-0x29 ,2A left-0x27 right0x26 - - if arg in ['left', 'right', 'topright', 'topleft', 'bottomleft', 'bottomright']: - self.SSD1306_command(0x00) # dummy - self.SSD1306_command(0x0) # start page - self.SSD1306_command(0x7) # time interval 0b100 - 3 frames - self.SSD1306_command(0xf) # end page - if arg in ['topleft', 'topright']: - self.SSD1306_command(0x02) # dummy 00 . xx for horizontal scroll (speed) - elif arg in ['bottomleft', 'bottomright']: - self.SSD1306_command(0xfe) # dummy 00 . xx for horizontal scroll (speed) - - if arg in ['left', 'right']: - self.SSD1306_command(0x02) # dummy 00 . xx for horizontal scroll (speed) - self.SSD1306_command(0xff) - - self.SSD1306_command(0x2F) - - if arg == 'stop': - self.SSD1306_command(0x2E) - - def pulseIt(self): - for a in range(2): - self.SSD1306_command(0xD6) - self.SSD1306_command(0x01) - time.sleep(0.1) - self.SSD1306_command(0xD6) - self.SSD1306_command(0x00) - time.sleep(0.1) - - -if __name__ == "__main__": - from PSL import sciencelab - - I = sciencelab.connect() - O = connect(I.I2C) - textbgcolor = 0 - textcolor = 1 - O.load('logo') - O.scroll('topright') - import time - - time.sleep(2.8) - O.scroll('stop') diff --git a/PSL/SENSORS/Sx1276.py b/PSL/SENSORS/Sx1276.py deleted file mode 100644 index 022a3c76..00000000 --- a/PSL/SENSORS/Sx1276.py +++ /dev/null @@ -1,331 +0,0 @@ -#Registers adapted from sample code for SEMTECH SX1276 -import time - -def connect(SPI,frq,**kwargs): - return SX1276(SPI,frq,**kwargs) - - -class SX1276(): - name = 'SX1276' - #registers - REG_FIFO = 0x00 - REG_OP_MODE = 0x01 - REG_FRF_MSB = 0x06 - REG_FRF_MID = 0x07 - REG_FRF_LSB = 0x08 - REG_PA_CONFIG = 0x09 - REG_LNA = 0x0c - REG_FIFO_ADDR_PTR = 0x0d - REG_FIFO_TX_BASE_ADDR = 0x0e - REG_FIFO_RX_BASE_ADDR = 0x0f - REG_FIFO_RX_CURRENT_ADDR = 0x10 - REG_IRQ_FLAGS = 0x12 - REG_RX_NB_BYTES = 0x13 - REG_PKT_RSSI_VALUE = 0x1a - REG_PKT_SNR_VALUE = 0x1b - REG_MODEM_CONFIG_1 = 0x1d - REG_MODEM_CONFIG_2 = 0x1e - REG_PREAMBLE_MSB = 0x20 - REG_PREAMBLE_LSB = 0x21 - REG_PAYLOAD_LENGTH = 0x22 - REG_MODEM_CONFIG_3 = 0x26 - REG_RSSI_WIDEBAND = 0x2c - REG_DETECTION_OPTIMIZE = 0x31 - REG_DETECTION_THRESHOLD = 0x37 - REG_SYNC_WORD = 0x39 - REG_DIO_MAPPING_1 = 0x40 - REG_VERSION = 0x42 - REG_PA_DAC = 0x4D - #modes - MODE_LONG_RANGE_MODE = 0x80 - MODE_SLEEP = 0x00 - MODE_STDBY = 0x01 - MODE_TX = 0x03 - MODE_RX_CONTINUOUS = 0x05 - MODE_RX_SINGLE = 0x06 - - #PA config - PA_BOOST = 0x80 - - #IRQ masks - IRQ_TX_DONE_MASK = 0x08 - IRQ_PAYLOAD_CRC_ERROR_MASK = 0x20 - IRQ_RX_DONE_MASK = 0x40 - - MAX_PKT_LENGTH = 255 - - PA_OUTPUT_RFO_PIN =0 - PA_OUTPUT_PA_BOOST_PIN =1 - _onReceive = 0 - _frequency = 10 - _packetIndex = 0 - packetLength =0 - - def __init__(self,SPI,frq,**kwargs): - self.SPI = SPI - self.SPI.set_parameters(2,6,1,0) - self.name = 'SX1276' - self.frequency = frq - - self.reset() - self.version = self.SPIRead(self.REG_VERSION,1)[0] - if self.version!=0x12: - print 'version error',self.version - self.sleep() - self.setFrequency(self.frequency) - - #set base address - self.SPIWrite(self.REG_FIFO_TX_BASE_ADDR,[0]) - self.SPIWrite(self.REG_FIFO_RX_BASE_ADDR,[0]) - - #set LNA boost - self.SPIWrite(self.REG_LNA,[self.SPIRead(self.REG_LNA)[0]|0x03]) - - #set auto ADC - self.SPIWrite(self.REG_MODEM_CONFIG_3,[0x04]) - - #output power 17dbm - self.setTxPower(kwargs.get('power',17),self.PA_OUTPUT_PA_BOOST_PIN if kwargs.get('boost',True) else self.PA_OUTPUT_RFO_PIN) - self.idle() - - #set bandwidth - self.setSignalBandwidth(kwargs.get('BW',125e3)) - self.setSpreadingFactor(kwargs.get('SF',12)) - self.setCodingRate4(kwargs.get('CF',5)) - - def beginPacket(self,implicitHeader=False): - self.idle() - if implicitHeader: - self.implicitHeaderMode() - else: - self.explicitHeaderMode() - - #reset FIFO & payload length - self.SPIWrite(self.REG_FIFO_ADDR_PTR,[0]) - self.SPIWrite(self.REG_PAYLOAD_LENGTH,[0]) - - def endPacket(self): - #put in TX mode - self.SPIWrite(self.REG_OP_MODE,[self.MODE_LONG_RANGE_MODE|self.MODE_TX]) - while 1: #Wait for TX done - if self.SPIRead(self.REG_IRQ_FLAGS,1)[0] & self.IRQ_TX_DONE_MASK: break - else: - print ('wait...') - time.sleep(0.1) - self.SPIWrite(self.REG_IRQ_FLAGS,[self.IRQ_TX_DONE_MASK]) - - def parsePacket(self,size=0): - self.packetLength = 0 - irqFlags = self.SPIRead(self.REG_IRQ_FLAGS,1)[0] - if size>0: - self.implicitHeaderMode() - self.SPIWrite(self.REG_PAYLOAD_LENGTH,[size&0xFF]) - else: - self.explicitHeaderMode() - self.SPIWrite(self.REG_IRQ_FLAGS,[irqFlags]) - if (irqFlags & self.IRQ_RX_DONE_MASK) and (irqFlags & self.IRQ_PAYLOAD_CRC_ERROR_MASK)==0 : - self._packetIndex = 0 - if self._implicitHeaderMode: - self.packetLength = self.SPIRead(self.REG_PAYLOAD_LENGTH,1)[0] - else: - self.packetLength = self.SPIRead(self.REG_RX_NB_BYTES,1)[0] - self.SPIWrite(self.REG_FIFO_ADDR_PTR,self.SPIRead(self.REG_FIFO_RX_CURRENT_ADDR,1)) - self.idle() - elif self.SPIRead(self.REG_OP_MODE)[0] != (self.MODE_LONG_RANGE_MODE|self.MODE_RX_SINGLE): - self.SPIWrite(self.REG_FIFO_ADDR_PTR,[0]) - self.SPIWrite(self.REG_OP_MODE,[self.MODE_LONG_RANGE_MODE|self.MODE_RX_SINGLE]) - return self.packetLength - - def packetRssi(self): - return self.SPIRead(self.REG_PKT_RSSI_VALUE)[0] - (164 if self._frequency<868e6 else 157) - def packetSnr(self): - return self.SPIRead(self.REG_PKT_SNR_VALUE)[0]*0.25 - - def write(self,byteArray): - size = len(byteArray) - currentLength = self.SPIRead(self.REG_PAYLOAD_LENGTH)[0] - if (currentLength+size) > self.MAX_PKT_LENGTH: - size = self.MAX_PKT_LENGTH - currentLength - self.SPIWrite(self.REG_FIFO,byteArray[:size]) - self.SPIWrite(self.REG_PAYLOAD_LENGTH,[currentLength+size]) - return size - def available(self): - return self.SPIRead(self.REG_RX_NB_BYTES)[0] - self._packetIndex - - def checkRx(self): - irqFlags = self.SPIRead(self.REG_IRQ_FLAGS,1)[0] - if (irqFlags & self.IRQ_RX_DONE_MASK) and (irqFlags & self.IRQ_PAYLOAD_CRC_ERROR_MASK)==0 : - return 1 - return 0; - - - def read(self): - if not self.available():return -1 - self._packetIndex+=1 - return self.SPIRead(self.REG_FIFO)[0] - - def readAll(self): - p=[] - while self.available(): - p.append(self.read()) - return p - - def peek(self): - if not self.available():return -1 - self.currentAddress = self.SPIRead(self.REG_FIFO_ADDR_PTR) - val = self.SPIRead(self.REG_FIFO)[0] - self.SPIWrite(self.REG_FIFO_ADDR_PTR,self.currentAddress) - return val - def flush(self): - pass - - - def receive(self,size): - if size>0: - self.implicitHeaderMode() - self.SPIWrite(self.REG_PAYLOAD_LENGTH,[size&0xFF]) - else: - self.explicitHeaderMode() - - self.SPIWrite(self.REG_OP_MODE,[self.MODE_LONG_RANGE_MODE|self.MODE_RX_SINGLE]) - - - def reset(self): - pass - - - def idle(self): - self.SPIWrite(self.REG_OP_MODE,[self.MODE_LONG_RANGE_MODE|self.MODE_STDBY]) - - def sleep(self): - self.SPIWrite(self.REG_OP_MODE,[self.MODE_LONG_RANGE_MODE|self.MODE_SLEEP]) - - def setTxPower(self,level,pin): - if pin == self.PA_OUTPUT_RFO_PIN: - if level<0: level=0 - elif level>14: level = 14 - self.SPIWrite(self.REG_PA_CONFIG,[0x70|level]) - else: - if level<2: level=2 - elif level>17: - level = 17 - if level==17: - print ('max power output') - self.SPIWrite(self.REG_PA_DAC,[0x87]) - else: - self.SPIWrite(self.REG_PA_DAC,[0x84]) - self.SPIWrite(self.REG_PA_CONFIG,[self.PA_BOOST|0x70|(level-2)]) - - print 'power',hex(self.SPIRead(self.REG_PA_CONFIG)[0]) - - def setFrequency(self,frq): - self._frequency = frq - frf = (int(frq)<<19)/32000000 - print ('frf',frf) - print ('freq',(frf>>16)&0xFF,(frf>>8)&0xFF,(frf)&0xFF) - self.SPIWrite(self.REG_FRF_MSB,[(frf>>16)&0xFF]) - self.SPIWrite(self.REG_FRF_MID,[(frf>>8)&0xFF]) - self.SPIWrite(self.REG_FRF_LSB,[frf&0xFF]) - - def setSpreadingFactor(self,sf): - if sf<6:sf=6 - elif sf>12:sf=12 - - if sf==6: - self.SPIWrite(self.REG_DETECTION_OPTIMIZE,[0xc5]) - self.SPIWrite(self.REG_DETECTION_THRESHOLD,[0x0c]) - else: - self.SPIWrite(self.REG_DETECTION_OPTIMIZE,[0xc3]) - self.SPIWrite(self.REG_DETECTION_THRESHOLD,[0x0a]) - self.SPIWrite(self.REG_MODEM_CONFIG_2,[(self.SPIRead(self.REG_MODEM_CONFIG_2)[0]&0x0F)|((sf<<4)&0xF0)]) - - def setSignalBandwidth(self,sbw): - bw=9 - num=0 - for a in [7.8e3,10.4e3,15.6e3,20.8e3,31.25e3,41.7e3,62.5e3,125e3,250e3]: - if sbw<=a: - bw = num - break - num+=1 - print ('bandwidth: ',bw) - self.SPIWrite(self.REG_MODEM_CONFIG_1,[(self.SPIRead(self.REG_MODEM_CONFIG_1)[0]&0x0F)|(bw<<4)]) - - def setCodingRate4(self,denominator): - if denominator<5:denominator = 5 - elif denominator>8:denominator = 8 - self.SPIWrite(self.REG_MODEM_CONFIG_1,[(self.SPIRead(self.REG_MODEM_CONFIG_1)[0]&0xF1)|((denominator-4)<<4)]) - - def setPreambleLength(self,length): - self.SPIWrite(self.REG_PREAMBLE_MSB,[(length>>8)&0xFF]) - self.SPIWrite(self.REG_PREAMBLE_LSB,[length&0xFF]) - - def setSyncWord(self,sw): - self.SPIWrite(self.REG_SYNC_WORD,[sw]) - - def crc(self): - self.SPIWrite(self.REG_MODEM_CONFIG_2,[self.SPIRead(self.REG_MODEM_CONFIG_2)[0]|0x04]) - def noCrc(self): - self.SPIWrite(self.REG_MODEM_CONFIG_2,[self.SPIRead(self.REG_MODEM_CONFIG_2)[0]&0xFB]) - def random(self): - return self.SPIRead(self.REG_RSSI_WIDEBAND)[0] - - def explicitHeaderMode(self): - self._implicitHeaderMode=0 - self.SPIWrite(self.REG_MODEM_CONFIG_1,[self.SPIRead(self.REG_MODEM_CONFIG_1)[0]&0xFE]) - - def implicitHeaderMode(self): - self._implicitHeaderMode=1 - self.SPIWrite(self.REG_MODEM_CONFIG_1,[self.SPIRead(self.REG_MODEM_CONFIG_1)[0]|0x01]) - - def handleDio0Rise(self): - irqFlags = self.SPIRead(self.REG_IRQ_FLAGS,1)[0] - self.SPIWrite(self.REG_IRQ_FLAGS,[irqFlags]) - - if (irqFlags & self.IRQ_PAYLOAD_CRC_ERROR_MASK)==0 : - self._packetIndex = 0 - if self._implicitHeaderMode: - self.packetLength = self.SPIRead(self.REG_PAYLOAD_LENGTH,1)[0] - else: - self.packetLength = self.SPIRead(self.REG_RX_NB_BYTES,1)[0] - - self.SPIWrite(self.REG_FIFO_ADDR_PTR,self.SPIRead(self.REG_FIFO_RX_CURRENT_ADDR,1)) - if self._onReceive: - print self.packetLength - #self._onReceive(self.packetLength) - - self.SPIWrite(self.REG_FIFO_ADDR_PTR,[0]) - - def SPIWrite(self,adr,byteArray): - return self.SPI.xfer('CS1',[0x80|adr]+byteArray)[1:] - - def SPIRead(self,adr,total_bytes=1): - return self.SPI.xfer('CS1',[adr]+[0]*total_bytes)[1:] - - def getRaw(self): - val = self.SPIRead(0x02,1) - return val - -if __name__ == "__main__": - RX = 0; TX=1 - mode = RX - from PSL import sciencelab - I= sciencelab.connect() - lora = SX1276(I.SPI,434e6,boost=True,power=17,BW=125e3,SF=12,CR=5) #settings for maximum range - lora.crc() - cntr=0 - while 1: - time.sleep(0.01) - if mode==TX: - lora.beginPacket() - lora.write([cntr]) - #lora.write([ord(a) for a in ":"]+[cntr]) - print (time.ctime(),[ord(a) for a in ":"]+[cntr], hex(lora.SPIRead(lora.REG_OP_MODE)[0])) - lora.endPacket() - cntr+=1 - if cntr==255:cntr=0 - elif mode==RX: - packet_size = lora.parsePacket() - if packet_size: - print 'data',lora.readAll() - print ('Rssi',lora.packetRssi(),lora.packetSnr()) - diff --git a/PSL/SENSORS/__init__.py b/PSL/SENSORS/__init__.py deleted file mode 100644 index 8b137891..00000000 --- a/PSL/SENSORS/__init__.py +++ /dev/null @@ -1 +0,0 @@ - diff --git a/PSL/__init__.py b/PSL/__init__.py deleted file mode 100644 index 8b137891..00000000 --- a/PSL/__init__.py +++ /dev/null @@ -1 +0,0 @@ - diff --git a/PSL/achan.py b/PSL/achan.py deleted file mode 100644 index fb7a5fdb..00000000 --- a/PSL/achan.py +++ /dev/null @@ -1,197 +0,0 @@ -from __future__ import print_function -import numpy as np - -gains = [1, 2, 4, 5, 8, 10, 16, 32, 1 / 11.] - -# -----------------------Classes for input sources---------------------- -allAnalogChannels = ['CH1', 'CH2', 'CH3', 'MIC', 'CAP', 'SEN', 'AN8'] - -bipolars = ['CH1', 'CH2', 'CH3', 'MIC'] - -inputRanges = {'CH1': [16.5, -16.5], # Specify inverted channels explicitly by reversing range!!!!!!!!! - 'CH2': [16.5, -16.5], - 'CH3': [-3.3, 3.3], # external gain control analog input - 'MIC': [-3.3, 3.3], # connected to MIC amplifier - 'CAP': [0, 3.3], - 'SEN': [0, 3.3], - 'AN8': [0, 3.3] - } - -picADCMultiplex = {'CH1': 3, 'CH2': 0, 'CH3': 1, 'MIC': 2, 'AN4': 4, 'SEN': 7, 'CAP': 5, 'AN8': 8, } - - -class analogInputSource: - gain_values = gains - gainEnabled = False - gain = None - gainPGA = None - inverted = False - inversion = 1. - calPoly10 = np.poly1d([0, 3.3 / 1023, 0.]) - calPoly12 = np.poly1d([0, 3.3 / 4095, 0.]) - calibrationReady = False - defaultOffsetCode = 0 - - def __init__(self, name, **args): - self.name = name # The generic name of the input. like 'CH1', 'IN1' etc - self.CHOSA = picADCMultiplex[self.name] - self.adc_shifts = [] - self.polynomials = {} - - self.R = inputRanges[name] - - if self.R[1] - self.R[0] < 0: - self.inverted = True - self.inversion = -1 - - self.scaling = 1. - if name == 'CH1': - self.gainEnabled = True - self.gainPGA = 1 - self.gain = 0 # This is not the gain factor. use self.gain_values[self.gain] to find that. - elif name == 'CH2': - self.gainEnabled = True - self.gainPGA = 2 - self.gain = 0 - else: - pass - - self.gain = 0 - self.regenerateCalibration() - - def setGain(self, g): - if not self.gainEnabled: - print('Analog gain is not available on', self.name) - return False - self.gain = self.gain_values.index(g) - self.regenerateCalibration() - - def inRange(self, val): - v = self.voltToCode12(val) - return 0 <= v <= 4095 - - def __conservativeInRange__(self, val): - v = self.voltToCode12(val) - return 50 <= v <= 4000 - - def loadCalibrationTable(self, table, slope, intercept): - self.adc_shifts = np.array(table) * slope - intercept - - def __ignoreCalibration__(self): - self.calibrationReady = False - - def loadPolynomials(self, polys): - for a in range(len(polys)): - epoly = [float(b) for b in polys[a]] - self.polynomials[a] = np.poly1d(epoly) - - def regenerateCalibration(self): - B = self.R[1] - A = self.R[0] - intercept = self.R[0] - - if self.gain is not None: - gain = self.gain_values[self.gain] - B /= gain - A /= gain - - slope = B - A - intercept = A - if self.calibrationReady and self.gain != 8: # special case for 1/11. gain - self.calPoly10 = self.__cal10__ - self.calPoly12 = self.__cal12__ - - else: - self.calPoly10 = np.poly1d([0, slope / 1023., intercept]) - self.calPoly12 = np.poly1d([0, slope / 4095., intercept]) - - self.voltToCode10 = np.poly1d([0, 1023. / slope, -1023 * intercept / slope]) - self.voltToCode12 = np.poly1d([0, 4095. / slope, -4095 * intercept / slope]) - - def __cal12__(self, RAW): - avg_shifts = (self.adc_shifts[np.int16(np.floor(RAW))] + self.adc_shifts[np.int16(np.ceil(RAW))]) / 2. - RAW -= 4095 * avg_shifts / 3.3 - return self.polynomials[self.gain](RAW) - - def __cal10__(self, RAW): - RAW *= 4095 / 1023. - avg_shifts = (self.adc_shifts[np.int16(np.floor(RAW))] + self.adc_shifts[np.int16(np.ceil(RAW))]) / 2. - RAW -= 4095 * avg_shifts / 3.3 - return self.polynomials[self.gain](RAW) - - -''' -for a in ['CH1']: - x=analogInputSource(a) - print (x.name,x.calPoly10#,calfacs[x.name][0]) - print ('CAL:',x.calPoly10(0),x.calPoly10(1023)) - x.setOffset(1.65) - x.setGain(32) - print (x.name,x.calPoly10#,calfacs[x.name][0]) - print ('CAL:',x.calPoly10(0),x.calPoly10(1023)) -''' - - -# --------------------------------------------------------------------- - - - -class analogAcquisitionChannel: - """ - This class takes care of oscilloscope data fetched from the device. - Each instance may be linked to a particular input. - Since only up to two channels may be captured at a time with the PSLab, only two instances will be required - - Each instance will be linked to a particular inputSource instance by the capture routines. - When data is requested , it will return after applying calibration and gain details - stored in the selected inputSource - """ - - def __init__(self, a): - self.name = '' - self.gain = 0 - self.channel = a - self.channel_names = allAnalogChannels - # REFERENCE VOLTAGE = 3.3 V - self.calibration_ref196 = 1. # measured reference voltage/3.3 - self.resolution = 10 - self.xaxis = np.zeros(10000) - self.yaxis = np.zeros(10000) - self.length = 100 - self.timebase = 1. - self.source = analogInputSource('CH1') # use CH1 for initialization. It will be overwritten by set_params - - def fix_value(self, val): - # val[val>1020]=np.NaN - # val[val<2]=np.NaN - if self.resolution == 12: - return self.calibration_ref196 * self.source.calPoly12(val) - else: - return self.calibration_ref196 * self.source.calPoly10(val) - - def set_yval(self, pos, val): - self.yaxis[pos] = self.fix_value(val) - - def set_xval(self, pos, val): - self.xaxis[pos] = val - - def set_params(self, **keys): - self.gain = keys.get('gain', self.gain) - self.name = keys.get('channel', self.channel) - self.source = keys.get('source', self.source) - self.resolution = keys.get('resolution', self.resolution) - l = keys.get('length', self.length) - t = keys.get('timebase', self.timebase) - if t != self.timebase or l != self.length: - self.timebase = t - self.length = l - self.regenerate_xaxis() - - def regenerate_xaxis(self): - for a in range(int(self.length)): self.xaxis[a] = self.timebase * a - - def get_xaxis(self): - return self.xaxis[:self.length] - - def get_yaxis(self): - return self.yaxis[:self.length] diff --git a/PSL/analyticsClass.py b/PSL/analyticsClass.py deleted file mode 100644 index eb91de39..00000000 --- a/PSL/analyticsClass.py +++ /dev/null @@ -1,301 +0,0 @@ -from __future__ import print_function -import time - -import numpy as np - - -class analyticsClass(): - """ - This class contains methods that allow mathematical analysis such as curve fitting - """ - - def __init__(self): - try: - import scipy.optimize as optimize - except ImportError: - self.optimize = None - else: - self.optimize = optimize - - try: - import scipy.fftpack as fftpack - except ImportError: - self.fftpack = None - else: - self.fftpack = fftpack - - try: - from scipy.optimize import leastsq - except ImportError: - self.leastsq = None - else: - self.leastsq = leastsq - - try: - import scipy.signal as signal - except ImportError: - self.signal = None - else: - self.signal = signal - - try: - from PSL.commands_proto import applySIPrefix as applySIPrefix - except ImportError: - self.applySIPrefix = None - else: - self.applySIPrefix = applySIPrefix - - def sineFunc(self, x, a1, a2, a3, a4): - return a4 + a1 * np.sin(abs(a2 * (2 * np.pi)) * x + a3) - - def squareFunc(self, x, amp, freq, phase, dc, offset): - return offset + amp * self.signal.square(2 * np.pi * freq * (x - phase), duty=dc) - - # -------------------------- Exponential Fit ---------------------------------------- - - def func(self, x, a, b, c): - return a * np.exp(-x / b) + c - - def fit_exp(self, t, v): # accepts numpy arrays - size = len(t) - v80 = v[0] * 0.8 - for k in range(size - 1): - if v[k] < v80: - rc = t[k] / .223 - break - pg = [v[0], rc, 0] - po, err = self.optimize.curve_fit(self.func, t, v, pg) - if abs(err[0][0]) > 0.1: - return None, None - vf = po[0] * np.exp(-t / po[1]) + po[2] - return po, vf - - def squareFit(self, xReal, yReal): - N = len(xReal) - mx = yReal.max() - mn = yReal.min() - OFFSET = (mx + mn) / 2. - amplitude = (np.average(yReal[yReal > OFFSET]) - np.average(yReal[yReal < OFFSET])) / 2.0 - yTmp = np.select([yReal < OFFSET, yReal > OFFSET], [0, 2]) - bools = abs(np.diff(yTmp)) > 1 - edges = xReal[bools] - levels = yTmp[bools] - frequency = 1. / (edges[2] - edges[0]) - - phase = edges[0] # .5*np.pi*((yReal[0]-offset)/amplitude) - dc = 0.5 - if len(edges) >= 4: - if levels[0] == 0: - dc = (edges[1] - edges[0]) / (edges[2] - edges[0]) - else: - dc = (edges[2] - edges[1]) / (edges[3] - edges[1]) - phase = edges[1] - - guess = [amplitude, frequency, phase, dc, 0] - - try: - (amplitude, frequency, phase, dc, offset), pcov = self.optimize.curve_fit(self.squareFunc, xReal, - yReal - OFFSET, guess) - offset += OFFSET - - if (frequency < 0): - # print ('negative frq') - return False - - freq = 1e6 * abs(frequency) - amp = abs(amplitude) - pcov[0] *= 1e6 - # print (pcov) - if (abs(pcov[-1][0]) > 1e-6): - False - return [amp, freq, phase, dc, offset] - except: - return False - - def sineFit(self, xReal, yReal, **kwargs): - N = len(xReal) - OFFSET = (yReal.max() + yReal.min()) / 2. - yhat = self.fftpack.rfft(yReal - OFFSET) - idx = (yhat ** 2).argmax() - freqs = self.fftpack.rfftfreq(N, d=(xReal[1] - xReal[0]) / (2 * np.pi)) - frequency = kwargs.get('freq', freqs[idx]) - frequency /= (2 * np.pi) # Convert angular velocity to freq - amplitude = kwargs.get('amp', (yReal.max() - yReal.min()) / 2.0) - phase = kwargs.get('phase', 0) # .5*np.pi*((yReal[0]-offset)/amplitude) - guess = [amplitude, frequency, phase, 0] - try: - (amplitude, frequency, phase, offset), pcov = self.optimize.curve_fit(self.sineFunc, xReal, yReal - OFFSET, - guess) - offset += OFFSET - ph = ((phase) * 180 / (np.pi)) - if (frequency < 0): - # print ('negative frq') - return False - - if (amplitude < 0): - ph -= 180 - - if (ph < 0): - ph = (ph + 720) % 360 - - freq = 1e6 * abs(frequency) - amp = abs(amplitude) - pcov[0] *= 1e6 - # print (pcov) - if (abs(pcov[-1][0]) > 1e-6): - return False - return [amp, freq, offset, ph] - except: - return False - - def find_frequency(self, v, si): # voltages, samplimg interval is seconds - from numpy import fft - NP = len(v) - v = v - v.mean() # remove DC component - frq = fft.fftfreq(NP, si)[:NP / 2] # take only the +ive half of the frequncy array - amp = abs(fft.fft(v)[:NP / 2]) / NP # and the fft result - index = amp.argmax() # search for the tallest peak, the fundamental - return frq[index] - - def sineFit2(self, x, y): - freq = self.find_frequency(y, x[1] - x[0]) - amp = (y.max() - y.min()) / 2.0 - guess = [amp, freq, 0, 0] # amplitude, freq, phase,offset - # print (guess) - OS = y.mean() - try: - par, pcov = self.optimize.curve_fit(self.sineFunc, x, y - OS, guess) - except: - return None - vf = self.sineFunc(t, par[0], par[1], par[2], par[3]) - diff = sum((v - vf) ** 2) / max(v) - if diff > self.error_limit: - guess[2] += np.pi / 2 # try an out of phase - try: - # print 'L1: diff = %5.0f frset= %6.3f fr = %6.2f phi = %6.2f'%(diff, res,par[1]*1e6,par[2]) - par, pcov = self.optimize.curve_fit(self.sineFunc, x, y, guess) - except: - return None - vf = self.sineFunc(t, par[0], par[1], par[2], par[3]) - diff = sum((v - vf) ** 2) / max(v) - if diff > self.error_limit: - # print 'L2: diff = %5.0f frset= %6.3f fr = %6.2f phi = %6.2f'%(diff, res,par[1]*1e6,par[2]) - return None - else: - pass - # print 'fixed ',par[1]*1e6 - return par, vf - - def amp_spectrum(self, v, si, nhar=8): - # voltages, samplimg interval is seconds, number of harmonics to retain - from numpy import fft - NP = len(v) - frq = fft.fftfreq(NP, si)[:NP / 2] # take only the +ive half of the frequncy array - amp = abs(fft.fft(v)[:NP / 2]) / NP # and the fft result - index = amp.argmax() # search for the tallest peak, the fundamental - if index == 0: # DC component is dominating - index = amp[4:].argmax() # skip frequencies close to zero - return frq[:index * nhar], amp[:index * nhar] # restrict to 'nhar' harmonics - - def dampedSine(self, x, amp, freq, phase, offset, damp): - """ - A damped sine wave function - - """ - return offset + amp * np.exp(-damp * x) * np.sin(abs(freq) * x + phase) - - def getGuessValues(self, xReal, yReal, func='sine'): - if (func == 'sine' or func == 'damped sine'): - N = len(xReal) - offset = np.average(yReal) - yhat = self.fftpack.rfft(yReal - offset) - idx = (yhat ** 2).argmax() - freqs = self.fftpack.rfftfreq(N, d=(xReal[1] - xReal[0]) / (2 * np.pi)) - frequency = freqs[idx] - - amplitude = (yReal.max() - yReal.min()) / 2.0 - phase = 0. - if func == 'sine': - return amplitude, frequency, phase, offset - if func == 'damped sine': - return amplitude, frequency, phase, offset, 0 - - def arbitFit(self, xReal, yReal, func, **args): - N = len(xReal) - guess = args.get('guess', []) - try: - results, pcov = self.optimize.curve_fit(func, xReal, yReal, guess) - pcov[0] *= 1e6 - return True, results, pcov - except: - return False, [], [] - - def fft(self, ya, si): - ''' - Returns positive half of the Fourier transform of the signal ya. - Sampling interval 'si', in milliseconds - ''' - ns = len(ya) - if ns % 2 == 1: # odd values of np give exceptions - ns -= 1 # make it even - ya = ya[:-1] - v = np.array(ya) - tr = abs(np.fft.fft(v)) / ns - frq = np.fft.fftfreq(ns, si) - x = frq.reshape(2, ns / 2) - y = tr.reshape(2, ns / 2) - return x[0], y[0] - - def sineFitAndDisplay(self, chan, displayObject): - ''' - chan : an object containing a get_xaxis, and a get_yaxis method. - displayObject : an object containing a setValue method - - Fits against a sine function, and writes to the object - ''' - fitres = None - fit = '' - try: - fitres = self.sineFit(chan.get_xaxis(), chan.get_yaxis()) - if fitres: - amp, freq, offset, phase = fitres - if amp > 0.05: fit = 'Voltage=%s\nFrequency=%s' % ( - self.applySIPrefix(amp, 'V'), self.applySIPrefix(freq, 'Hz')) - except Exception as e: - fires = None - - if not fitres or len(fit) == 0: fit = 'Voltage=%s\n' % (self.applySIPrefix(np.average(chan.get_yaxis()), 'V')) - displayObject.setValue(fit) - if fitres: - return fitres - else: - return 0, 0, 0, 0 - - def rmsAndDisplay(self, data, displayObject): - ''' - data : an array of numbers - displayObject : an object containing a setValue method - - Fits against a sine function, and writes to the object - ''' - rms = self.RMS(data) - displayObject.setValue('Voltage=%s' % (self.applySIPrefix(rms, 'V'))) - return rms - - def RMS(self, data): - data = np.array(data) - return np.sqrt(np.average(data * data)) - - def butter_notch(self, lowcut, highcut, fs, order=5): - from scipy.signal import butter - nyq = 0.5 * fs - low = lowcut / nyq - high = highcut / nyq - b, a = butter(order, [low, high], btype='bandstop') - return b, a - - def butter_notch_filter(self, data, lowcut, highcut, fs, order=5): - from scipy.signal import lfilter - b, a = self.butter_notch(lowcut, highcut, fs, order=order) - y = lfilter(b, a, data) - return y diff --git a/PSL/digital_channel.py b/PSL/digital_channel.py deleted file mode 100644 index ef8c0396..00000000 --- a/PSL/digital_channel.py +++ /dev/null @@ -1,120 +0,0 @@ -from __future__ import print_function -import numpy as np - -digital_channel_names = ['ID1', 'ID2', 'ID3', 'ID4', 'SEN', 'EXT', 'CNTR'] - - -class digital_channel: - EVERY_SIXTEENTH_RISING_EDGE = 5 - EVERY_FOURTH_RISING_EDGE = 4 - EVERY_RISING_EDGE = 3 - EVERY_FALLING_EDGE = 2 - EVERY_EDGE = 1 - DISABLED = 0 - - def __init__(self, a): - self.gain = 0 - self.channel_number = a - self.digital_channel_names = digital_channel_names - self.name = self.digital_channel_names[a] - self.xaxis = np.zeros(20000) - self.yaxis = np.zeros(20000) - self.timestamps = np.zeros(10000) - self.length = 100 - self.initial_state = 0 - self.prescaler = 0 - self.datatype = 'int' - self.trigger = 0 - self.dlength = 0 - self.plot_length = 0 - self.maximum_time = 0 - self.maxT = 0 - self.initial_state_override = False - self.mode = self.EVERY_EDGE - - def set_params(self, **keys): - self.channel_number = keys.get('channel_number', self.channel_number) - self.name = keys.get('name', 'ErrOr') - - def load_data(self, initial_state, timestamps): - if self.initial_state_override: - self.initial_state = (self.initial_state_override - 1) == 1 - self.initial_state_override = False - else: - self.initial_state = initial_state[self.name] - self.timestamps = timestamps - self.dlength = len(self.timestamps) - # print('dchan.py',self.channel_number,self.name,initial_state,self.initial_state) - self.timestamps = np.array(self.timestamps) * [1. / 64, 1. / 8, 1., 4.][self.prescaler] - - if self.dlength: - self.maxT = self.timestamps[-1] - else: - self.maxT = 0 - - def generate_axes(self): - HIGH = 1 # (4-self.channel_number)*(3) - LOW = 0 # HIGH - 2.5 - state = HIGH if self.initial_state else LOW - - if self.mode == self.DISABLED: - self.xaxis[0] = 0 - self.yaxis[0] = state - n = 1 - self.plot_length = n - - elif self.mode == self.EVERY_EDGE: - self.xaxis[0] = 0 - self.yaxis[0] = state - n = 1 - for a in range(self.dlength): - self.xaxis[n] = self.timestamps[a] - self.yaxis[n] = state - state = LOW if state == HIGH else HIGH - n += 1 - self.xaxis[n] = self.timestamps[a] - self.yaxis[n] = state - n += 1 - - self.plot_length = n - - elif self.mode == self.EVERY_FALLING_EDGE: - self.xaxis[0] = 0 - self.yaxis[0] = HIGH - n = 1 - for a in range(self.dlength): - self.xaxis[n] = self.timestamps[a] - self.yaxis[n] = HIGH - n += 1 - self.xaxis[n] = self.timestamps[a] - self.yaxis[n] = LOW - n += 1 - self.xaxis[n] = self.timestamps[a] - self.yaxis[n] = HIGH - n += 1 - state = HIGH - self.plot_length = n - - elif self.mode == self.EVERY_RISING_EDGE or self.mode == self.EVERY_FOURTH_RISING_EDGE or self.mode == self.EVERY_SIXTEENTH_RISING_EDGE: - self.xaxis[0] = 0 - self.yaxis[0] = LOW - n = 1 - for a in range(self.dlength): - self.xaxis[n] = self.timestamps[a] - self.yaxis[n] = LOW - n += 1 - self.xaxis[n] = self.timestamps[a] - self.yaxis[n] = HIGH - n += 1 - self.xaxis[n] = self.timestamps[a] - self.yaxis[n] = LOW - n += 1 - state = LOW - self.plot_length = n - # print(self.channel_number,self.dlength,self.mode,len(self.yaxis),self.plot_length) - - def get_xaxis(self): - return self.xaxis[:self.plot_length] - - def get_yaxis(self): - return self.yaxis[:self.plot_length] diff --git a/PSL/packet_handler.py b/PSL/packet_handler.py deleted file mode 100644 index a4f6d79f..00000000 --- a/PSL/packet_handler.py +++ /dev/null @@ -1,243 +0,0 @@ -from __future__ import print_function -import time - -import PSL.commands_proto as CP -import serial, subprocess, inspect - - -class Handler(): - def __init__(self, timeout=1.0, **kwargs): - self.burstBuffer = b'' - self.loadBurst = False - self.inputQueueSize = 0 - self.BAUD = 1000000 - self.timeout = timeout - self.version_string = b'' - self.connected = False - self.fd = None - self.expected_version1 = b'CS' - self.expected_version2 = b'PS' - self.occupiedPorts = set() - self.blockingSocket = None - if 'port' in kwargs: - self.portname = kwargs.get('port', None) - try: - self.fd, self.version_string, self.connected = self.connectToPort(self.portname) - if self.connected: return - except Exception as ex: - print('Failed to connect to ', self.portname, ex.message) - - else: # Scan and pick a port - L = self.listPorts() - for a in L: - try: - self.portname = a - self.fd, self.version_string, self.connected = self.connectToPort(self.portname) - if self.connected: - print(a + ' .yes.', self.version_string) - return - except: - pass - if not self.connected: - if len(self.occupiedPorts): print('Device not found. Programs already using :', self.occupiedPorts) - - def listPorts(self): - import platform, glob - system_name = platform.system() - if system_name == "Windows": - # Scan for available ports. - available = [] - for i in range(256): - try: - s = serial.Serial(i) - available.append(i) - s.close() - except serial.SerialException: - pass - return available - elif system_name == "Darwin": - # Mac - return glob.glob('/dev/tty*') + glob.glob('/dev/cu*') - else: - # Assume Linux or something else - return glob.glob('/dev/ttyACM*') + glob.glob('/dev/ttyUSB*') - - def connectToPort(self, portname): - import platform - if platform.system() not in ["Windows", "Darwin"]: - import socket - try: - self.blockingSocket = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) - self.blockingSocket.bind('\0PSghhhLab%s' % portname) - self.blockingSocket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) - except socket.error as e: - self.occupiedPorts.add(portname) - raise RuntimeError("Another program is using %s (%d)" % (portname)) - - fd = serial.Serial(portname, 9600, stopbits=1, timeout=0.02) - fd.read(100) - fd.close() - fd = serial.Serial(portname, self.BAUD, stopbits=1, timeout=1.0) - if (fd.inWaiting()): - fd.setTimeout(0.1) - fd.read(1000) - fd.flush() - fd.setTimeout(1.0) - version = self.get_version(fd) - if version[:len(self.expected_version1)] == self.expected_version1 or version[:len(self.expected_version2)] == self.expected_version2: - return fd, version, True - - return None, '', False - - def disconnect(self): - if self.connected: - self.fd.close() - if self.blockingSocket: - print('Releasing port') - self.blockingSocket.shutdown(1) - self.blockingSocket.close() - self.blockingSocket = None - - def get_version(self, fd): - fd.write(CP.COMMON) - fd.write(CP.GET_VERSION) - x = fd.readline() - # print('remaining',[ord(a) for a in fd.read(10)]) - if len(x): - x = x[:-1] - return x - - def reconnect(self, **kwargs): - if 'port' in kwargs: - self.portname = kwargs.get('port', None) - - try: - self.fd, self.version_string, self.connected = self.connectToPort(self.portname) - except serial.SerialException as ex: - msg = "failed to reconnect. Check device connections." - print(msg) - raise RuntimeError(msg) - - def __del__(self): - # print('closing port') - try: - self.fd.close() - except: - pass - - def __get_ack__(self): - """ - fetches the response byte - 1 SUCCESS - 2 ARGUMENT_ERROR - 3 FAILED - used as a handshake - """ - if not self.loadBurst: - x = self.fd.read(1) - else: - self.inputQueueSize += 1 - return 1 - try: - return CP.Byte.unpack(x)[0] - except: - return 3 - - def __sendInt__(self, val): - """ - transmits an integer packaged as two characters - :params int val: int to send - """ - if not self.loadBurst: - self.fd.write(CP.ShortInt.pack(int(val))) - else: - self.burstBuffer += CP.ShortInt.pack(int(val)) - - def __sendByte__(self, val): - """ - transmits a BYTE - val - byte to send - """ - # print (val) - if (type(val) == int): - if not self.loadBurst: - self.fd.write(CP.Byte.pack(val)) - else: - self.burstBuffer += CP.Byte.pack(val) - else: - if not self.loadBurst: - self.fd.write(val) - else: - self.burstBuffer += val - - def __getByte__(self): - """ - reads a byte from the serial port and returns it - """ - ss = self.fd.read(1) - try: - if len(ss): - return CP.Byte.unpack(ss)[0] - except Exception as ex: - print('byte communication error.', time.ctime()) - self.raiseException(ex, "Communication Error , Function : " + inspect.currentframe().f_code.co_name) - # sys.exit(1) - - def __getInt__(self): - """ - reads two bytes from the serial port and - returns an integer after combining them - """ - ss = self.fd.read(2) - try: - if len(ss) == 2: - return CP.ShortInt.unpack(ss)[0] - except Exception as ex: - print('int communication error.', time.ctime()) - self.raiseException(ex, "Communication Error , Function : " + inspect.currentframe().f_code.co_name) - # sys.exit(1) - - def __getLong__(self): - """ - reads four bytes. - returns long - """ - ss = self.fd.read(4) - if len(ss) == 4: - return CP.Integer.unpack(ss)[0] - else: - # print('.') - return -1 - - def waitForData(self, timeout=0.2): - start_time = time.time() - while time.time() - start_time < timeout: - time.sleep(0.02) - if self.fd.inWaiting(): return True - return False - - def sendBurst(self): - """ - Transmits the commands stored in the burstBuffer. - empties input buffer - empties the burstBuffer. - - The following example initiates the capture routine and sets OD1 HIGH immediately. - - It is used by the Transient response experiment where the input needs to be toggled soon - after the oscilloscope has been started. - - >>> I.loadBurst=True - >>> I.capture_traces(4,800,2) - >>> I.set_state(I.OD1,I.HIGH) - >>> I.sendBurst() - - - """ - # print([Byte.unpack(a)[0] for a in self.burstBuffer],self.inputQueueSize) - self.fd.write(self.burstBuffer) - self.burstBuffer = '' - self.loadBurst = False - acks = self.fd.read(self.inputQueueSize) - self.inputQueueSize = 0 - return [CP.Byte.unpack(a)[0] for a in acks] diff --git a/PSL/sciencelab.py b/PSL/sciencelab.py deleted file mode 100644 index c1b853cd..00000000 --- a/PSL/sciencelab.py +++ /dev/null @@ -1,4129 +0,0 @@ -# -*- coding: utf-8 -*- -# Communication Library for Pocket Science Lab from FOSSASIA -# -# License : GNU GPL - -from __future__ import print_function -import os, time - -import PSL.commands_proto as CP -import PSL.packet_handler as packet_handler - -from PSL.achan import * -from PSL.digital_channel import * -import serial, string, inspect -import time -import sys -import numpy as np -import math - - -def connect(**kwargs): - ''' - If hardware is found, returns an instance of 'ScienceLab', else returns None. - ''' - obj = ScienceLab(**kwargs) - if obj.H.fd is not None: - return obj - else: - print('Err') - raise RuntimeError('Could Not Connect') - - -class ScienceLab(): - """ - **Communications library.** - - This class contains methods that can be used to interact with the FOSSASIA PSLab - - Initialization does the following - - * connects to tty device - * loads calibration values. - - .. tabularcolumns:: |p{3cm}|p{11cm}| - - +----------+-----------------------------------------------------------------+ - |Arguments |Description | - +==========+=================================================================+ - |timeout | serial port read timeout. default = 1s | - +----------+-----------------------------------------------------------------+ - - >>> from PSL import sciencelab - >>> I = sciencelab.connect() - >>> self.__print__(I) - - - - Once you have initiated this class, its various methods will allow access to all the features built - into the device. - - - - """ - - CAP_AND_PCS = 0 - ADC_SHIFTS_LOCATION1 = 1 - ADC_SHIFTS_LOCATION2 = 2 - ADC_POLYNOMIALS_LOCATION = 3 - - # DAC_POLYNOMIALS_LOCATION=1 - DAC_SHIFTS_PV1A = 4 - DAC_SHIFTS_PV1B = 5 - DAC_SHIFTS_PV2A = 6 - DAC_SHIFTS_PV2B = 7 - DAC_SHIFTS_PV3A = 8 - DAC_SHIFTS_PV3B = 9 - LOC_DICT = {'PV1': [4, 5], 'PV2': [6, 7], 'PV3': [8, 9]} - BAUD = 1000000 - WType = {'W1': 'sine', 'W2': 'sine'} - - def __init__(self, timeout=1.0, **kwargs): - self.verbose = kwargs.get('verbose', False) - self.initialArgs = kwargs - self.generic_name = 'PSLab' - self.DDS_CLOCK = 0 - self.timebase = 40 - self.MAX_SAMPLES = CP.MAX_SAMPLES - self.samples = self.MAX_SAMPLES - self.triggerLevel = 550 - self.triggerChannel = 0 - self.error_count = 0 - self.channels_in_buffer = 0 - self.digital_channels_in_buffer = 0 - self.currents = [0.55e-3, 0.55e-6, 0.55e-5, 0.55e-4] - self.currentScalers = [1.0, 1.0, 1.0, 1.0] - - self.data_splitting = kwargs.get('data_splitting', CP.DATA_SPLITTING) - self.allAnalogChannels = allAnalogChannels - self.analogInputSources = {} - for a in allAnalogChannels: self.analogInputSources[a] = analogInputSource(a) - - self.sine1freq = None - self.sine2freq = None - self.sqrfreq = {'SQR1': None, 'SQR2': None, 'SQR3': None, 'SQR4': None} - self.aboutArray = [] - self.errmsg = '' - # --------------------------Initialize communication handler, and subclasses----------------- - try: - self.H = packet_handler.Handler(**kwargs) - except Exception as ex: - self.errmsg = "failed to Connect. Please check connections/arguments\n" + ex.message - self.connected = False - print(self.errmsg) # raise RuntimeError(msg) - - try: - self.__runInitSequence__(**kwargs) - except Exception as ex: - self.errmsg = "failed to run init sequence. Check device connections\n" + str(ex) - self.connected = False - print(self.errmsg) # raise RuntimeError(msg) - - def __runInitSequence__(self, **kwargs): - self.aboutArray = [] - from PSL.Peripherals import I2C, SPI, NRF24L01, MCP4728, RadioLink - self.connected = self.H.connected - if not self.H.connected: - self.__print__('Check hardware connections. Not connected') - - self.streaming = False - self.achans = [analogAcquisitionChannel(a) for a in ['CH1', 'CH2', 'CH3', 'MIC']] - self.gain_values = gains - self.buff = np.zeros(10000) - self.SOCKET_CAPACITANCE = 42e-12 # 42e-12 is typical for the FOSSASIA PSLab. Actual values will be updated during calibration loading - self.resistanceScaling = 1. - - self.digital_channel_names = digital_channel_names - self.allDigitalChannels = self.digital_channel_names - self.gains = {'CH1': 0, 'CH2': 0} - - # This array of four instances of digital_channel is used to store data retrieved from the - # logic analyzer section of the device. It also contains methods to generate plottable data - # from the original timestamp arrays. - self.dchans = [digital_channel(a) for a in range(4)] - - self.I2C = I2C(self.H) - # self.I2C.pullSCLLow(5000) - self.SPI = SPI(self.H) - self.hexid = '' - if self.H.connected: - for a in ['CH1', 'CH2']: self.set_gain(a, 0, True) # Force load gain - for a in ['W1', 'W2']: self.load_equation(a, 'sine') - self.SPI.set_parameters(1, 7, 1, 0) - self.hexid = hex(self.device_id()) - - self.NRF = NRF24L01(self.H) - - self.aboutArray.append(['Radio Transceiver is :', 'Installed' if self.NRF.ready else 'Not Installed']) - - self.DAC = MCP4728(self.H, 3.3, 0) - self.calibrated = False - # -------Check for calibration data if connected. And process them if found--------------- - if kwargs.get('load_calibration', True) and self.H.connected: - import struct - # Load constants for CTMU and PCS - cap_and_pcs = self.read_bulk_flash(self.CAP_AND_PCS, 8 * 4 + 5) # READY+calibration_string - if cap_and_pcs[:5] == 'READY': - scalers = list(struct.unpack('8f', cap_and_pcs[5:])) - self.SOCKET_CAPACITANCE = scalers[0] - self.DAC.CHANS['PCS'].load_calibration_twopoint(scalers[1], - scalers[2]) # Slope and offset for current source - self.__calibrate_ctmu__(scalers[4:]) - self.resistanceScaling = scalers[3] # SEN - self.aboutArray.append(['Capacitance[sock,550uA,55uA,5.5uA,.55uA]'] + scalers[:1] + scalers[4:]) - self.aboutArray.append(['PCS slope,offset'] + scalers[1:3]) - self.aboutArray.append(['SEN'] + [scalers[3]]) - else: - self.SOCKET_CAPACITANCE = 42e-12 # approx - self.__print__('Cap and PCS calibration invalid') # ,cap_and_pcs[:10],'...') - - # Load constants for ADC and DAC - polynomials = self.read_bulk_flash(self.ADC_POLYNOMIALS_LOCATION, 2048) - polyDict = {} - if polynomials[:9] == 'PSLab': - self.__print__('ADC calibration found...') - self.aboutArray.append(['Calibration Found']) - self.aboutArray.append([]) - self.calibrated = True - adc_shifts = self.read_bulk_flash(self.ADC_SHIFTS_LOCATION1, 2048) + self.read_bulk_flash( - self.ADC_SHIFTS_LOCATION2, 2048) - adc_shifts = [CP.Byte.unpack(a)[0] for a in adc_shifts] - # print(adc_shifts) - self.__print__('ADC INL correction table loaded.') - self.aboutArray.append(['ADC INL Correction found', adc_shifts[0], adc_shifts[1], adc_shifts[2], '...']) - poly_sections = polynomials.split( - 'STOP') # The 2K array is split into sections containing data for ADC_INL fit, ADC_CHANNEL fit, DAC_CHANNEL fit, PCS, CAP ... - - adc_slopes_offsets = poly_sections[0] - dac_slope_intercept = poly_sections[1] - inl_slope_intercept = poly_sections[2] - # print('COMMON#########',self.__stoa__(slopes_offsets)) - # print('DAC#########',self.__stoa__(dac_slope_intercept)) - # print('ADC INL ############',self.__stoa__(inl_slope_intercept),len(inl_slope_intercept)) - # Load calibration data for ADC channels into an array that'll be evaluated in the next code block - for a in adc_slopes_offsets.split('>|')[1:]: - self.__print__('\n', '>' * 20, a[:3], '<' * 20) - self.aboutArray.append([]) - self.aboutArray.append(['ADC Channel', a[:3]]) - self.aboutArray.append(['Gain', 'X^3', 'X^2', 'X', 'C']) - cals = a[5:] - polyDict[a[:3]] = [] - for b in range(len(cals) // 16): - try: - poly = struct.unpack('4f', cals[b * 16:(b + 1) * 16]) - except: - self.__print__(a[:3], ' not calibrated') - self.__print__(b, poly) - self.aboutArray.append([b] + ['%.3e' % v for v in poly]) - polyDict[a[:3]].append(poly) - - # Load calibration data (slopes and offsets) for ADC channels - inl_slope_intercept = struct.unpack('2f', inl_slope_intercept) - for a in self.analogInputSources: - self.analogInputSources[a].loadCalibrationTable(adc_shifts, inl_slope_intercept[0], - inl_slope_intercept[1]) - if a in polyDict: - self.__print__('loading polynomials for ', a, polyDict[a]) - self.analogInputSources[a].loadPolynomials(polyDict[a]) - self.analogInputSources[a].calibrationReady = True - self.analogInputSources[a].regenerateCalibration() - - # Load calibration data for DAC channels - for a in dac_slope_intercept.split('>|')[1:]: - NAME = a[:3] # Name of the DAC channel . PV1, PV2 ... - self.aboutArray.append([]) - self.aboutArray.append(['Calibrated :', NAME]) - try: - fits = struct.unpack('6f', a[5:]) - self.__print__(NAME, ' calibrated', a[5:]) - except: - self.__print__(NAME, ' not calibrated', a[5:], len(a[5:]), a) - continue - slope = fits[0] - intercept = fits[1] - fitvals = fits[2:] - if NAME in ['PV1', 'PV2', 'PV3']: - ''' - DACs have inherent non-linear behaviour, and the following algorithm generates a correction - array from the calibration data that contains information about the offset(in codes) of each DAC code. - - The correction array defines for each DAC code, the number of codes to skip forwards or backwards - in order to output the most accurate voltage value. - - E.g. if Code 1024 was found to output a voltage corresponding to code 1030 , and code 1020 was found to output a voltage corresponding to code 1024, - then correction array[1024] = -4 , correction_array[1030]=-6. Adding -4 to the code 1024 will give code 1020 which will output the - correct voltage value expected from code 1024. - - The variables LOOKAHEAD and LOOKBEHIND define the range of codes to search around a particular DAC code in order to - find the code with the minimum deviation from the expected value. - - ''' - DACX = np.linspace(self.DAC.CHANS[NAME].range[0], self.DAC.CHANS[NAME].range[1], 4096) - if NAME == 'PV1': - OFF = self.read_bulk_flash(self.DAC_SHIFTS_PV1A, 2048) + self.read_bulk_flash( - self.DAC_SHIFTS_PV1B, 2048) - elif NAME == 'PV2': - OFF = self.read_bulk_flash(self.DAC_SHIFTS_PV2A, 2048) + self.read_bulk_flash( - self.DAC_SHIFTS_PV2B, 2048) - elif NAME == 'PV3': - OFF = self.read_bulk_flash(self.DAC_SHIFTS_PV3A, 2048) + self.read_bulk_flash( - self.DAC_SHIFTS_PV3B, 2048) - OFF = np.array([ord(data) for data in OFF]) - self.__print__('\n', '>' * 20, NAME, '<' * 20) - self.__print__('Offsets :', OFF[:20], '...') - fitfn = np.poly1d(fitvals) - YDATA = fitfn(DACX) - (OFF * slope + intercept) - LOOKBEHIND = 100 - LOOKAHEAD = 100 - OFF = np.array([np.argmin( - np.fabs(YDATA[max(B - LOOKBEHIND, 0):min(4095, B + LOOKAHEAD)] - DACX[B])) - ( - B - max(B - LOOKBEHIND, 0)) for B in range(0, 4096)]) - self.aboutArray.append(['Err min:', min(OFF), 'Err max:', max(OFF)]) - self.DAC.CHANS[NAME].load_calibration_table(OFF) - - def get_resistance(self): - V = self.get_average_voltage('SEN') - if V > 3.295: return np.Inf - I = (3.3 - V) / 5.1e3 - res = V / I - return res * self.resistanceScaling - - def __ignoreCalibration__(self): - print('CALIBRATION DISABLED') - for a in self.analogInputSources: - self.analogInputSources[a].__ignoreCalibration__() - self.analogInputSources[a].regenerateCalibration() - - for a in ['PV1', 'PV2', 'PV3']: self.DAC.__ignoreCalibration__(a) - - def __print__(self, *args): - if self.verbose: - for a in args: - print(a, end="") - print() - - def __del__(self): - self.__print__('closing port') - try: - self.H.fd.close() - except: - pass - - def get_version(self): - """ - Returns the version string of the device - format: LTS-...... - """ - try: - return self.H.get_version(self.H.fd) - except Exception as ex: - self.raiseException(ex, "Communication Error , Function : " + inspect.currentframe().f_code.co_name) - - def getRadioLinks(self): - try: - return self.NRF.get_nodelist() - except Exception as ex: - self.raiseException(ex, "Communication Error , Function : " + inspect.currentframe().f_code.co_name) - - def newRadioLink(self, **args): - ''' - - .. tabularcolumns:: |p{3cm}|p{11cm}| - - ============== ============================================================================== - **Arguments** Description - ============== ============================================================================== - \*\*Kwargs Keyword Arguments - address Address of the node. a 24 bit number. Printed on the nodes.\n - can also be retrieved using :py:meth:`~NRF24L01_class.NRF24L01.get_nodelist` - ============== ============================================================================== - - - :return: :py:meth:`~NRF_NODE.RadioLink` - - - ''' - from PSL.Peripherals import RadioLink - try: - return RadioLink(self.NRF, **args) - except Exception as ex: - self.raiseException(ex, "Communication Error , Function : " + inspect.currentframe().f_code.co_name) - - # -------------------------------------------------------------------------------------------------------------------# - - # |================================================ANALOG SECTION====================================================| - # |This section has commands related to analog measurement and control. These include the oscilloscope routines, | - # |voltmeters, ammeters, and Programmable voltage sources. | - # -------------------------------------------------------------------------------------------------------------------# - - def reconnect(self, **kwargs): - ''' - Attempts to reconnect to the device in case of a commmunication error or accidental disconnect. - ''' - try: - self.H.reconnect(**kwargs) - self.__runInitSequence__(**kwargs) - except Exception as ex: - self.errmsg = str(ex) - self.H.disconnect() - print(self.errmsg) - raise RuntimeError(self.errmsg) - - def capture1(self, ch, ns, tg, *args, **kwargs): - """ - Blocking call that fetches an oscilloscope trace from the specified input channel - - .. tabularcolumns:: |p{3cm}|p{11cm}| - - ============== ============================================================================================ - **Arguments** - ============== ============================================================================================ - ch Channel to select as input. ['CH1'..'CH3','SEN'] - ns Number of samples to fetch. Maximum 10000 - tg Timegap between samples in microseconds - ============== ============================================================================================ - - .. figure:: images/capture1.png - :width: 11cm - :align: center - :alt: alternate text - :figclass: align-center - - A sine wave captured and plotted. - - Example - - >>> from pylab import * - >>> from PSL import sciencelab - >>> I=sciencelab.connect() - >>> x,y = I.capture1('CH1',3200,1) - >>> plot(x,y) - >>> show() - - - :return: Arrays X(timestamps),Y(Corresponding Voltage values) - - """ - return self.capture_fullspeed(ch, ns, tg, *args, **kwargs) - - def capture2(self, ns, tg, TraceOneRemap='CH1'): - """ - Blocking call that fetches oscilloscope traces from CH1,CH2 - - .. tabularcolumns:: |p{3cm}|p{11cm}| - - ============== ======================================================================================================= - **Arguments** - ============== ======================================================================================================= - ns Number of samples to fetch. Maximum 5000 - tg Timegap between samples in microseconds - TraceOneRemap Choose the analog input for channel 1. It is connected to CH1 by default. Channel 2 always reads CH2. - ============== ======================================================================================================= - - .. figure:: images/capture2.png - :width: 11cm - :align: center - :alt: alternate text - :figclass: align-center - - Two sine waves captured and plotted. - - Example - - >>> from pylab import * - >>> from PSL import sciencelab - >>> I=sciencelab.connect() - >>> x,y1,y2 = I.capture2(1600,2,'MIC') #Chan1 remapped to MIC. Chan2 reads CH2 - >>> plot(x,y1) #Plot of analog input MIC - >>> plot(x,y2) #plot of analog input CH2 - >>> show() - - :return: Arrays X(timestamps),Y1(Voltage at CH1),Y2(Voltage at CH2) - - """ - try: - self.capture_traces(2, ns, tg, TraceOneRemap) - time.sleep(1e-6 * self.samples * self.timebase + .01) - while not self.oscilloscope_progress()[0]: - pass - - self.__fetch_channel__(1) - self.__fetch_channel__(2) - - except Exception as ex: - self.raiseException(ex, "Communication Error , Function : " + inspect.currentframe().f_code.co_name) - - x = self.achans[0].get_xaxis() - y = self.achans[0].get_yaxis() - y2 = self.achans[1].get_yaxis() - # x,y2=self.fetch_trace(2) - return x, y, y2 - - def capture4(self, ns, tg, TraceOneRemap='CH1'): - """ - Blocking call that fetches oscilloscope traces from CH1,CH2,CH3,CH4 - - .. tabularcolumns:: |p{3cm}|p{11cm}| - - ============== ====================================================================================================== - **Arguments** - ============== ====================================================================================================== - ns Number of samples to fetch. Maximum 2500 - tg Timegap between samples in microseconds. Minimum 1.75uS - TraceOneRemap Choose the analog input for channel 1. It is connected to CH1 by default. Channel 2 always reads CH2. - ============== ====================================================================================================== - - .. figure:: images/capture4.png - :width: 11cm - :align: center - :alt: alternate text - :figclass: align-center - - Four traces captured and plotted. - - Example - - >>> from pylab import * - >>> I=sciencelab.ScienceLab() - >>> x,y1,y2,y3,y4 = I.capture4(800,1.75) - >>> plot(x,y1) - >>> plot(x,y2) - >>> plot(x,y3) - >>> plot(x,y4) - >>> show() - - :return: Arrays X(timestamps),Y1(Voltage at CH1),Y2(Voltage at CH2),Y3(Voltage at CH3),Y4(Voltage at CH4) - - """ - try: - self.capture_traces(4, ns, tg, TraceOneRemap) - time.sleep(1e-6 * self.samples * self.timebase + .01) - while not self.oscilloscope_progress()[0]: - pass - x, y = self.fetch_trace(1) - x, y2 = self.fetch_trace(2) - x, y3 = self.fetch_trace(3) - x, y4 = self.fetch_trace(4) - except Exception as ex: - self.raiseException(ex, "Communication Error , Function : " + inspect.currentframe().f_code.co_name) - - return x, y, y2, y3, y4 - - def capture_multiple(self, samples, tg, *args): - """ - Blocking call that fetches oscilloscope traces from a set of specified channels - - .. tabularcolumns:: |p{3cm}|p{11cm}| - - ============== ============================================================================================ - **Arguments** - ============== ============================================================================================ - samples Number of samples to fetch. Maximum 10000/(total specified channels) - tg Timegap between samples in microseconds. - \*args channel names - ============== ============================================================================================ - - Example - - >>> from pylab import * - >>> I=sciencelab.ScienceLab() - >>> x,y1,y2,y3,y4 = I.capture_multiple(800,1.75,'CH1','CH2','MIC','SEN') - >>> plot(x,y1) - >>> plot(x,y2) - >>> plot(x,y3) - >>> plot(x,y4) - >>> show() - - :return: Arrays X(timestamps),Y1,Y2 ... - - """ - if len(args) == 0: - self.__print__('please specify channels to record') - return - tg = int(tg * 8) / 8. # Round off the timescale to 1/8uS units - if (tg < 1.5): tg = int(1.5 * 8) / 8. - total_chans = len(args) - - total_samples = samples * total_chans - if (total_samples > self.MAX_SAMPLES): - self.__print__('Sample limit exceeded. 10,000 total') - total_samples = self.MAX_SAMPLES - samples = self.MAX_SAMPLES / total_chans - - CHANNEL_SELECTION = 0 - for chan in args: - C = self.analogInputSources[chan].CHOSA - self.__print__(chan, C) - CHANNEL_SELECTION |= (1 << C) - self.__print__('selection', CHANNEL_SELECTION, len(args), hex(CHANNEL_SELECTION | ((total_chans - 1) << 12))) - - try: - self.H.__sendByte__(CP.ADC) - self.H.__sendByte__(CP.CAPTURE_MULTIPLE) - self.H.__sendInt__(CHANNEL_SELECTION | ((total_chans - 1) << 12)) - - self.H.__sendInt__(total_samples) # total number of samples to record - self.H.__sendInt__(int(self.timebase * 8)) # Timegap between samples. 8MHz timer clock - self.H.__get_ack__() - self.__print__('wait') - time.sleep(1e-6 * total_samples * tg + .01) - self.__print__('done') - data = b'' - for i in range(int(total_samples / self.data_splitting)): - self.H.__sendByte__(CP.ADC) - self.H.__sendByte__(CP.GET_CAPTURE_CHANNEL) - self.H.__sendByte__(0) # channel number . starts with A0 on PIC - self.H.__sendInt__(self.data_splitting) - self.H.__sendInt__(i * self.data_splitting) - data += self.H.fd.read(int( - self.data_splitting * 2)) # reading int by int sometimes causes a communication error. this works better. - self.H.__get_ack__() - - if total_samples % self.data_splitting: - self.H.__sendByte__(CP.ADC) - self.H.__sendByte__(CP.GET_CAPTURE_CHANNEL) - self.H.__sendByte__(0) # channel number starts with A0 on PIC - self.H.__sendInt__(total_samples % self.data_splitting) - self.H.__sendInt__(total_samples - total_samples % self.data_splitting) - data += self.H.fd.read(int(2 * ( - total_samples % self.data_splitting))) # reading int by int may cause packets to be dropped. this works better. - self.H.__get_ack__() - - for a in range(int(total_samples)): self.buff[a] = CP.ShortInt.unpack(data[a * 2:a * 2 + 2])[0] - # self.achans[channel_number-1].yaxis = self.achans[channel_number-1].fix_value(self.buff[:samples]) - yield np.linspace(0, tg * (samples - 1), samples) - for a in range(int(total_chans)): - yield self.buff[a:total_samples][::total_chans] - except Exception as ex: - self.raiseException(ex, "Communication Error , Function : " + inspect.currentframe().f_code.co_name) - - def __capture_fullspeed__(self, chan, samples, tg, *args, **kwargs): - tg = int(tg * 8) / 8. # Round off the timescale to 1/8uS units - if (tg < 0.5): tg = int(0.5 * 8) / 8. - if (samples > self.MAX_SAMPLES): - self.__print__('Sample limit exceeded. 10,000 max') - samples = self.MAX_SAMPLES - - self.timebase = int(tg * 8) / 8. - self.samples = samples - CHOSA = self.analogInputSources[chan].CHOSA - - try: - self.H.__sendByte__(CP.ADC) - if 'SET_LOW' in args: - self.H.__sendByte__(CP.SET_LO_CAPTURE) - elif 'SET_HIGH' in args: - self.H.__sendByte__(CP.SET_HI_CAPTURE) - elif 'FIRE_PULSES' in args: - self.H.__sendByte__(CP.PULSE_TRAIN) - self.__print__('firing sqr1 pulses for ', kwargs.get('interval', 1000), 'uS') - else: - self.H.__sendByte__(CP.CAPTURE_DMASPEED) - self.H.__sendByte__(CHOSA) - self.H.__sendInt__(samples) # total number of samples to record - self.H.__sendInt__(int(tg * 8)) # Timegap between samples. 8MHz timer clock - if 'FIRE_PULSES' in args: - t = kwargs.get('interval', 1000) - print('Firing for', t, 'uS') - self.H.__sendInt__(t) - time.sleep( - t * 1e-6) # Wait for hardware to free up from firing pulses(blocking call). Background capture starts immediately after this - self.H.__get_ack__() - except Exception as ex: - self.raiseException(ex, "Communication Error , Function : " + inspect.currentframe().f_code.co_name) - - def capture_fullspeed(self, chan, samples, tg, *args, **kwargs): - """ - Blocking call that fetches oscilloscope traces from a single oscilloscope channel at a maximum speed of 2MSPS - - .. tabularcolumns:: |p{3cm}|p{11cm}| - - ============== ============================================================================================ - **Arguments** - ============== ============================================================================================ - chan channel name 'CH1' / 'CH2' ... 'SEN' - samples Number of samples to fetch. Maximum 10000/(total specified channels) - tg Timegap between samples in microseconds. minimum 0.5uS - \*args specify if SQR1 must be toggled right before capturing. - 'SET_LOW' will set SQR1 to 0V - 'SET_HIGH' will set it to 5V. - 'FIRE_PULSES' will output a preset frequency on SQR1 for a given interval (keyword arg 'interval' - must be specified or it will default to 1000uS) before acquiring data. This is - used for measuring speed of sound using piezos - if no arguments are specified, a regular capture will be executed. - \*\*kwargs - interval units:uS . Necessary if 'FIRE_PULSES' argument was supplied. default 1000uS - ============== ============================================================================================ - - .. code-block:: python - - from pylab import * - I=sciencelab.ScienceLab() - x,y = I.capture_fullspeed('CH1',2000,1) - plot(x,y) - show() - - - .. code-block:: python - - x,y = I.capture_fullspeed('CH1',2000,1,'SET_LOW') - plot(x,y) - show() - - .. code-block:: python - - I.sqr1(40e3 , 50, True ) # Prepare a 40KHz, 50% square wave. Do not output it yet - x,y = I.capture_fullspeed('CH1',2000,1,'FIRE_PULSES',interval = 250) #Output the prepared 40KHz(25uS) wave for 250uS(10 cycles) before acquisition - plot(x,y) - show() - - :return: timestamp array ,voltage_value array - - """ - self.__capture_fullspeed__(chan, samples, tg, *args, **kwargs) - time.sleep(1e-6 * self.samples * self.timebase + kwargs.get('interval', 0) * 1e-6 + 0.1) - x, y = self.__retrieveBufferData__(chan, self.samples, self.timebase) - - return x, self.analogInputSources[chan].calPoly10(y) - - def __capture_fullspeed_hr__(self, chan, samples, tg, *args): - tg = int(tg * 8) / 8. # Round off the timescale to 1/8uS units - if (tg < 1): tg = 1. - if (samples > self.MAX_SAMPLES): - self.__print__('Sample limit exceeded. 10,000 max') - samples = self.MAX_SAMPLES - - self.timebase = int(tg * 8) / 8. - self.samples = samples - CHOSA = self.analogInputSources[chan].CHOSA - try: - self.H.__sendByte__(CP.ADC) - if 'SET_LOW' in args: - self.H.__sendByte__(CP.SET_LO_CAPTURE) - elif 'SET_HIGH' in args: - self.H.__sendByte__(CP.SET_HI_CAPTURE) - elif 'READ_CAP' in args: - self.H.__sendByte__(CP.MULTIPOINT_CAPACITANCE) - else: - self.H.__sendByte__(CP.CAPTURE_DMASPEED) - self.H.__sendByte__(CHOSA | 0x80) - self.H.__sendInt__(samples) # total number of samples to record - self.H.__sendInt__(int(tg * 8)) # Timegap between samples. 8MHz timer clock - self.H.__get_ack__() - except Exception as ex: - self.raiseException(ex, "Communication Error , Function : " + inspect.currentframe().f_code.co_name) - - def capture_fullspeed_hr(self, chan, samples, tg, *args): - try: - self.__capture_fullspeed_hr__(chan, samples, tg, *args) - time.sleep(1e-6 * self.samples * self.timebase + .01) - x, y = self.__retrieveBufferData__(chan, self.samples, self.timebase) - except Exception as ex: - self.raiseException(ex, "Communication Error , Function : " + inspect.currentframe().f_code.co_name) - - return x, self.analogInputSources[chan].calPoly12(y) - - def __retrieveBufferData__(self, chan, samples, tg): - ''' - - ''' - data = b'' - try: - for i in range(int(samples / self.data_splitting)): - self.H.__sendByte__(CP.ADC) - self.H.__sendByte__(CP.GET_CAPTURE_CHANNEL) - self.H.__sendByte__(0) # channel number . starts with A0 on PIC - self.H.__sendInt__(self.data_splitting) - self.H.__sendInt__(i * self.data_splitting) - data += self.H.fd.read(int( - self.data_splitting * 2)) # reading int by int sometimes causes a communication error. this works better. - self.H.__get_ack__() - - if samples % self.data_splitting: - self.H.__sendByte__(CP.ADC) - self.H.__sendByte__(CP.GET_CAPTURE_CHANNEL) - self.H.__sendByte__(0) # channel number starts with A0 on PIC - self.H.__sendInt__(samples % self.data_splitting) - self.H.__sendInt__(samples - samples % self.data_splitting) - data += self.H.fd.read(int(2 * ( - samples % self.data_splitting))) # reading int by int may cause packets to be dropped. this works better. - self.H.__get_ack__() - - except Exception as ex: - self.raiseException(ex, "Communication Error , Function : " + inspect.currentframe().f_code.co_name) - try: - for a in range(int(samples)): self.buff[a] = CP.ShortInt.unpack(data[a * 2:a * 2 + 2])[0] - except Exception as ex: - msg = "Incorrect Number of Bytes Received\n" - raise RuntimeError(msg) - - # self.achans[channel_number-1].yaxis = self.achans[channel_number-1].fix_value(self.buff[:samples]) - return np.linspace(0, tg * (samples - 1), samples), self.buff[:samples] - - def capture_traces(self, num, samples, tg, channel_one_input='CH1', CH123SA=0, **kwargs): - """ - Instruct the ADC to start sampling. use fetch_trace to retrieve the data - - .. tabularcolumns:: |p{3cm}|p{11cm}| - - =================== ============================================================================================ - **Arguments** - =================== ============================================================================================ - num Channels to acquire. 1/2/4 - samples Total points to store per channel. Maximum 3200 total. - tg Timegap between two successive samples (in uSec) - channel_one_input map channel 1 to 'CH1' ... 'CH9' - \*\*kwargs - - \*trigger Whether or not to trigger the oscilloscope based on the voltage level set by :func:`configure_trigger` - =================== ============================================================================================ - - - see :ref:`capture_video` - - .. _adc_example: - - .. figure:: images/transient.png - :width: 11cm - :align: center - :alt: alternate text - :figclass: align-center - - Transient response of an Inductor and Capacitor in series - - The following example demonstrates how to use this function to record active events. - - * Connect a capacitor and an Inductor in series. - * Connect CH1 to the spare leg of the inductor. Also Connect OD1 to this point - * Connect CH2 to the junction between the capacitor and the inductor - * connect the spare leg of the capacitor to GND( ground ) - * set OD1 initially high using set_state(SQR1=1) - - :: - - >>> I.set_state(OD1=1) #Turn on OD1 - #Arbitrary delay to wait for stabilization - >>> time.sleep(0.5) - #Start acquiring data (2 channels,800 samples, 2microsecond intervals) - >>> I.capture_traces(2,800,2,trigger=False) - #Turn off OD1. This must occur immediately after the previous line was executed. - >>> I.set_state(OD1=0) - #Minimum interval to wait for completion of data acquisition. - #samples*timegap*(convert to Seconds) - >>> time.sleep(800*2*1e-6) - >>> x,CH1=I.fetch_trace(1) - >>> x,CH2=I.fetch_trace(2) - >>> plot(x,CH1-CH2) #Voltage across the inductor - >>> plot(x,CH2) ##Voltage across the capacitor - >>> show() - - The following events take place when the above snippet runs - - #. The oscilloscope starts storing voltages present at CH1 and CH2 every 2 microseconds - #. The output OD1 was enabled, and this causes the voltage between the L and C to approach OD1 voltage. - (It may or may not oscillate) - #. The data from CH1 and CH2 was read into x,CH1,CH2 - #. Both traces were plotted in order to visualize the Transient response of series LC - - :return: nothing - - .. seealso:: - :func:`fetch_trace` , :func:`oscilloscope_progress` , :func:`capture1` , :func:`capture2` , :func:`capture4` - - """ - triggerornot = 0x80 if kwargs.get('trigger', True) else 0 - self.timebase = tg - self.timebase = int(self.timebase * 8) / 8. # Round off the timescale to 1/8uS units - if channel_one_input not in self.analogInputSources: raise RuntimeError( - 'Invalid input %s, not in %s' % (channel_one_input, str(self.analogInputSources.keys()))) - CHOSA = self.analogInputSources[channel_one_input].CHOSA - try: - self.H.__sendByte__(CP.ADC) - if (num == 1): - if (self.timebase < 1.5): self.timebase = int(1.5 * 8) / 8. - if (samples > self.MAX_SAMPLES): samples = self.MAX_SAMPLES - - self.achans[0].set_params(channel=channel_one_input, length=samples, timebase=self.timebase, - resolution=10, source=self.analogInputSources[channel_one_input]) - self.H.__sendByte__(CP.CAPTURE_ONE) # read 1 channel - self.H.__sendByte__(CHOSA | triggerornot) # channelk number - - elif (num == 2): - if (self.timebase < 1.75): self.timebase = int(1.75 * 8) / 8. - if (samples > self.MAX_SAMPLES / 2): samples = self.MAX_SAMPLES / 2 - - self.achans[0].set_params(channel=channel_one_input, length=samples, timebase=self.timebase, - resolution=10, source=self.analogInputSources[channel_one_input]) - self.achans[1].set_params(channel='CH2', length=samples, timebase=self.timebase, resolution=10, - source=self.analogInputSources['CH2']) - - self.H.__sendByte__(CP.CAPTURE_TWO) # capture 2 channels - self.H.__sendByte__(CHOSA | triggerornot) # channel 0 number - - elif (num == 3 or num == 4): - if (self.timebase < 1.75): self.timebase = int(1.75 * 8) / 8. - if (samples > self.MAX_SAMPLES / 4): samples = self.MAX_SAMPLES / 4 - - self.achans[0].set_params(channel=channel_one_input, length=samples, timebase=self.timebase, \ - resolution=10, source=self.analogInputSources[channel_one_input]) - - for a in range(1, 4): - chans = ['NONE', 'CH2', 'CH3', 'MIC'] - self.achans[a].set_params(channel=chans[a], length=samples, timebase=self.timebase, \ - resolution=10, source=self.analogInputSources[chans[a]]) - - self.H.__sendByte__(CP.CAPTURE_FOUR) # read 4 channels - self.H.__sendByte__(CHOSA | (CH123SA << 4) | triggerornot) # channel number - - self.samples = samples - self.H.__sendInt__(samples) # number of samples per channel to record - self.H.__sendInt__(int(self.timebase * 8)) # Timegap between samples. 8MHz timer clock - self.H.__get_ack__() - self.channels_in_buffer = num - except Exception as ex: - self.raiseException(ex, "Communication Error , Function : " + inspect.currentframe().f_code.co_name) - - def capture_highres_traces(self, channel, samples, tg, **kwargs): - """ - Instruct the ADC to start sampling. use fetch_trace to retrieve the data - - .. tabularcolumns:: |p{3cm}|p{11cm}| - - =================== ============================================================================================ - **Arguments** - =================== ============================================================================================ - channel channel to acquire data from 'CH1' ... 'CH9' - samples Total points to store per channel. Maximum 3200 total. - tg Timegap between two successive samples (in uSec) - \*\*kwargs - - \*trigger Whether or not to trigger the oscilloscope based on the voltage level set by :func:`configure_trigger` - =================== ============================================================================================ - - - :return: nothing - - .. seealso:: - - :func:`fetch_trace` , :func:`oscilloscope_progress` , :func:`capture1` , :func:`capture2` , :func:`capture4` - - """ - triggerornot = 0x80 if kwargs.get('trigger', True) else 0 - self.timebase = tg - try: - self.H.__sendByte__(CP.ADC) - CHOSA = self.analogInputSources[channel].CHOSA - if (self.timebase < 3): self.timebase = 3 - if (samples > self.MAX_SAMPLES): samples = self.MAX_SAMPLES - self.achans[0].set_params(channel=channel, length=samples, timebase=self.timebase, resolution=12, - source=self.analogInputSources[channel]) - - self.H.__sendByte__(CP.CAPTURE_12BIT) # read 1 channel - self.H.__sendByte__(CHOSA | triggerornot) # channelk number - - self.samples = samples - self.H.__sendInt__(samples) # number of samples to read - self.H.__sendInt__(int(self.timebase * 8)) # Timegap between samples. 8MHz timer clock - self.H.__get_ack__() - self.channels_in_buffer = 1 - except Exception as ex: - self.raiseException(ex, "Communication Error , Function : " + inspect.currentframe().f_code.co_name) - - def fetch_trace(self, channel_number): - """ - fetches a channel(1-4) captured by :func:`capture_traces` called prior to this, and returns xaxis,yaxis - - .. tabularcolumns:: |p{3cm}|p{11cm}| - - ============== ============================================================================================ - **Arguments** - ============== ============================================================================================ - channel_number Any of the maximum of four channels that the oscilloscope captured. 1/2/3/4 - ============== ============================================================================================ - - :return: time array,voltage array - - .. seealso:: - - :func:`capture_traces` , :func:`oscilloscope_progress` - - """ - self.__fetch_channel__(channel_number) - return self.achans[channel_number - 1].get_xaxis(), self.achans[channel_number - 1].get_yaxis() - - def oscilloscope_progress(self): - """ - returns the number of samples acquired by the capture routines, and the conversion_done status - - :return: conversion done(bool) ,samples acquired (number) - - >>> I.start_capture(1,3200,2) - >>> self.__print__(I.oscilloscope_progress()) - (0,46) - >>> time.sleep(3200*2e-6) - >>> self.__print__(I.oscilloscope_progress()) - (1,3200) - - .. seealso:: - - :func:`fetch_trace` , :func:`capture_traces` - - """ - conversion_done = 0 - samples = 0 - try: - self.H.__sendByte__(CP.ADC) - self.H.__sendByte__(CP.GET_CAPTURE_STATUS) - conversion_done = self.H.__getByte__() - samples = self.H.__getInt__() - self.H.__get_ack__() - except Exception as ex: - self.raiseException(ex, "Communication Error , Function : " + inspect.currentframe().f_code.co_name) - return conversion_done, samples - - def __fetch_channel__(self, channel_number): - """ - Fetches a section of data from any channel and stores it in the relevant instance of achan() - - .. tabularcolumns:: |p{3cm}|p{11cm}| - - ============== ============================================================================================ - **Arguments** - ============== ============================================================================================ - channel_number channel number (1,2,3,4) - ============== ============================================================================================ - - :return: True if successful - """ - samples = self.achans[channel_number - 1].length - if (channel_number > self.channels_in_buffer): - self.__print__('Channel unavailable') - return False - data = b'' - try: - for i in range(int(samples / self.data_splitting)): - self.H.__sendByte__(CP.ADC) - self.H.__sendByte__(CP.GET_CAPTURE_CHANNEL) - self.H.__sendByte__(channel_number - 1) # starts with A0 on PIC - self.H.__sendInt__(self.data_splitting) - self.H.__sendInt__(i * self.data_splitting) - data += self.H.fd.read( - int(self.data_splitting * 2)) # reading int by int sometimes causes a communication error. - self.H.__get_ack__() - - if samples % self.data_splitting: - self.H.__sendByte__(CP.ADC) - self.H.__sendByte__(CP.GET_CAPTURE_CHANNEL) - self.H.__sendByte__(channel_number - 1) # starts with A0 on PIC - self.H.__sendInt__(samples % self.data_splitting) - self.H.__sendInt__(samples - samples % self.data_splitting) - data += self.H.fd.read( - int(2 * (samples % self.data_splitting))) # reading int by int may cause packets to be dropped. - self.H.__get_ack__() - except Exception as ex: - self.raiseException(ex, "Communication Error , Function : " + inspect.currentframe().f_code.co_name) - - try: - for a in range(int(samples)): self.buff[a] = CP.ShortInt.unpack(data[a * 2:a * 2 + 2])[0] - self.achans[channel_number - 1].yaxis = self.achans[channel_number - 1].fix_value(self.buff[:samples]) - except Exception as ex: - msg = "Incorrect Number of bytes received.\n" - raise RuntimeError(msg) - - return True - - def __fetch_channel_oneshot__(self, channel_number): - """ - Fetches all data from given channel and stores it in the relevant instance of achan() - - .. tabularcolumns:: |p{3cm}|p{11cm}| - - ============== ============================================================================================ - **Arguments** - ============== ============================================================================================ - channel_number channel number (1,2,3,4) - ============== ============================================================================================ - - """ - offset = 0 - samples = self.achans[channel_number - 1].length - if (channel_number > self.channels_in_buffer): - self.__print__('Channel unavailable') - return False - try: - self.H.__sendByte__(CP.ADC) - self.H.__sendByte__(CP.GET_CAPTURE_CHANNEL) - self.H.__sendByte__(channel_number - 1) # starts with A0 on PIC - self.H.__sendInt__(samples) - self.H.__sendInt__(offset) - data = self.H.fd.read( - int(samples * 2)) # reading int by int sometimes causes a communication error. this works better. - self.H.__get_ack__() - for a in range(int(samples)): self.buff[a] = CP.ShortInt.unpack(data[a * 2:a * 2 + 2])[0] - self.achans[channel_number - 1].yaxis = self.achans[channel_number - 1].fix_value(self.buff[:samples]) - except Exception as ex: - self.raiseException(ex, "Communication Error , Function : " + inspect.currentframe().f_code.co_name) - - return True - - def configure_trigger(self, chan, name, voltage, resolution=10, **kwargs): - """ - configure trigger parameters for 10-bit capture commands - The capture routines will wait till a rising edge of the input signal crosses the specified level. - The trigger will timeout within 8mS, and capture routines will start regardless. - - These settings will not be used if the trigger option in the capture routines are set to False - - .. tabularcolumns:: |p{3cm}|p{11cm}| - - ============== ===================================================================================================================== - **Arguments** - ============== ===================================================================================================================== - chan channel . 0, 1,2,3. corresponding to the channels being recorded by the capture routine(not the analog inputs) - name the name of the channel. 'CH1'... 'V+' - voltage The voltage level that should trigger the capture sequence(in Volts) - ============== ===================================================================================================================== - - **Example** - - >>> I.configure_trigger(0,'CH1',1.1) - >>> I.capture_traces(4,800,2) - #Unless a timeout occured, the first point of this channel will be close to 1.1Volts - >>> I.fetch_trace(1) - #This channel was acquired simultaneously with channel 1, - #so it's triggered along with the first - >>> I.fetch_trace(2) - - .. seealso:: - - :func:`capture_traces` , adc_example_ - - """ - prescaler = kwargs.get('prescaler', 0) - try: - self.H.__sendByte__(CP.ADC) - self.H.__sendByte__(CP.CONFIGURE_TRIGGER) - self.H.__sendByte__( - (prescaler << 4) | (1 << chan)) # Trigger channel (4lsb) , trigger timeout prescaler (4msb) - - if resolution == 12: - level = self.analogInputSources[name].voltToCode12(voltage) - level = np.clip(level, 0, 4095) - else: - level = self.analogInputSources[name].voltToCode10(voltage) - level = np.clip(level, 0, 1023) - - if level > (2 ** resolution - 1): - level = (2 ** resolution - 1) - elif level < 0: - level = 0 - - self.H.__sendInt__(int(level)) # Trigger - self.H.__get_ack__() - except Exception as ex: - self.raiseException(ex, "Communication Error , Function : " + inspect.currentframe().f_code.co_name) - - def set_gain(self, channel, gain, Force=False): - """ - set the gain of the selected PGA - - .. tabularcolumns:: |p{3cm}|p{11cm}| - - ============== ============================================================================================ - **Arguments** - ============== ============================================================================================ - channel 'CH1','CH2' - gain (0-8) -> (1x,2x,4x,5x,8x,10x,16x,32x,1/11x) - Force If True, the amplifier gain will be set even if it was previously set to the same value. - ============== ============================================================================================ - - .. note:: - The gain value applied to a channel will result in better resolution for small amplitude signals. - - However, values read using functions like :func:`get_average_voltage` or :func:`capture_traces` - will not be 2x, or 4x times the input signal. These are calibrated to return accurate values of the original input signal. - - in case the gain specified is 8 (1/11x) , an external 10MOhm resistor must be connected in series with the device. The input range will - be +/-160 Volts - - >>> I.set_gain('CH1',7) #gain set to 32x on CH1 - - """ - if gain < 0 or gain > 8: - print('Invalid gain parameter. 0-7 only.') - return - if self.analogInputSources[channel].gainPGA == None: - self.__print__('No amplifier exists on this channel :', channel) - return False - - refresh = False - if self.gains[channel] != gain: - self.gains[channel] = gain - time.sleep(0.01) - refresh = True - if refresh or Force: - try: - self.analogInputSources[channel].setGain(self.gain_values[gain]) - if gain > 7: gain = 0 # external attenuator mode. set gain 1x - self.H.__sendByte__(CP.ADC) - self.H.__sendByte__(CP.SET_PGA_GAIN) - self.H.__sendByte__(self.analogInputSources[channel].gainPGA) # send the channel. SPI, not multiplexer - self.H.__sendByte__(gain) # send the gain - self.H.__get_ack__() - return self.gain_values[gain] - except Exception as ex: - self.raiseException(ex, "Communication Error , Function : " + inspect.currentframe().f_code.co_name) - - return refresh - - def select_range(self, channel, voltage_range): - """ - set the gain of the selected PGA - - .. tabularcolumns:: |p{3cm}|p{11cm}| - - ============== ============================================================================================ - **Arguments** - ============== ============================================================================================ - channel 'CH1','CH2' - voltage_range choose from [16,8,4,3,2,1.5,1,.5,160] - ============== ============================================================================================ - - .. note:: - Setting the right voltage range will result in better resolution. - in case the range specified is 160 , an external 10MOhm resistor must be connected in series with the device. - - Note : this function internally calls set_gain with the appropriate gain value - - >>> I.select_range('CH1',8) #gain set to 2x on CH1. Voltage range +/-8V - - """ - ranges = [16, 8, 4, 3, 2, 1.5, 1, .5, 160] - if voltage_range in ranges: - g = ranges.index(voltage_range) - return self.set_gain(channel, g) - else: - print('not a valid range. try : ', ranges) - return None - - def __calcCHOSA__(self, name): - name = name.upper() - source = self.analogInputSources[name] - - if name not in self.allAnalogChannels: - self.__print__('not a valid channel name. selecting CH1') - return self.__calcCHOSA__('CH1') - - return source.CHOSA - - def get_voltage(self, channel_name, **kwargs): - self.voltmeter_autorange(channel_name) - return self.get_average_voltage(channel_name, **kwargs) - - def voltmeter_autorange(self, channel_name): - if self.analogInputSources[channel_name].gainPGA == None: return None - self.set_gain(channel_name, 0) - V = self.get_average_voltage(channel_name) - return self.__autoSelectRange__(channel_name, V) - - def __autoSelectRange__(self, channel_name, V): - keys = [8, 4, 3, 2, 1.5, 1, .5, 0] - cutoffs = {8: 0, 4: 1, 3: 2, 2: 3, 1.5: 4, 1.: 5, .5: 6, 0: 7} - for a in keys: - if abs(V) > a: - g = cutoffs[a] - break - self.set_gain(channel_name, g) - return g - - def __autoRangeScope__(self, tg): - x, y1, y2 = self.capture2(1000, tg) - self.__autoSelectRange__('CH1', max(abs(y1))) - self.__autoSelectRange__('CH2', max(abs(y2))) - - def get_average_voltage(self, channel_name, **kwargs): - """ - Return the voltage on the selected channel - - .. tabularcolumns:: |p{3cm}|p{11cm}| - - +------------+-----------------------------------------------------------------------------------------+ - |Arguments |Description | - +============+=========================================================================================+ - |channel_name| 'CH1','CH2','CH3', 'MIC','IN1','SEN','V+' | - +------------+-----------------------------------------------------------------------------------------+ - |sleep | read voltage in CPU sleep mode. not particularly useful. Also, Buggy. | - +------------+-----------------------------------------------------------------------------------------+ - |\*\*kwargs | Samples to average can be specified. eg. samples=100 will average a hundred readings | - +------------+-----------------------------------------------------------------------------------------+ - - - see :ref:`stream_video` - - Example: - - >>> self.__print__(I.get_average_voltage('CH4')) - 1.002 - - """ - try: - poly = self.analogInputSources[channel_name].calPoly12 - except Exception as ex: - msg = "Invalid Channel" + str(ex) - raise RuntimeError(msg) - vals = [self.__get_raw_average_voltage__(channel_name, **kwargs) for a in range(int(kwargs.get('samples', 1)))] - # if vals[0]>2052:print (vals) - val = np.average([poly(a) for a in vals]) - return val - - def __get_raw_average_voltage__(self, channel_name, **kwargs): - """ - Return the average of 16 raw 12-bit ADC values of the voltage on the selected channel - - .. tabularcolumns:: |p{3cm}|p{11cm}| - - ============== ============================================================================================================ - **Arguments** - ============== ============================================================================================================ - channel_name 'CH1', 'CH2', 'CH3', 'MIC', '5V', 'IN1','SEN' - sleep read voltage in CPU sleep mode - ============== ============================================================================================================ - - """ - try: - chosa = self.__calcCHOSA__(channel_name) - self.H.__sendByte__(CP.ADC) - self.H.__sendByte__(CP.GET_VOLTAGE_SUMMED) - self.H.__sendByte__(chosa) - V_sum = self.H.__getInt__() - self.H.__get_ack__() - return V_sum / 16. # sum(V)/16.0 # - except Exception as ex: - self.raiseException(ex, "Communication Error , Function : " + inspect.currentframe().f_code.co_name) - - def fetch_buffer(self, starting_position=0, total_points=100): - """ - fetches a section of the ADC hardware buffer - """ - try: - self.H.__sendByte__(CP.COMMON) - self.H.__sendByte__(CP.RETRIEVE_BUFFER) - self.H.__sendInt__(starting_position) - self.H.__sendInt__(total_points) - for a in range(int(total_points)): self.buff[a] = self.H.__getInt__() - self.H.__get_ack__() - except Exception as ex: - self.raiseException(ex, "Communication Error , Function : " + inspect.currentframe().f_code.co_name) - - def clear_buffer(self, starting_position, total_points): - """ - clears a section of the ADC hardware buffer - """ - try: - self.H.__sendByte__(CP.COMMON) - self.H.__sendByte__(CP.CLEAR_BUFFER) - self.H.__sendInt__(starting_position) - self.H.__sendInt__(total_points) - self.H.__get_ack__() - except Exception as ex: - self.raiseException(ex, "Communication Error , Function : " + inspect.currentframe().f_code.co_name) - - def fill_buffer(self, starting_position, point_array): - """ - fill a section of the ADC hardware buffer with data - """ - try: - self.H.__sendByte__(CP.COMMON) - self.H.__sendByte__(CP.FILL_BUFFER) - self.H.__sendInt__(starting_position) - self.H.__sendInt__(len(point_array)) - for a in point_array: - self.H.__sendInt__(int(a)) - self.H.__get_ack__() - except Exception as ex: - self.raiseException(ex, "Communication Error , Function : " + inspect.currentframe().f_code.co_name) - - def start_streaming(self, tg, channel='CH1'): - """ - Instruct the ADC to start streaming 8-bit data. use stop_streaming to stop. - - .. tabularcolumns:: |p{3cm}|p{11cm}| - - ============== ============================================================================================ - **Arguments** - ============== ============================================================================================ - tg timegap. 250KHz clock - channel channel 'CH1'... 'CH9','IN1','SEN' - ============== ============================================================================================ - - """ - if (self.streaming): self.stop_streaming() - try: - self.H.__sendByte__(CP.ADC) - self.H.__sendByte__(CP.START_ADC_STREAMING) - self.H.__sendByte__(self.__calcCHOSA__(channel)) - self.H.__sendInt__(tg) # Timegap between samples. 8MHz timer clock - self.streaming = True - except Exception as ex: - self.raiseException(ex, "Communication Error , Function : " + inspect.currentframe().f_code.co_name) - - def stop_streaming(self): - """ - Instruct the ADC to stop streaming data - """ - if (self.streaming): - self.H.__sendByte__(CP.STOP_STREAMING) - self.H.fd.read(20000) - self.H.fd.flush() - else: - self.__print__('not streaming') - self.streaming = False - - # -------------------------------------------------------------------------------------------------------------------# - - # |===============================================DIGITAL SECTION====================================================| - # |This section has commands related to digital measurement and control. These include the Logic Analyzer, frequency | - # |measurement calls, timing routines, digital outputs etc | - # -------------------------------------------------------------------------------------------------------------------# - - def __calcDChan__(self, name): - """ - accepts a string represention of a digital input ['ID1','ID2','ID3','ID4','SEN','EXT','CNTR'] - and returns a corresponding number - """ - - if name in self.digital_channel_names: - return self.digital_channel_names.index(name) - else: - self.__print__(' invalid channel', name, ' , selecting ID1 instead ') - return 0 - - def __get_high_freq__backup__(self, pin): - try: - self.H.__sendByte__(CP.COMMON) - self.H.__sendByte__(CP.GET_HIGH_FREQUENCY) - self.H.__sendByte__(self.__calcDChan__(pin)) - scale = self.H.__getByte__() - val = self.H.__getLong__() - self.H.__get_ack__() - return scale * (val) / 1.0e-1 # 100mS sampling - except Exception as ex: - self.raiseException(ex, "Communication Error , Function : " + inspect.currentframe().f_code.co_name) - - def get_high_freq(self, pin): - """ - retrieves the frequency of the signal connected to ID1. for frequencies > 1MHz - also good for lower frequencies, but avoid using it since - the oscilloscope cannot be used simultaneously due to hardware limitations. - - The input frequency is fed to a 32 bit counter for a period of 100mS. - The value of the counter at the end of 100mS is used to calculate the frequency. - - see :ref:`freq_video` - - - .. seealso:: :func:`get_freq` - - .. tabularcolumns:: |p{3cm}|p{11cm}| - - ============== ============================================================================================ - **Arguments** - ============== ============================================================================================ - pin The input pin to measure frequency from : ['ID1','ID2','ID3','ID4','SEN','EXT','CNTR'] - ============== ============================================================================================ - - :return: frequency - """ - try: - self.H.__sendByte__(CP.COMMON) - self.H.__sendByte__(CP.GET_ALTERNATE_HIGH_FREQUENCY) - self.H.__sendByte__(self.__calcDChan__(pin)) - scale = self.H.__getByte__() - val = self.H.__getLong__() - self.H.__get_ack__() - # self.__print__(hex(val)) - return scale * (val) / 1.0e-1 # 100mS sampling - except Exception as ex: - self.raiseException(ex, "Communication Error , Function : " + inspect.currentframe().f_code.co_name) - - def get_freq(self, channel='CNTR', timeout=2): - """ - Frequency measurement on IDx. - Measures time taken for 16 rising edges of input signal. - returns the frequency in Hertz - - .. tabularcolumns:: |p{3cm}|p{11cm}| - - ============== ============================================================================================ - **Arguments** - ============== ============================================================================================ - channel The input to measure frequency from. ['ID1','ID2','ID3','ID4','SEN','EXT','CNTR'] - timeout This is a blocking call which will wait for one full wavelength before returning the - calculated frequency. - Use the timeout option if you're unsure of the input signal. - returns 0 if timed out - ============== ============================================================================================ - - :return float: frequency - - - .. _timing_example: - - * connect SQR1 to ID1 - - >>> I.sqr1(4000,25) - >>> self.__print__(I.get_freq('ID1')) - 4000.0 - >>> self.__print__(I.r2r_time('ID1')) - #time between successive rising edges - 0.00025 - >>> self.__print__(I.f2f_time('ID1')) - #time between successive falling edges - 0.00025 - >>> self.__print__(I.pulse_time('ID1')) - #may detect a low pulse, or a high pulse. Whichever comes first - 6.25e-05 - >>> I.duty_cycle('ID1') - #returns wavelength, high time - (0.00025,6.25e-05) - - """ - try: - self.H.__sendByte__(CP.COMMON) - self.H.__sendByte__(CP.GET_FREQUENCY) - timeout_msb = int((timeout * 64e6)) >> 16 - self.H.__sendInt__(timeout_msb) - self.H.__sendByte__(self.__calcDChan__(channel)) - - self.H.waitForData(timeout) - - tmt = self.H.__getByte__() - x = [self.H.__getLong__() for a in range(2)] - self.H.__get_ack__() - freq = lambda t: 16 * 64e6 / t if (t) else 0 - # self.__print__(x,tmt,timeout_msb) - except Exception as ex: - self.raiseException(ex, "Communication Error , Function : " + inspect.currentframe().f_code.co_name) - if (tmt): return 0 - return freq(x[1] - x[0]) - - ''' - def r2r_time(self,channel='ID1',timeout=0.1): - """ - Returns the time interval between two rising edges - of input signal on ID1 - - .. tabularcolumns:: |p{3cm}|p{11cm}| - - ============== ================================================================================================ - **Arguments** - ============== ================================================================================================ - channel The input to measure time between two rising edges.['ID1','ID2','ID3','ID4','SEN','EXT','CNTR'] - timeout Use the timeout option if you're unsure of the input signal time period. - returns 0 if timed out - ============== ================================================================================================ - - :return float: time between two rising edges of input signal - - .. seealso:: timing_example_ - - """ - self.H.__sendByte__(CP.TIMING) - self.H.__sendByte__(CP.GET_TIMING) - timeout_msb = int((timeout*64e6))>>16 - self.H.__sendInt__(timeout_msb) - self.H.__sendByte__( EVERY_RISING_EDGE<<2 | 2) - self.H.__sendByte__(self.__calcDChan__(channel)) - tmt = self.H.__getInt__() - x=[self.H.__getLong__() for a in range(2)] - self.H.__get_ack__() - if(tmt >= timeout_msb):return -1 - rtime = lambda t: t/64e6 - y=x[1]-x[0] - return rtime(y) - ''' - - def r2r_time(self, channel, skip_cycle=0, timeout=5): - """ - Return a list of rising edges that occured within the timeout period. - - .. tabularcolumns:: |p{3cm}|p{11cm}| - - ============== ============================================================================================================== - **Arguments** - ============== ============================================================================================================== - channel The input to measure time between two rising edges.['ID1','ID2','ID3','ID4','SEN','EXT','CNTR'] - skip_cycle Number of points to skip. eg. Pendulums pass through light barriers twice every cycle. SO 1 must be skipped - timeout Number of seconds to wait for datapoints. (Maximum 60 seconds) - ============== ============================================================================================================== - - :return list: Array of points - - """ - try: - if timeout > 60: timeout = 60 - self.start_one_channel_LA(channel=channel, channel_mode=3, trigger_mode=0) # every rising edge - startTime = time.time() - while time.time() - startTime < timeout: - a, b, c, d, e = self.get_LA_initial_states() - if a == self.MAX_SAMPLES / 4: - a = 0 - if a >= skip_cycle + 2: - tmp = self.fetch_long_data_from_LA(a, 1) - self.dchans[0].load_data(e, tmp) - # print (self.dchans[0].timestamps) - return [1e-6 * (self.dchans[0].timestamps[skip_cycle + 1] - self.dchans[0].timestamps[0])] - time.sleep(0.1) - return [] - except Exception as ex: - self.raiseException(ex, "Communication Error , Function : " + inspect.currentframe().f_code.co_name) - - def f2f_time(self, channel, skip_cycle=0, timeout=5): - """ - Return a list of falling edges that occured within the timeout period. - - .. tabularcolumns:: |p{3cm}|p{11cm}| - - ============== ============================================================================================================== - **Arguments** - ============== ============================================================================================================== - channel The input to measure time between two falling edges.['ID1','ID2','ID3','ID4','SEN','EXT','CNTR'] - skip_cycle Number of points to skip. eg. Pendulums pass through light barriers twice every cycle. SO 1 must be skipped - timeout Number of seconds to wait for datapoints. (Maximum 60 seconds) - ============== ============================================================================================================== - - :return list: Array of points - - """ - try: - if timeout > 60: timeout = 60 - self.start_one_channel_LA(channel=channel, channel_mode=2, trigger_mode=0) # every falling edge - startTime = time.time() - while time.time() - startTime < timeout: - a, b, c, d, e = self.get_LA_initial_states() - if a == self.MAX_SAMPLES / 4: - a = 0 - if a >= skip_cycle + 2: - tmp = self.fetch_long_data_from_LA(a, 1) - self.dchans[0].load_data(e, tmp) - # print (self.dchans[0].timestamps) - return [1e-6 * (self.dchans[0].timestamps[skip_cycle + 1] - self.dchans[0].timestamps[0])] - time.sleep(0.1) - return [] - except Exception as ex: - self.raiseException(ex, "Communication Error , Function : " + inspect.currentframe().f_code.co_name) - - def MeasureInterval(self, channel1, channel2, edge1, edge2, timeout=0.1): - """ - Measures time intervals between two logic level changes on any two digital inputs(both can be the same) - - For example, one can measure the time interval between the occurence of a rising edge on ID1, and a falling edge on ID3. - If the returned time is negative, it simply means that the event corresponding to channel2 occurred first. - - returns the calculated time - - - .. tabularcolumns:: |p{3cm}|p{11cm}| - - ============== ============================================================================================ - **Arguments** - ============== ============================================================================================ - channel1 The input pin to measure first logic level change - channel2 The input pin to measure second logic level change - -['ID1','ID2','ID3','ID4','SEN','EXT','CNTR'] - edge1 The type of level change to detect in order to start the timer - * 'rising' - * 'falling' - * 'four rising edges' - edge2 The type of level change to detect in order to stop the timer - * 'rising' - * 'falling' - * 'four rising edges' - timeout Use the timeout option if you're unsure of the input signal time period. - returns -1 if timed out - ============== ============================================================================================ - - :return : time - - .. seealso:: timing_example_ - - - """ - try: - self.H.__sendByte__(CP.TIMING) - self.H.__sendByte__(CP.INTERVAL_MEASUREMENTS) - timeout_msb = int((timeout * 64e6)) >> 16 - self.H.__sendInt__(timeout_msb) - - self.H.__sendByte__(self.__calcDChan__(channel1) | (self.__calcDChan__(channel2) << 4)) - - params = 0 - if edge1 == 'rising': - params |= 3 - elif edge1 == 'falling': - params |= 2 - else: - params |= 4 - - if edge2 == 'rising': - params |= 3 << 3 - elif edge2 == 'falling': - params |= 2 << 3 - else: - params |= 4 << 3 - - self.H.__sendByte__(params) - A = self.H.__getLong__() - B = self.H.__getLong__() - tmt = self.H.__getInt__() - self.H.__get_ack__() - # self.__print__(A,B) - if (tmt >= timeout_msb or B == 0): return np.NaN - rtime = lambda t: t / 64e6 - return rtime(B - A + 20) - except Exception as ex: - self.raiseException(ex, "Communication Error , Function : " + inspect.currentframe().f_code.co_name) - - def DutyCycle(self, channel='ID1', timeout=1.): - """ - duty cycle measurement on channel - - returns wavelength(seconds), and length of first half of pulse(high time) - - low time = (wavelength - high time) - - .. tabularcolumns:: |p{3cm}|p{11cm}| - - ============== ============================================================================================== - **Arguments** - ============== ============================================================================================== - channel The input pin to measure wavelength and high time.['ID1','ID2','ID3','ID4','SEN','EXT','CNTR'] - timeout Use the timeout option if you're unsure of the input signal time period. - returns 0 if timed out - ============== ============================================================================================== - - :return : wavelength,duty cycle - - .. seealso:: timing_example_ - - """ - try: - x, y = self.MeasureMultipleDigitalEdges(channel, channel, 'rising', 'falling', 2, 2, timeout, zero=True) - if x != None and y != None: # Both timers registered something. did not timeout - if y[0] > 0: # rising edge occured first - dt = [y[0], x[1]] - else: # falling edge occured first - if y[1] > x[1]: - return -1, -1 # Edge dropped. return False - dt = [y[1], x[1]] - # self.__print__(x,y,dt) - params = dt[1], dt[0] / dt[1] - if params[1] > 0.5: - self.__print__(x, y, dt) - return params - else: - return -1, -1 - except Exception as ex: - self.raiseException(ex, "Communication Error , Function : " + inspect.currentframe().f_code.co_name) - - def PulseTime(self, channel='ID1', PulseType='LOW', timeout=0.1): - """ - duty cycle measurement on channel - - returns wavelength(seconds), and length of first half of pulse(high time) - - low time = (wavelength - high time) - - .. tabularcolumns:: |p{3cm}|p{11cm}| - - ============== ============================================================================================== - **Arguments** - ============== ============================================================================================== - channel The input pin to measure wavelength and high time.['ID1','ID2','ID3','ID4','SEN','EXT','CNTR'] - PulseType Type of pulse to detect. May be 'HIGH' or 'LOW' - timeout Use the timeout option if you're unsure of the input signal time period. - returns 0 if timed out - ============== ============================================================================================== - - :return : pulse width - - .. seealso:: timing_example_ - - """ - try: - x, y = self.MeasureMultipleDigitalEdges(channel, channel, 'rising', 'falling', 2, 2, timeout, zero=True) - if x != None and y != None: # Both timers registered something. did not timeout - if y[0] > 0: # rising edge occured first - if PulseType == 'HIGH': - return y[0] - elif PulseType == 'LOW': - return x[1] - y[0] - else: # falling edge occured first - if PulseType == 'HIGH': - return y[1] - elif PulseType == 'LOW': - return abs(y[0]) - return -1, -1 - except Exception as ex: - self.raiseException(ex, "Communication Error , Function : " + inspect.currentframe().f_code.co_name) - - def MeasureMultipleDigitalEdges(self, channel1, channel2, edgeType1, edgeType2, points1, points2, timeout=0.1, - **kwargs): - """ - Measures a set of timestamped logic level changes(Type can be selected) from two different digital inputs. - - Example - Aim : Calculate value of gravity using time of flight. - The setup involves a small metal nut attached to an electromagnet powered via SQ1. - When SQ1 is turned off, the set up is designed to make the nut fall through two - different light barriers(LED,detector pairs that show a logic change when an object gets in the middle) - placed at known distances from the initial position. - - one can measure the timestamps for rising edges on ID1 ,and ID2 to determine the speed, and then obtain value of g - - - .. tabularcolumns:: |p{3cm}|p{11cm}| - - ============== ============================================================================================ - **Arguments** - ============== ============================================================================================ - channel1 The input pin to measure first logic level change - channel2 The input pin to measure second logic level change - -['ID1','ID2','ID3','ID4','SEN','EXT','CNTR'] - edgeType1 The type of level change that should be recorded - * 'rising' - * 'falling' - * 'four rising edges' [default] - edgeType2 The type of level change that should be recorded - * 'rising' - * 'falling' - * 'four rising edges' - points1 Number of data points to obtain for input 1 (Max 4) - points2 Number of data points to obtain for input 2 (Max 4) - timeout Use the timeout option if you're unsure of the input signal time period. - returns -1 if timed out - **kwargs - SQ1 set the state of SQR1 output(LOW or HIGH) and then start the timer. eg. SQR1='LOW' - zero subtract the timestamp of the first point from all the others before returning. default:True - ============== ============================================================================================ - - :return : time - - .. seealso:: timing_example_ - - - """ - try: - self.H.__sendByte__(CP.TIMING) - self.H.__sendByte__(CP.TIMING_MEASUREMENTS) - timeout_msb = int((timeout * 64e6)) >> 16 - # print ('timeout',timeout_msb) - self.H.__sendInt__(timeout_msb) - self.H.__sendByte__(self.__calcDChan__(channel1) | (self.__calcDChan__(channel2) << 4)) - params = 0 - if edgeType1 == 'rising': - params |= 3 - elif edgeType1 == 'falling': - params |= 2 - else: - params |= 4 - - if edgeType2 == 'rising': - params |= 3 << 3 - elif edgeType2 == 'falling': - params |= 2 << 3 - else: - params |= 4 << 3 - - if ('SQR1' in kwargs): # User wants to toggle SQ1 before starting the timer - params |= (1 << 6) - if kwargs['SQR1'] == 'HIGH': params |= (1 << 7) - self.H.__sendByte__(params) - if points1 > 4: points1 = 4 - if points2 > 4: points2 = 4 - self.H.__sendByte__(points1 | (points2 << 4)) # Number of points to fetch from either channel - - self.H.waitForData(timeout) - - A = np.array([self.H.__getLong__() for a in range(points1)]) - B = np.array([self.H.__getLong__() for a in range(points2)]) - tmt = self.H.__getInt__() - self.H.__get_ack__() - # print(A,B) - if (tmt >= timeout_msb): return None, None - rtime = lambda t: t / 64e6 - if (kwargs.get('zero', True)): # User wants set a reference timestamp - return rtime(A - A[0]), rtime(B - A[0]) - else: - return rtime(A), rtime(B) - except Exception as ex: - self.raiseException(ex, "Communication Error , Function : " + inspect.currentframe().f_code.co_name) - - def capture_edges1(self, waiting_time=1., **args): - """ - log timestamps of rising/falling edges on one digital input - - .. tabularcolumns:: |p{3cm}|p{11cm}| - - ================= ====================================================================================================== - **Arguments** - ================= ====================================================================================================== - waiting_time Total time to allow the logic analyzer to collect data. - This is implemented using a simple sleep routine, so if large delays will be involved, - refer to :func:`start_one_channel_LA` to start the acquisition, and :func:`fetch_LA_channels` to - retrieve data from the hardware after adequate time. The retrieved data is stored - in the array self.dchans[0].timestamps. - keyword arguments - channel 'ID1',...,'ID4' - trigger_channel 'ID1',...,'ID4' - channel_mode acquisition mode\n - default value: 3 - - - EVERY_SIXTEENTH_RISING_EDGE = 5 - - EVERY_FOURTH_RISING_EDGE = 4 - - EVERY_RISING_EDGE = 3 - - EVERY_FALLING_EDGE = 2 - - EVERY_EDGE = 1 - - DISABLED = 0 - - trigger_mode same as channel_mode. - default_value : 3 - - ================= ====================================================================================================== - - :return: timestamp array in Seconds - - >>> I.capture_edges(0.2,channel='ID1',trigger_channel='ID1',channel_mode=3,trigger_mode = 3) - #captures rising edges only. with rising edge trigger on ID1 - - """ - aqchan = args.get('channel', 'ID1') - trchan = args.get('trigger_channel', aqchan) - - aqmode = args.get('channel_mode', 3) - trmode = args.get('trigger_mode', 3) - - try: - self.start_one_channel_LA(channel=aqchan, channel_mode=aqmode, trigger_channel=trchan, trigger_mode=trmode) - - time.sleep(waiting_time) - - data = self.get_LA_initial_states() - tmp = self.fetch_long_data_from_LA(data[0], 1) - # data[4][0] -> initial state - return tmp / 64e6 - except Exception as ex: - self.raiseException(ex, "Communication Error , Function : " + inspect.currentframe().f_code.co_name) - - def start_one_channel_LA_backup__(self, trigger=1, channel='ID1', maximum_time=67, **args): - """ - start logging timestamps of rising/falling edges on ID1 - - .. tabularcolumns:: |p{3cm}|p{11cm}| - - ================== ====================================================================================================== - **Arguments** - ================== ====================================================================================================== - trigger Bool . Enable edge trigger on ID1. use keyword argument edge='rising' or 'falling' - channel ['ID1','ID2','ID3','ID4','SEN','EXT','CNTR'] - maximum_time Total time to sample. If total time exceeds 67 seconds, a prescaler will be used in the reference clock - kwargs - triggger_channels array of digital input names that can trigger the acquisition.eg. trigger= ['ID1','ID2','ID3'] - will triggger when a logic change specified by the keyword argument 'edge' occurs - on either or the three specified trigger inputs. - edge 'rising' or 'falling' . trigger edge type for trigger_channels. - ================== ====================================================================================================== - - :return: Nothing - - """ - try: - self.clear_buffer(0, self.MAX_SAMPLES / 2) - self.H.__sendByte__(CP.TIMING) - self.H.__sendByte__(CP.START_ONE_CHAN_LA) - self.H.__sendInt__(self.MAX_SAMPLES / 4) - # trigchan bit functions - # b0 - trigger or not - # b1 - trigger edge . 1 => rising. 0 => falling - # b2, b3 - channel to acquire data from. ID1,ID2,ID3,ID4,COMPARATOR - # b4 - trigger channel ID1 - # b5 - trigger channel ID2 - # b6 - trigger channel ID3 - - if ('trigger_channels' in args) and trigger & 1: - trigchans = args.get('trigger_channels', 0) - if 'ID1' in trigchans: trigger |= (1 << 4) - if 'ID2' in trigchans: trigger |= (1 << 5) - if 'ID3' in trigchans: trigger |= (1 << 6) - else: - trigger |= 1 << (self.__calcDChan__( - channel) + 4) # trigger on specified input channel if not trigger_channel argument provided - - trigger |= 2 if args.get('edge', 0) == 'rising' else 0 - trigger |= self.__calcDChan__(channel) << 2 - - self.H.__sendByte__(trigger) - self.H.__get_ack__() - self.digital_channels_in_buffer = 1 - for a in self.dchans: - a.prescaler = 0 - a.datatype = 'long' - a.length = self.MAX_SAMPLES / 4 - a.maximum_time = maximum_time * 1e6 # conversion to uS - a.mode = self.EVERY_EDGE - - except Exception as ex: - self.raiseException(ex, "Communication Error , Function : " + inspect.currentframe().f_code.co_name) - - # def start_one_channel_LA(self,**args): - """ - start logging timestamps of rising/falling edges on ID1 - - .. tabularcolumns:: |p{3cm}|p{11cm}| - - ================== ====================================================================================================== - **Arguments** - ================== ====================================================================================================== - args - channel ['ID1','ID2','ID3','ID4','SEN','EXT','CNTR'] - trigger_channel ['ID1','ID2','ID3','ID4','SEN','EXT','CNTR'] - - channel_mode acquisition mode\n - default value: 1(EVERY_EDGE) - - - EVERY_SIXTEENTH_RISING_EDGE = 5 - - EVERY_FOURTH_RISING_EDGE = 4 - - EVERY_RISING_EDGE = 3 - - EVERY_FALLING_EDGE = 2 - - EVERY_EDGE = 1 - - DISABLED = 0 - - trigger_edge 1=Falling edge - 0=Rising Edge - -1=Disable Trigger - - ================== ====================================================================================================== - - :return: Nothing - - self.clear_buffer(0,self.MAX_SAMPLES/2); - self.H.__sendByte__(CP.TIMING) - self.H.__sendByte__(CP.START_ONE_CHAN_LA) - self.H.__sendInt__(self.MAX_SAMPLES/4) - aqchan = self.__calcDChan__(args.get('channel','ID1')) - aqmode = args.get('channel_mode',1) - - if 'trigger_channel' in args: - trchan = self.__calcDChan__(args.get('trigger_channel','ID1')) - tredge = args.get('trigger_edge',0) - self.__print__('trigger chan',trchan,' trigger edge ',tredge) - if tredge!=-1: - self.H.__sendByte__((trchan<<4)|(tredge<<1)|1) - else: - self.H.__sendByte__(0) #no triggering - elif 'trigger_edge' in args: - tredge = args.get('trigger_edge',0) - if tredge!=-1: - self.H.__sendByte__((aqchan<<4)|(tredge<<1)|1) #trigger on acquisition channel - else: - self.H.__sendByte__(0) #no triggering - else: - self.H.__sendByte__(0) #no triggering - - self.H.__sendByte__((aqchan<<4)|aqmode) - - - self.H.__get_ack__() - self.digital_channels_in_buffer = 1 - - a = self.dchans[0] - a.prescaler = 0 - a.datatype='long' - a.length = self.MAX_SAMPLES/4 - a.maximum_time = 67*1e6 #conversion to uS - a.mode = args.get('channel_mode',1) - a.initial_state_override=False - ''' - if trmode in [3,4,5]: - a.initial_state_override = 2 - elif trmode == 2: - a.initial_state_override = 1 - ''' - """ - - def start_one_channel_LA(self, **args): - """ - start logging timestamps of rising/falling edges on ID1 - - .. tabularcolumns:: |p{3cm}|p{11cm}| - - ================== ====================================================================================================== - **Arguments** - ================== ====================================================================================================== - args - channel ['ID1','ID2','ID3','ID4','SEN','EXT','CNTR'] - - channel_mode acquisition mode. - default value: 1 - - - EVERY_SIXTEENTH_RISING_EDGE = 5 - - EVERY_FOURTH_RISING_EDGE = 4 - - EVERY_RISING_EDGE = 3 - - EVERY_FALLING_EDGE = 2 - - EVERY_EDGE = 1 - - DISABLED = 0 - - - ================== ====================================================================================================== - - :return: Nothing - - see :ref:`LA_video` - - """ - # trigger_channel ['ID1','ID2','ID3','ID4','SEN','EXT','CNTR'] - # trigger_mode same as channel_mode. - # default_value : 3 - try: - self.clear_buffer(0, self.MAX_SAMPLES / 2) - self.H.__sendByte__(CP.TIMING) - self.H.__sendByte__(CP.START_ALTERNATE_ONE_CHAN_LA) - self.H.__sendInt__(self.MAX_SAMPLES / 4) - aqchan = self.__calcDChan__(args.get('channel', 'ID1')) - aqmode = args.get('channel_mode', 1) - trchan = self.__calcDChan__(args.get('trigger_channel', 'ID1')) - trmode = args.get('trigger_mode', 3) - - self.H.__sendByte__((aqchan << 4) | aqmode) - self.H.__sendByte__((trchan << 4) | trmode) - self.H.__get_ack__() - self.digital_channels_in_buffer = 1 - - a = self.dchans[0] - a.prescaler = 0 - a.datatype = 'long' - a.length = self.MAX_SAMPLES / 4 - a.maximum_time = 67 * 1e6 # conversion to uS - a.mode = args.get('channel_mode', 1) - a.name = args.get('channel', 'ID1') - - if trmode in [3, 4, 5]: - a.initial_state_override = 2 - elif trmode == 2: - a.initial_state_override = 1 - except Exception as ex: - self.raiseException(ex, "Communication Error , Function : " + inspect.currentframe().f_code.co_name) - - def start_two_channel_LA(self, **args): - """ - start logging timestamps of rising/falling edges on ID1,AD2 - - .. tabularcolumns:: |p{3cm}|p{11cm}| - - ============== ======================================================================================================= - **Arguments** - ============== ======================================================================================================= - trigger Bool . Enable rising edge trigger on ID1 - \*\*args - chans Channels to acquire data from . default ['ID1','ID2'] - modes modes for each channel. Array .\n - default value: [1,1] - - - EVERY_SIXTEENTH_RISING_EDGE = 5 - - EVERY_FOURTH_RISING_EDGE = 4 - - EVERY_RISING_EDGE = 3 - - EVERY_FALLING_EDGE = 2 - - EVERY_EDGE = 1 - - DISABLED = 0 - - maximum_time Total time to sample. If total time exceeds 67 seconds, a prescaler will be used in the reference clock - - ============== ======================================================================================================= - - :: - - "fetch_long_data_from_dma(samples,1)" to get data acquired from channel 1 - "fetch_long_data_from_dma(samples,2)" to get data acquired from channel 2 - The read data can be accessed from self.dchans[0 or 1] - """ - # Trigger not working up to expectations. DMA keeps dumping Null values even though not triggered. - - # trigger True/False : Whether or not to trigger the Logic Analyzer using the first channel of the two. - # trig_type 'rising' / 'falling' . Type of logic change to trigger on - # trig_chan channel to trigger on . Any digital input. default chans[0] - - - modes = args.get('modes', [1, 1]) - strchans = args.get('chans', ['ID1', 'ID2']) - chans = [self.__calcDChan__(strchans[0]), self.__calcDChan__(strchans[1])] # Convert strings to index - maximum_time = args.get('maximum_time', 67) - trigger = args.get('trigger', 0) - if trigger: - trigger = 1 - if args.get('edge', 'rising') == 'falling': trigger |= 2 - trigger |= (self.__calcDChan__(args.get('trig_chan', strchans[0])) << 4) - # print (args.get('trigger',0),args.get('edge'),args.get('trig_chan',strchans[0]),hex(trigger),args) - else: - trigger = 0 - - try: - self.clear_buffer(0, self.MAX_SAMPLES) - self.H.__sendByte__(CP.TIMING) - self.H.__sendByte__(CP.START_TWO_CHAN_LA) - self.H.__sendInt__(self.MAX_SAMPLES / 4) - self.H.__sendByte__(trigger) - - self.H.__sendByte__((modes[1] << 4) | modes[0]) # Modes. four bits each - self.H.__sendByte__((chans[1] << 4) | chans[0]) # Channels. four bits each - self.H.__get_ack__() - n = 0 - for a in self.dchans[:2]: - a.prescaler = 0 - a.length = self.MAX_SAMPLES / 4 - a.datatype = 'long' - a.maximum_time = maximum_time * 1e6 # conversion to uS - a.mode = modes[n] - a.channel_number = chans[n] - a.name = strchans[n] - n += 1 - self.digital_channels_in_buffer = 2 - except Exception as ex: - self.raiseException(ex, "Communication Error , Function : " + inspect.currentframe().f_code.co_name) - - def start_three_channel_LA(self, **args): - """ - start logging timestamps of rising/falling edges on ID1,ID2,ID3 - - .. tabularcolumns:: |p{3cm}|p{11cm}| - - ================== ====================================================================================================== - **Arguments** - ================== ====================================================================================================== - args - trigger_channel ['ID1','ID2','ID3','ID4','SEN','EXT','CNTR'] - - modes modes for each channel. Array .\n - default value: [1,1,1] - - - EVERY_SIXTEENTH_RISING_EDGE = 5 - - EVERY_FOURTH_RISING_EDGE = 4 - - EVERY_RISING_EDGE = 3 - - EVERY_FALLING_EDGE = 2 - - EVERY_EDGE = 1 - - DISABLED = 0 - - trigger_mode same as modes(previously documented keyword argument) - default_value : 3 - - ================== ====================================================================================================== - - :return: Nothing - - """ - try: - self.clear_buffer(0, self.MAX_SAMPLES) - self.H.__sendByte__(CP.TIMING) - self.H.__sendByte__(CP.START_THREE_CHAN_LA) - self.H.__sendInt__(self.MAX_SAMPLES / 4) - modes = args.get('modes', [1, 1, 1, 1]) - trchan = self.__calcDChan__(args.get('trigger_channel', 'ID1')) - trmode = args.get('trigger_mode', 3) - - self.H.__sendInt__(modes[0] | (modes[1] << 4) | (modes[2] << 8)) - self.H.__sendByte__((trchan << 4) | trmode) - - self.H.__get_ack__() - self.digital_channels_in_buffer = 3 - - n = 0 - for a in self.dchans[:3]: - a.prescaler = 0 - a.length = self.MAX_SAMPLES / 4 - a.datatype = 'int' - a.maximum_time = 1e3 # < 1 mS between each consecutive level changes in the input signal must be ensured to prevent rollover - a.mode = modes[n] - a.name = a.digital_channel_names[n] - if trmode in [3, 4, 5]: - a.initial_state_override = 2 - elif trmode == 2: - a.initial_state_override = 1 - n += 1 - except Exception as ex: - self.raiseException(ex, "Communication Error , Function : " + inspect.currentframe().f_code.co_name) - - def start_four_channel_LA(self, trigger=1, maximum_time=0.001, mode=[1, 1, 1, 1], **args): - """ - Four channel Logic Analyzer. - start logging timestamps from a 64MHz counter to record level changes on ID1,ID2,ID3,ID4. - - .. tabularcolumns:: |p{3cm}|p{11cm}| - - ============== ============================================================================================ - **Arguments** - ============== ============================================================================================ - trigger Bool . Enable rising edge trigger on ID1 - - maximum_time Maximum delay expected between two logic level changes.\n - If total time exceeds 1 mS, a prescaler will be used in the reference clock - However, this only refers to the maximum time between two successive level changes. If a delay larger - than .26 S occurs, it will be truncated by modulo .26 S.\n - If you need to record large intervals, try single channel/two channel modes which use 32 bit counters - capable of time interval up to 67 seconds. - - mode modes for each channel. List with four elements\n - default values: [1,1,1,1] - - - EVERY_SIXTEENTH_RISING_EDGE = 5 - - EVERY_FOURTH_RISING_EDGE = 4 - - EVERY_RISING_EDGE = 3 - - EVERY_FALLING_EDGE = 2 - - EVERY_EDGE = 1 - - DISABLED = 0 - - ============== ============================================================================================ - - :return: Nothing - - .. seealso:: - - Use :func:`fetch_long_data_from_LA` (points to read,x) to get data acquired from channel x. - The read data can be accessed from :class:`~ScienceLab.dchans` [x-1] - """ - self.clear_buffer(0, self.MAX_SAMPLES) - prescale = 0 - """ - if(maximum_time > 0.26): - #self.__print__('too long for 4 channel. try 2/1 channels') - prescale = 3 - elif(maximum_time > 0.0655): - prescale = 3 - elif(maximum_time > 0.008191): - prescale = 2 - elif(maximum_time > 0.0010239): - prescale = 1 - """ - try: - self.H.__sendByte__(CP.TIMING) - self.H.__sendByte__(CP.START_FOUR_CHAN_LA) - self.H.__sendInt__(self.MAX_SAMPLES / 4) - self.H.__sendInt__(mode[0] | (mode[1] << 4) | (mode[2] << 8) | (mode[3] << 12)) - self.H.__sendByte__(prescale) # prescaler - trigopts = 0 - trigopts |= 4 if args.get('trigger_ID1', 0) else 0 - trigopts |= 8 if args.get('trigger_ID2', 0) else 0 - trigopts |= 16 if args.get('trigger_ID3', 0) else 0 - if (trigopts == 0): trigger |= 4 # select one trigger channel(ID1) if none selected - trigopts |= 2 if args.get('edge', 0) == 'rising' else 0 - trigger |= trigopts - self.H.__sendByte__(trigger) - self.H.__get_ack__() - self.digital_channels_in_buffer = 4 - n = 0 - for a in self.dchans: - a.prescaler = prescale - a.length = self.MAX_SAMPLES / 4 - a.datatype = 'int' - a.name = a.digital_channel_names[n] - a.maximum_time = maximum_time * 1e6 # conversion to uS - a.mode = mode[n] - n += 1 - except Exception as ex: - self.raiseException(ex, "Communication Error , Function : " + inspect.currentframe().f_code.co_name) - - def get_LA_initial_states(self): - """ - fetches the initial states of digital inputs that were recorded right before the Logic analyzer was started, and the total points each channel recorded - - :return: chan1 progress,chan2 progress,chan3 progress,chan4 progress,[ID1,ID2,ID3,ID4]. eg. [1,0,1,1] - """ - try: - self.H.__sendByte__(CP.TIMING) - self.H.__sendByte__(CP.GET_INITIAL_DIGITAL_STATES) - initial = self.H.__getInt__() - A = (self.H.__getInt__() - initial) / 2 - B = (self.H.__getInt__() - initial) / 2 - self.MAX_SAMPLES / 4 - C = (self.H.__getInt__() - initial) / 2 - 2 * self.MAX_SAMPLES / 4 - D = (self.H.__getInt__() - initial) / 2 - 3 * self.MAX_SAMPLES / 4 - s = self.H.__getByte__() - s_err = self.H.__getByte__() - self.H.__get_ack__() - - if A == 0: A = self.MAX_SAMPLES / 4 - if B == 0: B = self.MAX_SAMPLES / 4 - if C == 0: C = self.MAX_SAMPLES / 4 - if D == 0: D = self.MAX_SAMPLES / 4 - - if A < 0: A = 0 - if B < 0: B = 0 - if C < 0: C = 0 - if D < 0: D = 0 - except Exception as ex: - self.raiseException(ex, "Communication Error , Function : " + inspect.currentframe().f_code.co_name) - - return A, B, C, D, {'ID1': (s & 1 != 0), 'ID2': (s & 2 != 0), 'ID3': (s & 4 != 0), 'ID4': (s & 8 != 0), - 'SEN': (s & 16 != 16)} # SEN is inverted comparator output. - - def stop_LA(self): - """ - Stop any running logic analyzer function - """ - try: - self.H.__sendByte__(CP.TIMING) - self.H.__sendByte__(CP.STOP_LA) - self.H.__get_ack__() - except Exception as ex: - self.raiseException(ex, "Communication Error , Function : " + inspect.currentframe().f_code.co_name) - - def fetch_int_data_from_LA(self, bytes, chan=1): - """ - fetches the data stored by DMA. integer address increments - - .. tabularcolumns:: |p{3cm}|p{11cm}| - - ============== ============================================================================================ - **Arguments** - ============== ============================================================================================ - bytes: number of readings(integers) to fetch - chan: channel number (1-4) - ============== ============================================================================================ - """ - try: - self.H.__sendByte__(CP.TIMING) - self.H.__sendByte__(CP.FETCH_INT_DMA_DATA) - self.H.__sendInt__(bytes) - self.H.__sendByte__(chan - 1) - - ss = self.H.fd.read(int(bytes * 2)) - t = np.zeros(bytes * 2) - for a in range(int(bytes)): - t[a] = CP.ShortInt.unpack(ss[a * 2:a * 2 + 2])[0] - - self.H.__get_ack__() - except Exception as ex: - self.raiseException(ex, "Communication Error , Function : " + inspect.currentframe().f_code.co_name) - - t = np.trim_zeros(t) - b = 1 - rollovers = 0 - while b < len(t): - if (t[b] < t[b - 1] and t[b] != 0): - rollovers += 1 - t[b:] += 65535 - b += 1 - return t - - def fetch_long_data_from_LA(self, bytes, chan=1): - """ - fetches the data stored by DMA. long address increments - - .. tabularcolumns:: |p{3cm}|p{11cm}| - - ============== ============================================================================================ - **Arguments** - ============== ============================================================================================ - bytes: number of readings(long integers) to fetch - chan: channel number (1,2) - ============== ============================================================================================ - """ - try: - self.H.__sendByte__(CP.TIMING) - self.H.__sendByte__(CP.FETCH_LONG_DMA_DATA) - self.H.__sendInt__(bytes) - self.H.__sendByte__(chan - 1) - ss = self.H.fd.read(int(bytes * 4)) - self.H.__get_ack__() - tmp = np.zeros(bytes) - for a in range(int(bytes)): - tmp[a] = CP.Integer.unpack(ss[a * 4:a * 4 + 4])[0] - tmp = np.trim_zeros(tmp) - return tmp - except Exception as ex: - self.raiseException(ex, "Communication Error , Function : " + inspect.currentframe().f_code.co_name) - - def fetch_LA_channels(self): - """ - reads and stores the channels in self.dchans. - - """ - try: - data = self.get_LA_initial_states() - # print (data) - for a in range(4): - if (self.dchans[a].channel_number < self.digital_channels_in_buffer): self.__fetch_LA_channel__(a, data) - return True - except Exception as ex: - self.raiseException(ex, "Communication Error , Function : " + inspect.currentframe().f_code.co_name) - - def __fetch_LA_channel__(self, channel_number, initial_states): - try: - s = initial_states[4] - a = self.dchans[channel_number] - if a.channel_number >= self.digital_channels_in_buffer: - self.__print__('channel unavailable') - return False - - samples = a.length - if a.datatype == 'int': - tmp = self.fetch_int_data_from_LA(initial_states[a.channel_number], a.channel_number + 1) - a.load_data(s, tmp) - else: - tmp = self.fetch_long_data_from_LA(initial_states[a.channel_number * 2], a.channel_number + 1) - a.load_data(s, tmp) - - # offset=0 - # a.timestamps -= offset - a.generate_axes() - return True - except Exception as ex: - self.raiseException(ex, "Communication Error , Function : " + inspect.currentframe().f_code.co_name) - - def get_states(self): - """ - gets the state of the digital inputs. returns dictionary with keys 'ID1','ID2','ID3','ID4' - - >>> self.__print__(get_states()) - {'ID1': True, 'ID2': True, 'ID3': True, 'ID4': False} - - """ - try: - self.H.__sendByte__(CP.DIN) - self.H.__sendByte__(CP.GET_STATES) - s = self.H.__getByte__() - self.H.__get_ack__() - return {'ID1': (s & 1 != 0), 'ID2': (s & 2 != 0), 'ID3': (s & 4 != 0), 'ID4': (s & 8 != 0)} - except Exception as ex: - self.raiseException(ex, "Communication Error , Function : " + inspect.currentframe().f_code.co_name) - - def get_state(self, input_id): - """ - returns the logic level on the specified input (ID1,ID2,ID3, or ID4) - - .. tabularcolumns:: |p{3cm}|p{11cm}| - - ============== ============================================================================================ - **Arguments** Description - ============== ============================================================================================ - input_id the input channel - 'ID1' -> state of ID1 - 'ID4' -> state of ID4 - ============== ============================================================================================ - - >>> self.__print__(I.get_state(I.ID1)) - False - - """ - return self.get_states()[input_id] - - def set_state(self, **kwargs): - """ - - set the logic level on digital outputs SQR1,SQR2,SQR3,SQR4 - - .. tabularcolumns:: |p{3cm}|p{11cm}| - - ============== ============================================================================================ - **Arguments** - ============== ============================================================================================ - \*\*kwargs SQR1,SQR2,SQR3,SQR4 - states(0 or 1) - ============== ============================================================================================ - - >>> I.set_state(SQR1=1,SQR2=0) - sets SQR1 HIGH, SQR2 LOw, but leave SQR3,SQR4 untouched. - - """ - data = 0 - if 'SQR1' in kwargs: - data |= 0x10 | (kwargs.get('SQR1')) - if 'SQR2' in kwargs: - data |= 0x20 | (kwargs.get('SQR2') << 1) - if 'SQR3' in kwargs: - data |= 0x40 | (kwargs.get('SQR3') << 2) - if 'SQR4' in kwargs: - data |= 0x80 | (kwargs.get('SQR4') << 3) - try: - self.H.__sendByte__(CP.DOUT) - self.H.__sendByte__(CP.SET_STATE) - self.H.__sendByte__(data) - self.H.__get_ack__() - except Exception as ex: - self.raiseException(ex, "Communication Error , Function : " + inspect.currentframe().f_code.co_name) - - def countPulses(self, channel='SEN'): - """ - - Count pulses on a digital input. Retrieve total pulses using readPulseCount - - .. tabularcolumns:: |p{3cm}|p{11cm}| - - ============== ============================================================================================ - **Arguments** - ============== ============================================================================================ - channel The input pin to measure rising edges on : ['ID1','ID2','ID3','ID4','SEN','EXT','CNTR'] - ============== ============================================================================================ - """ - try: - self.H.__sendByte__(CP.COMMON) - self.H.__sendByte__(CP.START_COUNTING) - self.H.__sendByte__(self.__calcDChan__(channel)) - self.H.__get_ack__() - except Exception as ex: - self.raiseException(ex, "Communication Error , Function : " + inspect.currentframe().f_code.co_name) - - def readPulseCount(self): - """ - - Read pulses counted using a digital input. Call countPulses before using this. - - .. tabularcolumns:: |p{3cm}|p{11cm}| - - ============== ============================================================================================ - **Arguments** - ============== ============================================================================================ - ============== ============================================================================================ - """ - try: - self.H.__sendByte__(CP.COMMON) - self.H.__sendByte__(CP.FETCH_COUNT) - count = self.H.__getInt__() - self.H.__get_ack__() - return count - except Exception as ex: - self.raiseException(ex, "Communication Error , Function : " + inspect.currentframe().f_code.co_name) - - def __charge_cap__(self, state, t): - try: - self.H.__sendByte__(CP.ADC) - self.H.__sendByte__(CP.SET_CAP) - self.H.__sendByte__(state) - self.H.__sendInt__(t) - self.H.__get_ack__() - except Exception as ex: - self.raiseException(ex, "Communication Error , Function : " + inspect.currentframe().f_code.co_name) - - def __capture_capacitance__(self, samples, tg): - from PSL.analyticsClass import analyticsClass - self.AC = analyticsClass() - self.__charge_cap__(1, 50000) - try: - x, y = self.capture_fullspeed_hr('CAP', samples, tg, 'READ_CAP') - except Exception as ex: - self.raiseException(ex, "Communication Error , Function : " + inspect.currentframe().f_code.co_name) - try: - fitres = self.AC.fit_exp(x * 1e-6, y) - if fitres: - cVal, newy = fitres - # from pylab import * - # plot(x,newy) - # show() - return x, y, newy, cVal - else: - return None - except Exception as ex: - raise RuntimeError(" Fit Failed ") - - def capacitance_via_RC_discharge(self): - cap = self.get_capacitor_range()[1] - T = 2 * cap * 20e3 * 1e6 # uS - samples = 500 - try: - if T > 5000 and T < 10e6: - if T > 50e3: samples = 250 - RC = self.__capture_capacitance__(samples, int(T / samples))[3][1] - return RC / 10e3 - else: - self.__print__('cap out of range %f %f' % (T, cap)) - return 0 - except Exception as e: - self.__print__(e) - return 0 - - def __get_capacitor_range__(self, ctime): - try: - self.__charge_cap__(0, 30000) - self.H.__sendByte__(CP.COMMON) - self.H.__sendByte__(CP.GET_CAP_RANGE) - self.H.__sendInt__(ctime) - V_sum = self.H.__getInt__() - self.H.__get_ack__() - V = V_sum * 3.3 / 16 / 4095 - C = -ctime * 1e-6 / 1e4 / np.log(1 - V / 3.3) - return V, C - except Exception as ex: - self.raiseException(ex, "Communication Error , Function : " + inspect.currentframe().f_code.co_name) - - def get_capacitor_range(self): - """ - Charges a capacitor connected to IN1 via a 20K resistor from a 3.3V source for a fixed interval - Returns the capacitance calculated using the formula Vc = Vs(1-exp(-t/RC)) - This function allows an estimation of the parameters to be used with the :func:`get_capacitance` function. - - """ - t = 10 - P = [1.5, 50e-12] - for a in range(4): - P = list(self.__get_capacitor_range__(50 * (10 ** a))) - if (P[0] > 1.5): - if a == 0 and P[0] > 3.28: # pico farads range. Values will be incorrect using this method - P[1] = 50e-12 - break - return P - - def get_capacitance(self): # time in uS - """ - measures capacitance of component connected between CAP and ground - - - :return: Capacitance (F) - - Constant Current Charging - - .. math:: - - Q_{stored} = C*V - - I_{constant}*time = C*V - - C = I_{constant}*time/V_{measured} - - Also uses Constant Voltage Charging via 20K resistor if required. - - """ - GOOD_VOLTS = [2.5, 2.8] - CT = 10 - CR = 1 - iterations = 0 - start_time = time.time() - try: - while (time.time() - start_time) < 1: - # self.__print__('vals',CR,',',CT) - if CT > 65000: - self.__print__('CT too high') - return self.capacitance_via_RC_discharge() - V, C = self.__get_capacitance__(CR, 0, CT) - # print(CR,CT,V,C) - if CT > 30000 and V < 0.1: - self.__print__('Capacitance too high for this method') - return 0 - - elif V > GOOD_VOLTS[0] and V < GOOD_VOLTS[1]: - return C - elif V < GOOD_VOLTS[0] and V > 0.01 and CT < 40000: - if GOOD_VOLTS[0] / V > 1.1 and iterations < 10: - CT = int(CT * GOOD_VOLTS[0] / V) - iterations += 1 - self.__print__('increased CT ', CT) - elif iterations == 10: - return 0 - else: - return C - elif V <= 0.1 and CR < 3: - CR += 1 - elif CR == 3: - self.__print__('Capture mode ') - return self.capacitance_via_RC_discharge() - except Exception as ex: - self.raiseException(ex, "Communication Error , Function : " + inspect.currentframe().f_code.co_name) - - def __calibrate_ctmu__(self, scalers): - # self.currents=[0.55e-3/scalers[0],0.55e-6/scalers[1],0.55e-5/scalers[2],0.55e-4/scalers[3]] - self.currents = [0.55e-3, 0.55e-6, 0.55e-5, 0.55e-4] - self.currentScalers = scalers - - # print (self.currentScalers,scalers,self.SOCKET_CAPACITANCE) - - def __get_capacitance__(self, current_range, trim, Charge_Time): # time in uS - try: - self.__charge_cap__(0, 30000) - self.H.__sendByte__(CP.COMMON) - self.H.__sendByte__(CP.GET_CAPACITANCE) - self.H.__sendByte__(current_range) - if (trim < 0): - self.H.__sendByte__(int(31 - abs(trim) / 2) | 32) - else: - self.H.__sendByte__(int(trim / 2)) - self.H.__sendInt__(Charge_Time) - time.sleep(Charge_Time * 1e-6 + .02) - VCode = self.H.__getInt__() - V = 3.3 * VCode / 4095 - self.H.__get_ack__() - Charge_Current = self.currents[current_range] * (100 + trim) / 100.0 - if V: - C = (Charge_Current * Charge_Time * 1e-6 / V - self.SOCKET_CAPACITANCE) / self.currentScalers[ - current_range] - else: - C = 0 - # self.__print__('Current if C=470pF :',V*(470e-12+self.SOCKET_CAPACITANCE)/(Charge_Time*1e-6)) - return V, C - except Exception as ex: - self.raiseException(ex, "Communication Error , Function : " + inspect.currentframe().f_code.co_name) - - def get_temperature(self): - """ - return the processor's temperature - - :return: Chip Temperature in degree Celcius - """ - cs = 3 - V = self.get_ctmu_voltage(0b11110, cs, 0) - - if cs == 1: - return (646 - V * 1000) / 1.92 # current source = 1 - elif cs == 2: - return (701.5 - V * 1000) / 1.74 # current source = 2 - elif cs == 3: - return (760 - V * 1000) / 1.56 # current source = 3 - - def get_ctmu_voltage(self, channel, Crange, tgen=1): - """ - get_ctmu_voltage(5,2) will activate a constant current source of 5.5uA on IN1 and then measure the voltage at the output. - If a diode is used to connect IN1 to ground, the forward voltage drop of the diode will be returned. e.g. .6V for a 4148diode. - - If a resistor is connected, ohm's law will be followed within reasonable limits - - channel=5 for IN1 - - CRange=0 implies 550uA - CRange=1 implies 0.55uA - CRange=2 implies 5.5uA - CRange=3 implies 55uA - - :return: Voltage - """ - if channel == 'CAP': channel = 5 - try: - self.H.__sendByte__(CP.COMMON) - self.H.__sendByte__(CP.GET_CTMU_VOLTAGE) - self.H.__sendByte__((channel) | (Crange << 5) | (tgen << 7)) - - # V = [self.H.__getInt__() for a in range(16)] - # print(V) - # V=V[3:] - v = self.H.__getInt__() # 16*voltage across the current source - # v=sum(V) - - self.H.__get_ack__() - V = 3.3 * v / 16 / 4095. - # print(V) - return V - except Exception as ex: - self.raiseException(ex, "Communication Error , Function : " + inspect.currentframe().f_code.co_name) - - def __start_ctmu__(self, Crange, trim, tgen=1): - try: - self.H.__sendByte__(CP.COMMON) - self.H.__sendByte__(CP.START_CTMU) - self.H.__sendByte__((Crange) | (tgen << 7)) - self.H.__sendByte__(trim) - self.H.__get_ack__() - except Exception as ex: - self.raiseException(ex, "Communication Error , Function : " + inspect.currentframe().f_code.co_name) - - def __stop_ctmu__(self): - try: - self.H.__sendByte__(CP.COMMON) - self.H.__sendByte__(CP.STOP_CTMU) - self.H.__get_ack__() - except Exception as ex: - self.raiseException(ex, "Communication Error , Function : " + inspect.currentframe().f_code.co_name) - - def resetHardware(self): - """ - Resets the device, and standalone mode will be enabled if an OLED is connected to the I2C port - """ - try: - self.H.__sendByte__(CP.COMMON) - self.H.__sendByte__(CP.RESTORE_STANDALONE) - except Exception as ex: - self.raiseException(ex, "Communication Error , Function : " + inspect.currentframe().f_code.co_name) - - def read_flash(self, page, location): - """ - Reads 16 BYTES from the specified location - - .. tabularcolumns:: |p{3cm}|p{11cm}| - - ================ ============================================================================================ - **Arguments** - ================ ============================================================================================ - page page number. 20 pages with 2KBytes each - location The flash location(0 to 63) to read from . - ================ ============================================================================================ - - :return: a string of 16 characters read from the location - """ - try: - self.H.__sendByte__(CP.FLASH) - self.H.__sendByte__(CP.READ_FLASH) - self.H.__sendByte__(page) # send the page number. 20 pages with 2K bytes each - self.H.__sendByte__(location) # send the location - ss = self.H.fd.read(16) - self.H.__get_ack__() - return ss - except Exception as ex: - self.raiseException(ex, "Communication Error , Function : " + inspect.currentframe().f_code.co_name) - - def __stoa__(self, s): - return [ord(a) for a in s] - - def __atos__(self, a): - return ''.join(chr(e) for e in a) - - def read_bulk_flash(self, page, numbytes): - """ - Reads BYTES from the specified location - - .. tabularcolumns:: |p{3cm}|p{11cm}| - - ================ ============================================================================================ - **Arguments** - ================ ============================================================================================ - page Block number. 0-20. each block is 2kB. - numbytes Total bytes to read - ================ ============================================================================================ - - :return: a string of 16 characters read from the location - """ - try: - self.H.__sendByte__(CP.FLASH) - self.H.__sendByte__(CP.READ_BULK_FLASH) - bytes_to_read = numbytes - if numbytes % 2: bytes_to_read += 1 # bytes+1 . stuff is stored as integers (byte+byte) in the hardware - self.H.__sendInt__(bytes_to_read) - self.H.__sendByte__(page) - ss = self.H.fd.read(int(bytes_to_read)) - self.H.__get_ack__() - self.__print__('Read from ', page, ',', bytes_to_read, ' :', self.__stoa__(ss[:40]), '...') - if numbytes % 2: return ss[:-1] # Kill the extra character we read. Don't surprise the user with extra data - return ss - except Exception as ex: - self.raiseException(ex, "Communication Error , Function : " + inspect.currentframe().f_code.co_name) - - def write_flash(self, page, location, string_to_write): - """ - write a 16 BYTE string to the selected location (0-63) - - DO NOT USE THIS UNLESS YOU'RE ABSOLUTELY SURE KNOW THIS! - YOU MAY END UP OVERWRITING THE CALIBRATION DATA, AND WILL HAVE - TO GO THROUGH THE TROUBLE OF GETTING IT FROM THE MANUFACTURER AND - REFLASHING IT. - - .. tabularcolumns:: |p{3cm}|p{11cm}| - - ================ ============================================================================================ - **Arguments** - ================ ============================================================================================ - page page number. 20 pages with 2KBytes each - location The flash location(0 to 63) to write to. - string_to_write a string of 16 characters can be written to each location - ================ ============================================================================================ - - """ - try: - while (len(string_to_write) < 16): string_to_write += '.' - self.H.__sendByte__(CP.FLASH) - self.H.__sendByte__(CP.WRITE_FLASH) # indicate a flash write coming through - self.H.__sendByte__(page) # send the page number. 20 pages with 2K bytes each - self.H.__sendByte__(location) # send the location - self.H.fd.write(string_to_write) - time.sleep(0.1) - self.H.__get_ack__() - except Exception as ex: - self.raiseException(ex, "Communication Error , Function : " + inspect.currentframe().f_code.co_name) - - def write_bulk_flash(self, location, data): - """ - write a byte array to the entire flash page. Erases any other data - - DO NOT USE THIS UNLESS YOU'RE ABSOLUTELY SURE YOU KNOW THIS! - YOU MAY END UP OVERWRITING THE CALIBRATION DATA, AND WILL HAVE - TO GO THROUGH THE TROUBLE OF GETTING IT FROM THE MANUFACTURER AND - REFLASHING IT. - - .. tabularcolumns:: |p{3cm}|p{11cm}| - - ================ ============================================================================================ - **Arguments** - ================ ============================================================================================ - location Block number. 0-20. each block is 2kB. - bytearray Array to dump onto flash. Max size 2048 bytes - ================ ============================================================================================ - - """ - if (type(data) == str): data = [ord(a) for a in data] - if len(data) % 2 == 1: data.append(0) - try: - # self.__print__('Dumping at',location,',',len(bytearray),' bytes into flash',bytearray[:10]) - self.H.__sendByte__(CP.FLASH) - self.H.__sendByte__(CP.WRITE_BULK_FLASH) # indicate a flash write coming through - self.H.__sendInt__(len(data)) # send the length - self.H.__sendByte__(location) - for n in range(len(data)): - self.H.__sendByte__(data[n]) - # Printer('Bytes written: %d'%(n+1)) - self.H.__get_ack__() - except Exception as ex: - self.raiseException(ex, "Communication Error , Function : " + inspect.currentframe().f_code.co_name) - - # verification by readback - tmp = [ord(a) for a in self.read_bulk_flash(location, len(data))] - print('Verification done', tmp == data) - if tmp != data: raise Exception('Verification by readback failed') - - # -------------------------------------------------------------------------------------------------------------------# - - # |===============================================WAVEGEN SECTION====================================================| - # |This section has commands related to waveform generators W1, W2, PWM outputs, servo motor control etc. | - # -------------------------------------------------------------------------------------------------------------------# - - def set_wave(self, chan, freq): - """ - Set the frequency of wavegen - - .. tabularcolumns:: |p{3cm}|p{11cm}| - - ============== ============================================================================================ - **Arguments** - ============== ============================================================================================ - chan Channel to set frequency for. W1 or W2 - frequency Frequency to set on wave generator - ============== ============================================================================================ - - - :return: frequency - """ - if chan == 'W1': - self.set_w1(freq) - elif chan == 'W2': - self.set_w2(freq) - - def set_sine1(self, freq): - """ - Set the frequency of wavegen 1 after setting its waveform type to sinusoidal - - .. tabularcolumns:: |p{3cm}|p{11cm}| - - ============== ============================================================================================ - **Arguments** - ============== ============================================================================================ - frequency Frequency to set on wave generator 1. - ============== ============================================================================================ - - - :return: frequency - """ - return self.set_w1(freq, 'sine') - - def set_sine2(self, freq): - """ - Set the frequency of wavegen 2 after setting its waveform type to sinusoidal - - .. tabularcolumns:: |p{3cm}|p{11cm}| - - ============== ============================================================================================ - **Arguments** - ============== ============================================================================================ - frequency Frequency to set on wave generator 1. - ============== ============================================================================================ - - - :return: frequency - """ - return self.set_w2(freq, 'sine') - - def set_w1(self, freq, waveType=None): - """ - Set the frequency of wavegen 1 - - .. tabularcolumns:: |p{3cm}|p{11cm}| - - ============== ============================================================================================ - **Arguments** - ============== ============================================================================================ - frequency Frequency to set on wave generator 1. - waveType 'sine','tria' . Default : Do not reload table. and use last set table - ============== ============================================================================================ - - - :return: frequency - """ - if freq < 0.1: - self.__print__('freq too low') - return 0 - elif freq < 1100: - HIGHRES = 1 - table_size = 512 - else: - HIGHRES = 0 - table_size = 32 - - if waveType: # User wants to set a particular waveform type. sine or tria - if waveType in ['sine', 'tria']: - if (self.WType['W1'] != waveType): - self.load_equation('W1', waveType) - else: - print('Not a valid waveform. try sine or tria') - - p = [1, 8, 64, 256] - prescaler = 0 - while prescaler <= 3: - wavelength = int(round(64e6 / freq / p[prescaler] / table_size)) - freq = (64e6 / wavelength / p[prescaler] / table_size) - if wavelength < 65525: break - prescaler += 1 - if prescaler == 4: - self.__print__('out of range') - return 0 - - try: - self.H.__sendByte__(CP.WAVEGEN) - self.H.__sendByte__(CP.SET_SINE1) - self.H.__sendByte__(HIGHRES | (prescaler << 1)) # use larger table for low frequencies - self.H.__sendInt__(wavelength - 1) - self.H.__get_ack__() - # if self.sine1freq == None: time.sleep(0.2) - self.sine1freq = freq - return freq - except Exception as ex: - self.raiseException(ex, "Communication Error , Function : " + inspect.currentframe().f_code.co_name) - - def set_w2(self, freq, waveType=None): - """ - Set the frequency of wavegen 2 - - .. tabularcolumns:: |p{3cm}|p{11cm}| - - ============== ============================================================================================ - **Arguments** - ============== ============================================================================================ - frequency Frequency to set on wave generator 1. - ============== ============================================================================================ - - :return: frequency - """ - if freq < 0.1: - self.__print__('freq too low') - return 0 - elif freq < 1100: - HIGHRES = 1 - table_size = 512 - else: - HIGHRES = 0 - table_size = 32 - - if waveType: # User wants to set a particular waveform type. sine or tria - if waveType in ['sine', 'tria']: - if (self.WType['W2'] != waveType): - self.load_equation('W2', waveType) - else: - print('Not a valid waveform. try sine or tria') - - p = [1, 8, 64, 256] - prescaler = 0 - while prescaler <= 3: - wavelength = int(round(64e6 / freq / p[prescaler] / table_size)) - freq = (64e6 / wavelength / p[prescaler] / table_size) - if wavelength < 65525: break - prescaler += 1 - if prescaler == 4: - self.__print__('out of range') - return 0 - try: - self.H.__sendByte__(CP.WAVEGEN) - self.H.__sendByte__(CP.SET_SINE2) - self.H.__sendByte__(HIGHRES | (prescaler << 1)) # use larger table for low frequencies - self.H.__sendInt__(wavelength - 1) - self.H.__get_ack__() - # if self.sine2freq == None: time.sleep(0.2) - self.sine2freq = freq - except Exception as ex: - self.raiseException(ex, "Communication Error , Function : " + inspect.currentframe().f_code.co_name) - - return freq - - def readbackWaveform(self, chan): - """ - Set the frequency of wavegen 1 - - .. tabularcolumns:: |p{3cm}|p{11cm}| - - ============== ============================================================================================ - **Arguments** - ============== ============================================================================================ - chan Any of W1,W2,SQR1,SQR2,SQR3,SQR4 - ============== ============================================================================================ - - - :return: frequency - """ - if chan == 'W1': - return self.sine1freq - elif chan == 'W2': - return self.sine2freq - elif chan[:3] == 'SQR': - return self.sqrfreq.get(chan, None) - - def set_waves(self, freq, phase, f2=None): - """ - Set the frequency of wavegen - - .. tabularcolumns:: |p{3cm}|p{11cm}| - - ============== ============================================================================================ - **Arguments** - ============== ============================================================================================ - frequency Frequency to set on both wave generators - phase Phase difference between the two. 0-360 degrees - f2 Only specify if you require two separate frequencies to be set - ============== ============================================================================================ - - :return: frequency - """ - if f2: - freq2 = f2 - else: - freq2 = freq - - if freq < 0.1: - self.__print__('freq1 too low') - return 0 - elif freq < 1100: - HIGHRES = 1 - table_size = 512 - else: - HIGHRES = 0 - table_size = 32 - - if freq2 < 0.1: - self.__print__('freq2 too low') - return 0 - elif freq2 < 1100: - HIGHRES2 = 1 - table_size2 = 512 - else: - HIGHRES2 = 0 - table_size2 = 32 - if freq < 1. or freq2 < 1.: - self.__print__('extremely low frequencies will have reduced amplitudes due to AC coupling restrictions') - - p = [1, 8, 64, 256] - prescaler1 = 0 - while prescaler1 <= 3: - wavelength = int(round(64e6 / freq / p[prescaler1] / table_size)) - retfreq = (64e6 / wavelength / p[prescaler1] / table_size) - if wavelength < 65525: break - prescaler1 += 1 - if prescaler1 == 4: - self.__print__('#1 out of range') - return 0 - - p = [1, 8, 64, 256] - prescaler2 = 0 - while prescaler2 <= 3: - wavelength2 = int(round(64e6 / freq2 / p[prescaler2] / table_size2)) - retfreq2 = (64e6 / wavelength2 / p[prescaler2] / table_size2) - if wavelength2 < 65525: break - prescaler2 += 1 - if prescaler2 == 4: - self.__print__('#2 out of range') - return 0 - - phase_coarse = int(table_size2 * (phase) / 360.) - phase_fine = int(wavelength2 * (phase - (phase_coarse) * 360. / table_size2) / (360. / table_size2)) - - try: - self.H.__sendByte__(CP.WAVEGEN) - self.H.__sendByte__(CP.SET_BOTH_WG) - - self.H.__sendInt__(wavelength - 1) # not really wavelength. time between each datapoint - self.H.__sendInt__(wavelength2 - 1) # not really wavelength. time between each datapoint - self.H.__sendInt__(phase_coarse) # table position for phase adjust - self.H.__sendInt__(phase_fine) # timer delay / fine phase adjust - - self.H.__sendByte__((prescaler2 << 4) | (prescaler1 << 2) | (HIGHRES2 << 1) | ( - HIGHRES)) # use larger table for low frequencies - self.H.__get_ack__() - # print ( phase_coarse,phase_fine) - # if self.sine1freq == None or self.sine2freq==None : time.sleep(0.2) - self.sine1freq = retfreq - self.sine2freq = retfreq2 - - return retfreq - except Exception as ex: - self.raiseException(ex, "Communication Error , Function : " + inspect.currentframe().f_code.co_name) - - def load_equation(self, chan, function, span=None, **kwargs): - ''' - Load an arbitrary waveform to the waveform generators - - .. tabularcolumns:: |p{3cm}|p{11cm}| - - ============== ============================================================================================ - **Arguments** - ============== ============================================================================================ - chan The waveform generator to alter. W1 or W2 - function A function that will be used to generate the datapoints - span the range of values in which to evaluate the given function - ============== ============================================================================================ - - .. code-block:: python - - fn = lambda x:abs(x-50) #Triangular waveform - self.I.load_waveform('W1',fn,[0,100]) - #Load triangular wave to wavegen 1 - - #Load sinusoidal wave to wavegen 2 - self.I.load_waveform('W2',np.sin,[0,2*np.pi]) - - ''' - if function == 'sine' or function == np.sin: - function = np.sin - span = [0, 2 * np.pi] - self.WType[chan] = 'sine' - elif function == 'tria': - function = lambda x: abs(x % 4 - 2) - 1 - span = [-1, 3] - self.WType[chan] = 'tria' - else: - self.WType[chan] = 'arbit' - - self.__print__('reloaded wave equation for %s : %s' % (chan, self.WType[chan])) - x1 = np.linspace(span[0], span[1], 512 + 1)[:-1] - y1 = function(x1) - self.load_table(chan, y1, self.WType[chan], **kwargs) - - def load_table(self, chan, points, mode='arbit', **kwargs): - ''' - Load an arbitrary waveform table to the waveform generators - - .. tabularcolumns:: |p{3cm}|p{11cm}| - - ============== ============================================================================================ - **Arguments** - ============== ============================================================================================ - chan The waveform generator to alter. 'W1' or 'W2' - points A list of 512 datapoints exactly - mode Optional argument. Type of waveform. default value 'arbit'. accepts 'sine', 'tria' - ============== ============================================================================================ - - example:: - - >>> self.I.load_waveform_table(1,range(512)) - #Load sawtooth wave to wavegen 1 - ''' - self.__print__('reloaded wave table for %s : %s' % (chan, mode)) - self.WType[chan] = mode - chans = ['W1', 'W2'] - if chan in chans: - num = chans.index(chan) + 1 - else: - print('Channel does not exist. Try W2 or W2') - return - - # Normalize and scale . - # y1 = array with 512 points between 0 and 512 - # y2 = array with 32 points between 0 and 64 - - amp = kwargs.get('amp', 0.95) - LARGE_MAX = 511 * amp # A form of amplitude control. This decides the max PWM duty cycle out of 512 clocks - SMALL_MAX = 63 * amp # Max duty cycle out of 64 clocks - y1 = np.array(points) - y1 -= min(y1) - y1 = y1 / float(max(y1)) - y1 = 1. - y1 - y1 = list(np.int16(np.round(LARGE_MAX - LARGE_MAX * y1))) - - y2 = np.array(points[::16]) - y2 -= min(y2) - y2 = y2 / float(max(y2)) - y2 = 1. - y2 - y2 = list(np.int16(np.round(SMALL_MAX - SMALL_MAX * y2))) - - try: - self.H.__sendByte__(CP.WAVEGEN) - if (num == 1): - self.H.__sendByte__(CP.LOAD_WAVEFORM1) - elif (num == 2): - self.H.__sendByte__(CP.LOAD_WAVEFORM2) - - # print(max(y1),max(y2)) - for a in y1: - self.H.__sendInt__(a) - # time.sleep(0.001) - for a in y2: - self.H.__sendByte__(CP.Byte.pack(a)) - # time.sleep(0.001) - time.sleep(0.01) - self.H.__get_ack__() - except Exception as ex: - self.raiseException(ex, "Communication Error , Function : " + inspect.currentframe().f_code.co_name) - - def sqr1(self, freq, duty_cycle=50, onlyPrepare=False): - """ - Set the frequency of sqr1 - - .. tabularcolumns:: |p{3cm}|p{11cm}| - - ============== ============================================================================================ - **Arguments** - ============== ============================================================================================ - frequency Frequency - duty_cycle Percentage of high time - ============== ============================================================================================ - """ - if freq == 0 or duty_cycle == 0: return None - if freq > 10e6: - print('Frequency is greater than 10MHz. Please use map_reference_clock for 16 & 32MHz outputs') - return 0 - - p = [1, 8, 64, 256] - prescaler = 0 - while prescaler <= 3: - wavelength = int(64e6 / freq / p[prescaler]) - if wavelength < 65525: break - prescaler += 1 - if prescaler == 4 or wavelength == 0: - self.__print__('out of range') - return 0 - high_time = wavelength * duty_cycle / 100. - self.__print__(wavelength, ':', high_time, ':', prescaler) - if onlyPrepare: self.set_state(SQR1=False) - try: - self.H.__sendByte__(CP.WAVEGEN) - self.H.__sendByte__(CP.SET_SQR1) - self.H.__sendInt__(int(round(wavelength))) - self.H.__sendInt__(int(round(high_time))) - if onlyPrepare: prescaler |= 0x4 # Instruct hardware to prepare the square wave, but do not connect it to the output. - self.H.__sendByte__(prescaler) - self.H.__get_ack__() - except Exception as ex: - self.raiseException(ex, "Communication Error , Function : " + inspect.currentframe().f_code.co_name) - - self.sqrfreq['SQR1'] = 64e6 / wavelength / p[prescaler & 0x3] - return self.sqrfreq['SQR1'] - - def sqr1_pattern(self, timing_array): - """ - output a preset sqr1 frequency in fixed intervals. Can be used for sending IR signals that are packets - of 38KHz pulses. - refer to the example - - .. tabularcolumns:: |p{3cm}|p{11cm}| - - ============== ============================================================================================ - **Arguments** - ============== ============================================================================================ - timing_array A list of on & off times in uS units - ============== ============================================================================================ - - .. code-block:: python - I.sqr1(38e3 , 50, True ) # Prepare a 38KHz, 50% square wave. Do not output it yet - I.sqr1_pattern([1000,1000,1000,1000,1000]) #On:1mS (38KHz packet), Off:1mS, On:1mS (38KHz packet), Off:1mS, On:1mS (38KHz packet), Off: indefinitely.. - """ - self.fill_buffer(self.MAX_SAMPLES / 2, timing_array) # Load the array to the ADCBuffer(second half) - try: - self.H.__sendByte__(CP.WAVEGEN) - self.H.__sendByte__(CP.SQR1_PATTERN) - self.H.__sendInt__(len(timing_array)) - time.sleep(sum(timing_array) * 1e-6) # Sleep for the whole duration - self.H.__get_ack__() - except Exception as ex: - self.raiseException(ex, "Communication Error , Function : " + inspect.currentframe().f_code.co_name) - return True - - def sqr2(self, freq, duty_cycle): - """ - Set the frequency of sqr2 - - .. tabularcolumns:: |p{3cm}|p{11cm}| - - ============== ============================================================================================ - **Arguments** - ============== ============================================================================================ - frequency Frequency - duty_cycle Percentage of high time - ============== ============================================================================================ - """ - p = [1, 8, 64, 256] - prescaler = 0 - while prescaler <= 3: - wavelength = 64e6 / freq / p[prescaler] - if wavelength < 65525: break - prescaler += 1 - - if prescaler == 4 or wavelength == 0: - self.__print__('out of range') - return 0 - try: - high_time = wavelength * duty_cycle / 100. - self.__print__(wavelength, high_time, prescaler) - self.H.__sendByte__(CP.WAVEGEN) - self.H.__sendByte__(CP.SET_SQR2) - self.H.__sendInt__(int(round(wavelength))) - self.H.__sendInt__(int(round(high_time))) - self.H.__sendByte__(prescaler) - self.H.__get_ack__() - except Exception as ex: - self.raiseException(ex, "Communication Error , Function : " + inspect.currentframe().f_code.co_name) - - self.sqrfreq['SQR2'] = 64e6 / wavelength / p[prescaler & 0x3] - return self.sqrfreq['SQR2'] - - def set_sqrs(self, wavelength, phase, high_time1, high_time2, prescaler=1): - """ - Set the frequency of sqr1,sqr2, with phase shift - - .. tabularcolumns:: |p{3cm}|p{11cm}| - - ============== ============================================================================================ - **Arguments** - ============== ============================================================================================ - wavelength Number of 64Mhz/prescaler clock cycles per wave - phase Clock cycles between rising edges of SQR1 and SQR2 - high time1 Clock cycles for which SQR1 must be HIGH - high time2 Clock cycles for which SQR2 must be HIGH - prescaler 0,1,2. Divides the 64Mhz clock by 8,64, or 256 - ============== ============================================================================================ - - """ - try: - self.H.__sendByte__(CP.WAVEGEN) - self.H.__sendByte__(CP.SET_SQRS) - self.H.__sendInt__(wavelength) - self.H.__sendInt__(phase) - self.H.__sendInt__(high_time1) - self.H.__sendInt__(high_time2) - self.H.__sendByte__(prescaler) - self.H.__get_ack__() - except Exception as ex: - self.raiseException(ex, "Communication Error , Function : " + inspect.currentframe().f_code.co_name) - - def sqrPWM(self, freq, h0, p1, h1, p2, h2, p3, h3, **kwargs): - """ - Initialize phase correlated square waves on SQR1,SQR2,SQR3,SQR4 - - .. tabularcolumns:: |p{3cm}|p{11cm}| - - ============== ============================================================================================ - **Arguments** - ============== ============================================================================================ - freq Frequency in Hertz - h0 Duty Cycle for SQR1 (0-1) - p1 Phase shift for SQR2 (0-1) - h1 Duty Cycle for SQR2 (0-1) - p2 Phase shift for OD1 (0-1) - h2 Duty Cycle for OD1 (0-1) - p3 Phase shift for OD2 (0-1) - h3 Duty Cycle for OD2 (0-1) - ============== ============================================================================================ - - """ - if freq == 0 or h0 == 0 or h1 == 0 or h2 == 0 or h3 == 0: return 0 - if freq > 10e6: - print('Frequency is greater than 10MHz. Please use map_reference_clock for 16 & 32MHz outputs') - return 0 - - p = [1, 8, 64, 256] - prescaler = 0 - while prescaler <= 3: - wavelength = int(64e6 / freq / p[prescaler]) - if wavelength < 65525: break - prescaler += 1 - if prescaler == 4 or wavelength == 0: - self.__print__('out of range') - return 0 - - if not kwargs.get('pulse', False): prescaler |= (1 << 5) - - A1 = int(p1 % 1 * wavelength) - B1 = int((h1 + p1) % 1 * wavelength) - A2 = int(p2 % 1 * wavelength) - B2 = int((h2 + p2) % 1 * wavelength) - A3 = int(p3 % 1 * wavelength) - B3 = int((h3 + p3) % 1 * wavelength) - # self.__print__(p1,h1,p2,h2,p3,h3) - # print(wavelength,int(wavelength*h0),A1,B1,A2,B2,A3,B3,prescaler) - - - self.H.__sendByte__(CP.WAVEGEN) - self.H.__sendByte__(CP.SQR4) - self.H.__sendInt__(wavelength - 1) - self.H.__sendInt__(int(wavelength * h0) - 1) - try: - self.H.__sendInt__(max(0, A1 - 1)) - self.H.__sendInt__(max(1, B1 - 1)) - self.H.__sendInt__(max(0, A2 - 1)) - self.H.__sendInt__(max(1, B2 - 1)) - self.H.__sendInt__(max(0, A3 - 1)) - self.H.__sendInt__(max(1, B3 - 1)) - self.H.__sendByte__(prescaler) - self.H.__get_ack__() - except Exception as ex: - self.raiseException(ex, "Communication Error , Function : " + inspect.currentframe().f_code.co_name) - - for a in ['SQR1', 'SQR2', 'SQR3', 'SQR4']: self.sqrfreq[a] = 64e6 / wavelength / p[prescaler & 0x3] - return 64e6 / wavelength / p[prescaler & 0x3] - - def map_reference_clock(self, scaler, *args): - """ - Map the internal oscillator output to SQR1,SQR2,SQR3,SQR4 or WAVEGEN - The output frequency is 128/(1< 128MHz - * 1 -> 64MHz - * 2 -> 32MHz - * 3 -> 16MHz - * . - * . - * 15 ->128./32768 MHz - - example:: - - >>> I.map_reference_clock(2,'SQR1','SQR2') - - outputs 32 MHz on SQR1, SQR2 pins - - .. note:: - if you change the reference clock for 'wavegen' , the external waveform generator(AD9833) resolution and range will also change. - default frequency for 'wavegen' is 16MHz. Setting to 1MHz will give you 16 times better resolution, but a usable range of - 0Hz to about 100KHz instead of the original 2MHz. - - """ - try: - self.H.__sendByte__(CP.WAVEGEN) - self.H.__sendByte__(CP.MAP_REFERENCE) - chan = 0 - if 'SQR1' in args: chan |= 1 - if 'SQR2' in args: chan |= 2 - if 'SQR3' in args: chan |= 4 - if 'SQR4' in args: chan |= 8 - if 'WAVEGEN' in args: chan |= 16 - self.H.__sendByte__(chan) - self.H.__sendByte__(scaler) - if 'WAVEGEN' in args: self.DDS_CLOCK = 128e6 / (1 << scaler) - self.H.__get_ack__() - except Exception as ex: - self.raiseException(ex, "Communication Error , Function : " + inspect.currentframe().f_code.co_name) - - # -------------------------------------------------------------------------------------------------------------------# - - # |===============================================ANALOG OUTPUTS ====================================================| - # |This section has commands related to current and voltage sources PV1,PV2,PV3,PCS | - # -------------------------------------------------------------------------------------------------------------------# - - def set_pv1(self, val): - """ - Set the voltage on PV1 - 12-bit DAC... -5V to 5V - - .. tabularcolumns:: |p{3cm}|p{11cm}| - - ============== ============================================================================================ - **Arguments** - ============== ============================================================================================ - val Output voltage on PV1. -5V to 5V - ============== ============================================================================================ - - """ - return self.DAC.setVoltage('PV1', val) - - def set_pv2(self, val): - """ - Set the voltage on PV2. - 12-bit DAC... 0-3.3V - - .. tabularcolumns:: |p{3cm}|p{11cm}| - - ============== ============================================================================================ - **Arguments** - ============== ============================================================================================ - val Output voltage on PV2. 0-3.3V - ============== ============================================================================================ - - :return: Actual value set on pv2 - """ - return self.DAC.setVoltage('PV2', val) - - def set_pv3(self, val): - """ - Set the voltage on PV3 - - .. tabularcolumns:: |p{3cm}|p{11cm}| - - ============== ============================================================================================ - **Arguments** - ============== ============================================================================================ - val Output voltage on PV3. 0V to 3.3V - ============== ============================================================================================ - - :return: Actual value set on pv3 - """ - return self.DAC.setVoltage('PV3', val) - - def set_pcs(self, val): - """ - Set programmable current source - - .. tabularcolumns:: |p{3cm}|p{11cm}| - - ============== ============================================================================================ - **Arguments** - ============== ============================================================================================ - val Output current on PCS. 0 to 3.3mA. Subject to load resistance. Read voltage on PCS to check. - ============== ============================================================================================ - - :return: value attempted to set on pcs - """ - return self.DAC.setCurrent(val) - - def get_pv1(self): - """ - get the last set voltage on PV1 - 12-bit DAC... -5V to 5V - """ - return self.DAC.getVoltage('PV1') - - def get_pv2(self): - return self.DAC.getVoltage('PV2') - - def get_pv3(self): - return self.DAC.getVoltage('PV3') - - def get_pcs(self): - return self.DAC.getVoltage('PCS') - - def WS2812B(self, cols, output='CS1'): - """ - set shade of WS2182 LED on SQR1 - - .. tabularcolumns:: |p{3cm}|p{11cm}| - - ============== ============================================================================================ - **Arguments** - ============== ============================================================================================ - cols 2Darray [[R,G,B],[R2,G2,B2],[R3,G3,B3]...] - brightness of R,G,B ( 0-255 ) - ============== ============================================================================================ - - example:: - - >>> I.WS2812B([[10,0,0],[0,10,10],[10,0,10]]) - #sets red, cyan, magenta to three daisy chained LEDs - - see :ref:`rgb_video` - - - """ - if output == 'CS1': - pin = CP.SET_RGB1 - elif output == 'CS2': - pin = CP.SET_RGB2 - elif output == 'SQR1': - pin = CP.SET_RGB3 - else: - print('invalid output') - return - try: - self.H.__sendByte__(CP.COMMON) - self.H.__sendByte__(pin) - self.H.__sendByte__(len(cols) * 3) - for col in cols: - # R=reverse_bits(int(col[0]));G=reverse_bits(int(col[1]));B=reverse_bits(int(col[2])) - R = col[0] - G = col[1] - B = col[2] - self.H.__sendByte__(G) - self.H.__sendByte__(R) - self.H.__sendByte__(B) - # print(col) - self.H.__get_ack__() - except Exception as ex: - self.raiseException(ex, "Communication Error , Function : " + inspect.currentframe().f_code.co_name) - - # -------------------------------------------------------------------------------------------------------------------# - - # |======================================READ PROGRAM AND DATA ADDRESSES=============================================| - # |Direct access to RAM and FLASH | - # -------------------------------------------------------------------------------------------------------------------# - - def read_program_address(self, address): - """ - Reads and returns the value stored at the specified address in program memory - - .. tabularcolumns:: |p{3cm}|p{11cm}| - - ============== ============================================================================================ - **Arguments** - ============== ============================================================================================ - address Address to read from. Refer to PIC24EP64GP204 programming manual - ============== ============================================================================================ - """ - try: - self.H.__sendByte__(CP.COMMON) - self.H.__sendByte__(CP.READ_PROGRAM_ADDRESS) - self.H.__sendInt__(address & 0xFFFF) - self.H.__sendInt__((address >> 16) & 0xFFFF) - v = self.H.__getInt__() - self.H.__get_ack__() - return v - except Exception as ex: - self.raiseException(ex, "Communication Error , Function : " + inspect.currentframe().f_code.co_name) - - def device_id(self): - try: - a = self.read_program_address(0x800FF8) - b = self.read_program_address(0x800FFa) - c = self.read_program_address(0x800FFc) - d = self.read_program_address(0x800FFe) - val = d | (c << 16) | (b << 32) | (a << 48) - self.__print__(a, b, c, d, hex(val)) - return val - except Exception as ex: - self.raiseException(ex, "Communication Error , Function : " + inspect.currentframe().f_code.co_name) - - def __write_program_address__(self, address, value): - """ - Writes a value to the specified address in program memory. Disabled in firmware. - - .. tabularcolumns:: |p{3cm}|p{11cm}| - ============== ============================================================================================ - **Arguments** - ============== ============================================================================================ - address Address to write to. Refer to PIC24EP64GP204 programming manual - Do Not Screw around with this. It won't work anyway. - ============== ============================================================================================ - """ - try: - self.H.__sendByte__(CP.COMMON) - self.H.__sendByte__(CP.WRITE_PROGRAM_ADDRESS) - self.H.__sendInt__(address & 0xFFFF) - self.H.__sendInt__((address >> 16) & 0xFFFF) - self.H.__sendInt__(value) - self.H.__get_ack__() - except Exception as ex: - self.raiseException(ex, "Communication Error , Function : " + inspect.currentframe().f_code.co_name) - - def read_data_address(self, address): - """ - Reads and returns the value stored at the specified address in RAM - - .. tabularcolumns:: |p{3cm}|p{11cm}| - - ============== ============================================================================================ - **Arguments** - ============== ============================================================================================ - address Address to read from. Refer to PIC24EP64GP204 programming manual| - ============== ============================================================================================ - """ - try: - self.H.__sendByte__(CP.COMMON) - self.H.__sendByte__(CP.READ_DATA_ADDRESS) - self.H.__sendInt__(address & 0xFFFF) - v = self.H.__getInt__() - self.H.__get_ack__() - return v - except Exception as ex: - self.raiseException(ex, "Communication Error , Function : " + inspect.currentframe().f_code.co_name) - - def __write_data_address__(self, address, value): - """ - Writes a value to the specified address in RAM - - .. tabularcolumns:: |p{3cm}|p{11cm}| - - ============== ============================================================================================ - **Arguments** - ============== ============================================================================================ - address Address to write to. Refer to PIC24EP64GP204 programming manual| - ============== ============================================================================================ - """ - try: - self.H.__sendByte__(CP.COMMON) - self.H.__sendByte__(CP.WRITE_DATA_ADDRESS) - self.H.__sendInt__(address & 0xFFFF) - self.H.__sendInt__(value) - self.H.__get_ack__() - except Exception as ex: - self.raiseException(ex, "Communication Error , Function : " + inspect.currentframe().f_code.co_name) - - # -------------------------------------------------------------------------------------------------------------------# - - # |==============================================MOTOR SIGNALLING====================================================| - # |Set servo motor angles via SQ1-4. Control one stepper motor using SQ1-4 | - # -------------------------------------------------------------------------------------------------------------------# - - - def __stepperMotor__(self, steps, delay, direction): - try: - self.H.__sendByte__(CP.NONSTANDARD_IO) - self.H.__sendByte__(CP.STEPPER_MOTOR) - self.H.__sendInt__((steps << 1) | direction) - self.H.__sendInt__(delay) - except Exception as ex: - self.raiseException(ex, "Communication Error , Function : " + inspect.currentframe().f_code.co_name) - - time.sleep(steps * delay * 1e-3) # convert mS to S - - def stepForward(self, steps, delay): - """ - Control stepper motors using SQR1-4 - - take a fixed number of steps in the forward direction with a certain delay( in milliseconds ) between each step. - - """ - self.__stepperMotor__(steps, delay, 1) - - def stepBackward(self, steps, delay): - """ - Control stepper motors using SQR1-4 - - take a fixed number of steps in the backward direction with a certain delay( in milliseconds ) between each step. - - """ - self.__stepperMotor__(steps, delay, 0) - - def servo(self, angle, chan='SQR1'): - ''' - Output A PWM waveform on SQR1/SQR2 corresponding to the angle specified in the arguments. - This is used to operate servo motors. Tested with 9G SG-90 Servo motor. - - .. tabularcolumns:: |p{3cm}|p{11cm}| - - ============== ============================================================================================ - **Arguments** - ============== ============================================================================================ - angle 0-180. Angle corresponding to which the PWM waveform is generated. - chan 'SQR1' or 'SQR2'. Whether to use SQ1 or SQ2 to output the PWM waveform used by the servo - ============== ============================================================================================ - ''' - if chan == 'SQR1': - self.sqr1(100, 7.5 + 19. * angle / 180) # 100Hz - elif chan == 'SQR2': - self.sqr2(100, 7.5 + 19. * angle / 180) # 100Hz - - def servo4(self, a1, a2, a3, a4): - """ - Operate Four servo motors independently using SQR1, SQR2, SQR3, SQR4. - tested with SG-90 9G servos. - For high current servos, please use a different power source, and a level convertor for the PWm output signals(if needed) - - .. tabularcolumns:: |p{3cm}|p{11cm}| - - ============== ============================================================================================ - **Arguments** - ============== ============================================================================================ - a1 Angle to set on Servo which uses SQR1 as PWM input. [0-180] - a2 Angle to set on Servo which uses SQR2 as PWM input. [0-180] - a3 Angle to set on Servo which uses SQR3 as PWM input. [0-180] - a4 Angle to set on Servo which uses SQR4 as PWM input. [0-180] - ============== ============================================================================================ - - """ - try: - params = (1 << 5) | 2 # continuous waveform. prescaler 2( 1:64) - self.H.__sendByte__(CP.WAVEGEN) - self.H.__sendByte__(CP.SQR4) - self.H.__sendInt__(10000) # 10mS wavelength - self.H.__sendInt__(750 + int(a1 * 1900 / 180)) - self.H.__sendInt__(0) - self.H.__sendInt__(750 + int(a2 * 1900 / 180)) - self.H.__sendInt__(0) - self.H.__sendInt__(750 + int(a3 * 1900 / 180)) - self.H.__sendInt__(0) - self.H.__sendInt__(750 + int(a4 * 1900 / 180)) - self.H.__sendByte__(params) - self.H.__get_ack__() - except Exception as ex: - self.raiseException(ex, "Communication Error , Function : " + inspect.currentframe().f_code.co_name) - - def enableUartPassthrough(self, baudrate, persist=False): - ''' - All data received by the device is relayed to an external port(SCL[TX],SDA[RX]) after this function is called - - If a period > .5 seconds elapses between two transmit/receive events, the device resets - and resumes normal mode. This timeout feature has been implemented in lieu of a hard reset option. - can be used to load programs into secondary microcontrollers with bootloaders such ATMEGA, and ESP8266 - - - .. tabularcolumns:: |p{3cm}|p{11cm}| - - ============== ============================================================================================ - **Arguments** - ============== ============================================================================================ - baudrate BAUDRATE to use - persist If set to True, the device will stay in passthrough mode until the next power cycle. - Otherwise(default scenario), the device will return to normal operation if no data is sent/ - received for a period greater than one second at a time. - ============== ============================================================================================ - ''' - try: - self.H.__sendByte__(CP.PASSTHROUGHS) - self.H.__sendByte__(CP.PASS_UART) - self.H.__sendByte__(1 if persist else 0) - self.H.__sendInt__(int(round(((64e6 / baudrate) / 4) - 1))) - self.__print__('BRGVAL:', int(round(((64e6 / baudrate) / 4) - 1))) - time.sleep(0.1) - self.__print__('junk bytes read:', len(self.H.fd.read(100))) - except Exception as ex: - self.raiseException(ex, "Communication Error , Function : " + inspect.currentframe().f_code.co_name) - - def estimateDistance(self): - ''' - - Read data from ultrasonic distance sensor HC-SR04/HC-SR05. Sensors must have separate trigger and output pins. - First a 10uS pulse is output on SQR1. SQR1 must be connected to the TRIG pin on the sensor prior to use. - - Upon receiving this pulse, the sensor emits a sequence of sound pulses, and the logic level of its output - pin(which we will monitor via ID1) is also set high. The logic level goes LOW when the sound packet - returns to the sensor, or when a timeout occurs. - - The ultrasound sensor outputs a series of 8 sound pulses at 40KHz which corresponds to a time period - of 25uS per pulse. These pulses reflect off of the nearest object in front of the sensor, and return to it. - The time between sending and receiving of the pulse packet is used to estimate the distance. - If the reflecting object is either too far away or absorbs sound, less than 8 pulses may be received, and this - can cause a measurement error of 25uS which corresponds to 8mm. - - Ensure 5V supply. You may set SQR2 to HIGH [ I.set_state(SQR2=True) ] , and use that as the power supply. - - returns 0 upon timeout - ''' - try: - self.H.__sendByte__(CP.NONSTANDARD_IO) - self.H.__sendByte__(CP.HCSR04_HEADER) - - timeout_msb = int((0.3 * 64e6)) >> 16 - self.H.__sendInt__(timeout_msb) - - A = self.H.__getLong__() - B = self.H.__getLong__() - tmt = self.H.__getInt__() - self.H.__get_ack__() - # self.__print__(A,B) - if (tmt >= timeout_msb or B == 0): return 0 - rtime = lambda t: t / 64e6 - return 330. * rtime(B - A + 20) / 2. - except Exception as ex: - self.raiseException(ex, "Communication Error , Function : " + inspect.currentframe().f_code.co_name) - - """ - def TemperatureAndHumidity(self): - ''' - init AM2302. - This effort was a waste. There are better humidity and temperature sensors available which use well documented I2C - ''' - try: - self.H.__sendByte__(CP.NONSTANDARD_IO) - self.H.__sendByte__(CP.AM2302_HEADER) - - self.H.__get_ack__() - except Exception as ex: - self.raiseException(ex, "Communication Error , Function : "+inspect.currentframe().f_code.co_name) - self.digital_channels_in_buffer=1 - """ - - def opticalArray(self, SS, delay, channel='CH3', **kwargs): - ''' - read from 3648 element optical sensor array TCD3648P from Toshiba. Experimental feature. - Neither Sine waves will be available. - Connect SQR1 to MS , SQR2 to MS , A0 to CHannel , and CS1(on the expansion slot) to ICG - - delay : ICG low duration - tp : clock wavelength=tp*15nS, SS=clock/4 - - ''' - samples = 3694 - res = kwargs.get('resolution', 10) - tweak = kwargs.get('tweak', 1) - - try: - self.H.__sendByte__(CP.NONSTANDARD_IO) - self.H.__sendByte__(CP.TCD1304_HEADER) - if res == 10: - self.H.__sendByte__(self.__calcCHOSA__(channel)) # 10-bit - else: - self.H.__sendByte__(self.__calcCHOSA__(channel) | 0x80) # 12-bit - self.H.__sendByte__(tweak) # Tweak the SH low to ICG high space. =tweak*delay - self.H.__sendInt__(delay) - self.H.__sendInt__(int(SS * 64)) - self.timebase = SS - self.achans[0].set_params(channel=0, length=samples, timebase=self.timebase, - resolution=12 if res != 10 else 10, source=self.analogInputSources[channel]) - self.samples = samples - self.channels_in_buffer = 1 - time.sleep(2 * delay * 1e-6) - self.H.__get_ack__() - except Exception as ex: - self.raiseException(ex, "Communication Error , Function : " + inspect.currentframe().f_code.co_name) - - def setUARTBAUD(self, BAUD): - try: - self.H.__sendByte__(CP.UART_2) - self.H.__sendByte__(CP.SET_BAUD) - self.H.__sendInt__(int(round(((64e6 / BAUD) / 4) - 1))) - self.__print__('BRG2VAL:', int(round(((64e6 / BAUD) / 4) - 1))) - self.H.__get_ack__() - except Exception as ex: - self.raiseException(ex, "Communication Error , Function : " + inspect.currentframe().f_code.co_name) - - def writeUART(self, character): - try: - self.H.__sendByte__(CP.UART_2) - self.H.__sendByte__(CP.SEND_BYTE) - self.H.__sendByte__(character) - self.H.__get_ack__() - except Exception as ex: - self.raiseException(ex, "Communication Error , Function : " + inspect.currentframe().f_code.co_name) - - def readUART(self): - try: - self.H.__sendByte__(CP.UART_2) - self.H.__sendByte__(CP.READ_BYTE) - return self.H.__getByte__() - except Exception as ex: - self.raiseException(ex, "Communication Error , Function : " + inspect.currentframe().f_code.co_name) - - def readUARTStatus(self): - ''' - return available bytes in UART buffer - ''' - try: - self.H.__sendByte__(CP.UART_2) - self.H.__sendByte__(CP.READ_UART2_STATUS) - return self.H.__getByte__() - except Exception as ex: - self.raiseException(ex, "Communication Error , Function : " + inspect.currentframe().f_code.co_name) - - def readLog(self): - ''' - read hardware debug log. - ''' - try: - self.H.__sendByte__(CP.COMMON) - self.H.__sendByte__(CP.READ_LOG) - log = self.H.fd.readline().strip() - self.H.__get_ack__() - return log - except Exception as ex: - self.raiseException(ex, "Communication Error , Function : " + inspect.currentframe().f_code.co_name) - - def raiseException(self, ex, msg): - msg += '\n' + ex.message - # self.H.disconnect() - raise RuntimeError(msg) - - -if __name__ == "__main__": - print("""this is not an executable file - from PSL import sciencelab - I=sciencelab.connect() - eg. - I.get_average_voltage('CH1') - """) - I=connect(verbose = True) - t = time.time() - for a in range(100): - s = I.read_flash(3,a) - #print(s.replace('\n','.'),len(s)) - print (time.time()-t) diff --git a/PSL/sensorlist.py b/PSL/sensorlist.py deleted file mode 100644 index 4e3bc6d3..00000000 --- a/PSL/sensorlist.py +++ /dev/null @@ -1,42 +0,0 @@ -# from http://www.ladyada.net/library/i2caddr.html -sensors = { - 0x0: ['Could be MLX90614. Try 0x5A'], - 0x13: ['VCNL4000'], - 0x3c: ['OLED SSD1306', ], - 0x3d: ['OLED SSD1306', ], - 0x48: ['PN532 RFID'], - 0x29: ['TSL2561'], - 0x39: ['TSL2561'], - 0x49: ['TSL2561'], - 0x1D: ['ADXL345', 'MMA7455L', 'LSM9DSO'], - 0x53: ['ADXL345'], - 0x5A: ['MLX90614 PIR temperature'], - 0x1E: ['HMC5883L magnetometer', 'LSM303 magnetometer'], - 0x77: ['BMP180/GY-68 altimeter', 'MS5607', 'MS5611'], - 0x68: ['MPU-6050/GY-521 accel+gyro+temp', 'ITG3200', 'DS1307', 'DS3231'], - 0x69: ['ITG3200'], - 0x76: ['MS5607', 'MS5611'], - 0x6B: ['LSM9DSO gyro'], - 0x19: ['LSM303 accel'], - 0x20: ['MCP23008', 'MCP23017'], - 0x21: ['MCP23008', 'MCP23017'], - 0x22: ['MCP23008', 'MCP23017'], - 0x23: ['BH1750', 'MCP23008', 'MCP23017'], - 0x24: ['MCP23008', 'MCP23017'], - 0x25: ['MCP23008', 'MCP23017'], - 0x26: ['MCP23008', 'MCP23017'], - 0x27: ['MCP23008', 'MCP23017'], - 0x40: ['SHT21(Temp/RH)'], - 0x60: ['MCP4725A0 4 chan DAC (onBoard)'], - 0x61: ['MCP4725A0 4 chan DAC'], - 0x62: ['MCP4725A1 4 chan DAC'], - 0x63: ['MCP4725A1 4 chan DAC', 'Si4713'], - 0x64: ['MCP4725A2 4 chan DAC'], - 0x65: ['MCP4725A2 4 chan DAC'], - 0x66: ['MCP4725A3 4 chan DAC'], - 0x67: ['MCP4725A3 4 chan DAC'], - 0x11: ['Si4713'], - 0x38: ['FT6206 touch controller'], - 0x41: ['STMPE610'], - -} diff --git a/README.md b/README.md index 0b622ed9..f94a1c0f 100644 --- a/README.md +++ b/README.md @@ -1,87 +1,66 @@ -# PSLab-python +# PSLab Python Library -The Pocket Science Lab from FOSSASIA +The Python library for the [Pocket Science Lab](https://pslab.io) from FOSSASIA. -[![Build Status](https://travis-ci.org/fossasia/pslab-python.svg?branch=development)](https://travis-ci.org/fossasia/pslab-python) +[![Build Status](https://github.com/fossasia/pslab-python/actions/workflows/workflow.yml/badge.svg)](https://github.com/fossasia/pslab-python/actions/workflows/workflow.yml) [![Gitter](https://badges.gitter.im/fossasia/pslab.svg)](https://gitter.im/fossasia/pslab?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge) [![Codacy Badge](https://api.codacy.com/project/badge/Grade/ce4af216571846308f66da4b7f26efc7)](https://www.codacy.com/app/mb/pslab-python?utm_source=github.com&utm_medium=referral&utm_content=fossasia/pslab&utm_campaign=Badge_Grade) +[![Mailing List](https://img.shields.io/badge/Mailing%20List-FOSSASIA-blue.svg)](https://groups.google.com/forum/#!forum/pslab-fossasia) +[![Twitter Follow](https://img.shields.io/twitter/follow/pslabio.svg?style=social&label=Follow&maxAge=2592000?style=flat-square)](https://twitter.com/pslabio) -This repository hosts the python library for communicating with PSLab. This can be installed on a linux pc/raspberry pi. With this, one can communicate with the hardware using simple python code. +This repository hosts the Python library for communicating with the Pocket Science Lab open hardware platform (PSLab). Using this library you can communicate with the PSLab using simple Python code. The Python library is also used by the PSLab GUI as a backend component. -The goal of PSLab is to create an Open Source hardware device (open on all layers) that can be used for experiments by teachers, students and citizen scientists. Our tiny pocket lab provides an array of sensors for doing science and engineering experiments. It provides functions of numerous measurement devices including an oscilloscope, a waveform generator, a frequency counter, a programmable voltage, current source and as a data logger. Our website is at: http://pslab.fossasia.org +The goal of PSLab is to create an Open Source hardware device (open on all layers) and software applications that can be used for experiments by teachers, students and scientists. Our tiny pocket lab provides an array of instruments for doing science and engineering experiments. It provides functions of numerous measurement tools including an oscilloscope, a waveform generator, a logic analyzer, a programmable voltage and current source, and even a component to control robots with up to four servos. -### Communication +For more information see [https://pslab.io](https://pslab.io). -Please join us on the following channels: -* [Pocket Science Channel](https://gitter.im/fossasia/pslab) -* [Mailing List](https://groups.google.com/forum/#!forum/pslab-fossasia) +## Buy -### Installation +* You can get a Pocket Science Lab device from the [FOSSASIA Shop](https://fossasia.com). +* More resellers are listed on the [PSLab website](https://pslab.io/shop/). -To install PSLab on Debian based Gnu/Linux system, the following dependencies must be installed. +## Installation -#### Dependencies +pslab-python can be installed from PyPI: -* PyQt 4.7+, PySide, or PyQt5 -* python 2.6, 2.7, or 3.x -* NumPy, Scipy -* pyqt4-dev-tools   **For pyuic4** -* Pyqtgraph   **For Plotting library** -* pyopengl and qt-opengl   **For 3D graphics** -* iPython-qtconsole   **optional** + $ pip install pslab +**Note**: Linux users must either install a udev rule by running 'pslab install' as root, or be part of the 'dialout' group in order for pslab-python to be able to communicate with the PSLab device. -##### Now clone both the repositories [pslab-apps](https://github.com/fossasia/pslab-apps) and [pslab](https://github.com/fossasia/pslab). +**Note**: Windows users who use the PSLab v6 device must download and install the CP210x Windows Drivers from the [Silicon Labs website](https://www.silabs.com/developers/usb-to-uart-bridge-vcp-drivers?tab=downloads) in order for pslab-python to be able to communicate with the PSLab device. +**Note**: If you are only interested in using PSLab as an acquisition device without a display/GUI, only pslab-python needs to be installed. If you would like a GUI, install the [pslab-desktop app](https://github.com/fossasia/pslab-desktop) and follow the instructions of the Readme in that repo. -##### Libraries must be installed in any order -1. pslab-apps -2. pslab +## Validate installation -**Note** -*If user is only interested in using PSLab as an acquisition device without a display/GUI, only one repository [pslab](https://github.com/fossasia/pslab) needs to be installed* +1. Plug in the PSLab device and check that both the LEDs light up. +2. The following piece of code should run without errors: +``` +from pslab import ScienceLab +psl = ScienceLab() +capacitance = psl.multimeter.measure_capacitance() +print(capacitance) +``` +## Communication -##### To install, cd into the directories +* If you encounter any bugs, please file them in our [issue tracker](https://github.com/fossasia/pslab-python/issues). +* You can chat with the PSLab developers on [Gitter](https://gitter.im/fossasia/pslab). +* There is also a [mailing list](https://groups.google.com/forum/#!forum/pslab-fossasia). - $ cd +Wherever we interact, we strive to follow the [FOSSASIA Code of Conduct](https://fossasia.org/coc/). -and run the following (for both the repos) +## Contributing - $ sudo make clean +See [CONTRIBUTING.md](https://github.com/fossasia/pslab-python/blob/development/CONTRIBUTING.md) to get started. - $ sudo make +## License - $ sudo make install +Copyright (C) 2014-2021 FOSSASIA -Now you are ready with the PSLab software on your machine :) - -For the main GUI (Control panel), you can run Experiments from the terminal. - - $ Experiments - ------------------------ - -#### Development Environment - -To set up the development environment, install the packages mentioned in dependencies. For building GUI's Qt Designer is used. - -## Steps to build documentation - -First install sphinx by running following command - - pip install -U Sphinx - -Then go to pslab/docs and run the following command - - $ make html - -### Blog posts related to PSLab on FOSSASIA blog -* [Installation of PSLab](http://blog.fossasia.org/pslab-code-repository-and-installation/) -* [Communicating with PSLab](http://blog.fossasia.org/communicating-with-pocket-science-lab-via-usb-and-capturing-and-plotting-sine-waves/) -* [Features and Controls of PSLab](http://blog.fossasia.org/features-and-controls-of-pocket-science-lab/) -* [Design your own Experiments](http://blog.fossasia.org/design-your-own-experiments-with-pslab/) -* [New Tools and Sensors for Fossasia PSLab and ExpEYES](http://blog.fossasia.org/new-tools-and-sensors-fossasia-pslab-and-expeyes/) +This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, version 3. +This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. +You should have received a copy of the GNU General Public License along with this program. If not, see . diff --git a/doc-requirements.txt b/doc-requirements.txt new file mode 100644 index 00000000..2ff3af24 --- /dev/null +++ b/doc-requirements.txt @@ -0,0 +1 @@ +recommonmark \ No newline at end of file diff --git a/docs/CONTRIBUTING.md b/docs/CONTRIBUTING.md new file mode 120000 index 00000000..44fcc634 --- /dev/null +++ b/docs/CONTRIBUTING.md @@ -0,0 +1 @@ +../CONTRIBUTING.md \ No newline at end of file diff --git a/docs/PSL.SENSORS.rst b/docs/PSL.SENSORS.rst deleted file mode 100644 index 44c2ed52..00000000 --- a/docs/PSL.SENSORS.rst +++ /dev/null @@ -1,126 +0,0 @@ -PSL.SENSORS package -=================== - -Submodules ----------- - -PSL.SENSORS.AD7718_class module -------------------------------- - -.. automodule:: PSL.SENSORS.AD7718_class - :members: - :undoc-members: - :show-inheritance: - -PSL.SENSORS.AD9833 module -------------------------- - -.. automodule:: PSL.SENSORS.AD9833 - :members: - :undoc-members: - :show-inheritance: - -PSL.SENSORS.BH1750 module -------------------------- - -.. automodule:: PSL.SENSORS.BH1750 - :members: - :undoc-members: - :show-inheritance: - -PSL.SENSORS.BMP180 module -------------------------- - -.. automodule:: PSL.SENSORS.BMP180 - :members: - :undoc-members: - :show-inheritance: - -PSL.SENSORS.ComplementaryFilter module --------------------------------------- - -.. automodule:: PSL.SENSORS.ComplementaryFilter - :members: - :undoc-members: - :show-inheritance: - -PSL.SENSORS.HMC5883L module ---------------------------- - -.. automodule:: PSL.SENSORS.HMC5883L - :members: - :undoc-members: - :show-inheritance: - -PSL.SENSORS.Kalman module -------------------------- - -.. automodule:: PSL.SENSORS.Kalman - :members: - :undoc-members: - :show-inheritance: - -PSL.SENSORS.MF522 module ------------------------- - -.. automodule:: PSL.SENSORS.MF522 - :members: - :undoc-members: - :show-inheritance: - -PSL.SENSORS.MLX90614 module ---------------------------- - -.. automodule:: PSL.SENSORS.MLX90614 - :members: - :undoc-members: - :show-inheritance: - -PSL.SENSORS.MPU6050 module --------------------------- - -.. automodule:: PSL.SENSORS.MPU6050 - :members: - :undoc-members: - :show-inheritance: - -PSL.SENSORS.SHT21 module ------------------------- - -.. automodule:: PSL.SENSORS.SHT21 - :members: - :undoc-members: - :show-inheritance: - -PSL.SENSORS.SSD1306 module --------------------------- - -.. automodule:: PSL.SENSORS.SSD1306 - :members: - :undoc-members: - :show-inheritance: - -PSL.SENSORS.TSL2561 module --------------------------- - -.. automodule:: PSL.SENSORS.TSL2561 - :members: - :undoc-members: - :show-inheritance: - -PSL.SENSORS.supported module ----------------------------- - -.. automodule:: PSL.SENSORS.supported - :members: - :undoc-members: - :show-inheritance: - - -Module contents ---------------- - -.. automodule:: PSL.SENSORS - :members: - :undoc-members: - :show-inheritance: diff --git a/docs/PSL.rst b/docs/PSL.rst deleted file mode 100644 index 3b708840..00000000 --- a/docs/PSL.rst +++ /dev/null @@ -1,85 +0,0 @@ -PSLab@FOSSASIA package ------------- - -Subpackages ------------ - -.. toctree:: - - PSL.SENSORS - -Submodules ----------- - -PSL.Peripherals module ------------------------ - -.. automodule:: PSL.Peripherals - :members: - :undoc-members: - :show-inheritance: - -PSL.achan module ------------------ - -.. automodule:: PSL.achan - :members: - :undoc-members: - :show-inheritance: - -PSL.analyticsClass module --------------------------- - -.. automodule:: PSL.analyticsClass - :members: - :undoc-members: - :show-inheritance: - -PSL.commands_proto module --------------------------- - -.. automodule:: PSL.commands_proto - :members: - :undoc-members: - :show-inheritance: - -PSL.digital_channel module ---------------------------- - -.. automodule:: PSL.digital_channel - :members: - :undoc-members: - :show-inheritance: - -PSL.sciencelab module ---------------------- - -.. automodule:: PSL.sciencelab - :members: - :undoc-members: - :show-inheritance: - -PSL.packet_handler module --------------------------- - -.. automodule:: PSL.packet_handler - :members: - :undoc-members: - :show-inheritance: - -PSL.sensorlist module ----------------------- - -.. automodule:: PSL.sensorlist - :members: - :undoc-members: - :show-inheritance: - - -Module contents ---------------- - -.. automodule:: PSL - :members: - :undoc-members: - :show-inheritance: diff --git a/docs/api.rst b/docs/api.rst new file mode 100644 index 00000000..faa1347c --- /dev/null +++ b/docs/api.rst @@ -0,0 +1,18 @@ +Library API +=========== + +.. toctree:: + :maxdepth: 1 + + bus + external + instrument + internal + +ScienceLab +---------- + +.. automodule:: pslab.sciencelab + :members: + :undoc-members: + :show-inheritance: diff --git a/docs/bus.rst b/docs/bus.rst new file mode 100644 index 00000000..ec6d046d --- /dev/null +++ b/docs/bus.rst @@ -0,0 +1,16 @@ +Bus +=== + +.. automodule:: pslab.bus + :members: + :undoc-members: + :show-inheritance: + + +I2C +--- + +.. automodule:: pslab.bus.i2c + :members: + :undoc-members: + :show-inheritance: diff --git a/docs/cli.rst b/docs/cli.rst new file mode 100644 index 00000000..0635cc48 --- /dev/null +++ b/docs/cli.rst @@ -0,0 +1,7 @@ +Command Line Interface +====================== + +.. automodule:: pslab.cli + :members: + :undoc-members: + :show-inheritance: \ No newline at end of file diff --git a/docs/conf.py b/docs/conf.py index a187af06..3b3420db 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -1,7 +1,7 @@ # -*- coding: utf-8 -*- # # PSLab documentation build configuration file, created by -# sphinx-quickstart +# sphinx-quickstart # # This file is execfile()d with the current directory set to its # containing dir. @@ -12,20 +12,20 @@ # All configuration values have a default; values that are commented out # serve to show the default. -import sys import os +import sys # If extensions (or modules to document with autodoc) are in another directory, # add these directories to sys.path here. If the directory is relative to the # documentation root, use os.path.abspath to make it absolute, like shown here. -#sys.path.insert(0, os.path.abspath('.')) +# sys.path.insert(0, os.path.abspath('.')) sys.path.insert(0, os.path.abspath('..')) # -- General configuration ------------------------------------------------ # If your documentation needs a minimal Sphinx version, state it here. -#needs_sphinx = '1.0' +# needs_sphinx = '1.0' # Add any Sphinx extension module names here, as strings. They can be # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom @@ -35,9 +35,11 @@ 'sphinx.ext.todo', 'sphinx.ext.viewcode', 'sphinx.ext.mathjax', + 'sphinx.ext.napoleon', + 'recommonmark', ] -mathjax_path='file:///usr/share/javascript/mathjax/MathJax.js' +mathjax_path = 'file:///usr/share/javascript/mathjax/MathJax.js' # Add any paths that contain templates here, relative to this directory. templates_path = ['_templates'] @@ -45,27 +47,27 @@ # The suffix(es) of source filenames. # You can specify multiple suffix as a list of string: # source_suffix = ['.rst', '.md'] -source_suffix = '.rst' +source_suffix = ['.rst', '-md'] # The encoding of source files. -#source_encoding = 'utf-8-sig' +# source_encoding = 'utf-8-sig' # The master toctree document. master_doc = 'index' # General information about the project. -project = u'FOSSASIA PSLab' -copyright = u'2016, Praveen Patil, Jithin BP' -author = u' Praveen Patil, Jithin BP' +project = u'pslab-python' +copyright = u'2021, FOSSASIA' +author = u'FOSSASIA Developers' # The version info for the project you're documenting, acts as replacement for # |version| and |release|, also used in various other places throughout the # built documents. # # The short X.Y version. -version = u'1.0' +version = u'2.0' # The full version, including alpha/beta/rc tags. -release = u'1.0.5' +release = u'2.0.0' # The language for content autogenerated by Sphinx. Refer to documentation # for a list of supported languages. @@ -76,9 +78,9 @@ # There are two options for replacing |today|: either, you set today to some # non-false value, then it is used: -#today = '' +# today = '' # Else, today_fmt is used as the format for a strftime call. -#today_fmt = '%B %d, %Y' +# today_fmt = '%B %d, %Y' # List of patterns, relative to source directory, that match files and # directories to ignore when looking for source files. @@ -86,176 +88,174 @@ # The reST default role (used for this markup: `text`) to use for all # documents. -#default_role = None +# default_role = None # If true, '()' will be appended to :func: etc. cross-reference text. -#add_function_parentheses = True +# add_function_parentheses = True # If true, the current module name will be prepended to all description # unit titles (such as .. function::). -#add_module_names = True +# add_module_names = True # If true, sectionauthor and moduleauthor directives will be shown in the # output. They are ignored by default. -#show_authors = False +# show_authors = False # The name of the Pygments (syntax highlighting) style to use. pygments_style = 'sphinx' # A list of ignored prefixes for module index sorting. -#modindex_common_prefix = [] +# modindex_common_prefix = [] # If true, keep warnings as "system message" paragraphs in the built documents. -#keep_warnings = False +# keep_warnings = False # If true, `todo` and `todoList` produce output, else they produce nothing. todo_include_todos = True - # -- Options for HTML output ---------------------------------------------- # The theme to use for HTML and HTML Help pages. See the documentation for # a list of builtin themes. -#html_theme = 'alabaster' -#html_theme_path = ["/usr/lib/python2.7/dist-packages/"] - +# html_theme = 'alabaster' +# html_theme_path = ["/usr/lib/python2.7/dist-packages/"] # Theme options are theme-specific and customize the look and feel of a theme # further. For a list of options available for each theme, see the # documentation. -#html_theme_options = {} +# html_theme_options = {} # Add any paths that contain custom themes here, relative to this directory. -#html_theme_path = [] +# html_theme_path = [] # The name for this set of Sphinx documents. If None, it defaults to # " v documentation". -#html_title = None +# html_title = None # A shorter title for the navigation bar. Default is the same as html_title. -#html_short_title = None +# html_short_title = None # The name of an image file (relative to this directory) to place at the top # of the sidebar. -#html_logo = None +# html_logo = None # The name of an image file (relative to this directory) to use as a favicon of # the docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 # pixels large. -#html_favicon = None +# html_favicon = None # Add any paths that contain custom static files (such as style sheets) here, # relative to this directory. They are copied after the builtin static files, # so a file named "default.css" will overwrite the builtin "default.css". -html_static_path = ['_static'] +# html_static_path = ['_static'] # Add any extra paths that contain custom files (such as robots.txt or # .htaccess) here, relative to this directory. These files are copied # directly to the root of the documentation. -#html_extra_path = [] +# html_extra_path = [] # If not '', a 'Last updated on:' timestamp is inserted at every page bottom, # using the given strftime format. -#html_last_updated_fmt = '%b %d, %Y' +# html_last_updated_fmt = '%b %d, %Y' # If true, SmartyPants will be used to convert quotes and dashes to # typographically correct entities. -#html_use_smartypants = True +# html_use_smartypants = True # Custom sidebar templates, maps document names to template names. -#html_sidebars = {} +# html_sidebars = {} # Additional templates that should be rendered to pages, maps page names to # template names. -#html_additional_pages = {} +# html_additional_pages = {} # If false, no module index is generated. -#html_domain_indices = True +# html_domain_indices = True # If false, no index is generated. -#html_use_index = True +# html_use_index = True # If true, the index is split into individual pages for each letter. -#html_split_index = False +# html_split_index = False # If true, links to the reST sources are added to the pages. -#html_show_sourcelink = True +# html_show_sourcelink = True # If true, "Created using Sphinx" is shown in the HTML footer. Default is True. -#html_show_sphinx = True +# html_show_sphinx = True # If true, "(C) Copyright ..." is shown in the HTML footer. Default is True. -#html_show_copyright = True +# html_show_copyright = True # If true, an OpenSearch description file will be output, and all pages will # contain a tag referring to it. The value of this option must be the # base URL from which the finished HTML is served. -#html_use_opensearch = '' +# html_use_opensearch = '' # This is the file name suffix for HTML files (e.g. ".xhtml"). -#html_file_suffix = None +# html_file_suffix = None # Language to be used for generating the HTML full-text search index. # Sphinx supports the following languages: # 'da', 'de', 'en', 'es', 'fi', 'fr', 'hu', 'it', 'ja' # 'nl', 'no', 'pt', 'ro', 'ru', 'sv', 'tr' -#html_search_language = 'en' +# html_search_language = 'en' # A dictionary with options for the search language support, empty by default. # Now only 'ja' uses this config value -#html_search_options = {'type': 'default'} +# html_search_options = {'type': 'default'} # The name of a javascript file (relative to the configuration directory) that # implements a search results scorer. If empty, the default will be used. -#html_search_scorer = 'scorer.js' +# html_search_scorer = 'scorer.js' # Output file base name for HTML help builder. -htmlhelp_basename = 'SEELdoc' +htmlhelp_basename = 'pslab-python-doc' # -- Options for LaTeX output --------------------------------------------- latex_elements = { -# The paper size ('letterpaper' or 'a4paper'). -#'papersize': 'letterpaper', + # The paper size ('letterpaper' or 'a4paper'). + # 'papersize': 'letterpaper', -# The font size ('10pt', '11pt' or '12pt'). -#'pointsize': '10pt', + # The font size ('10pt', '11pt' or '12pt'). + # 'pointsize': '10pt', -# Additional stuff for the LaTeX preamble. -#'preamble': '', + # Additional stuff for the LaTeX preamble. + # 'preamble': '', -# Latex figure (float) alignment -#'figure_align': 'htbp', + # Latex figure (float) alignment + # 'figure_align': 'htbp', } # Grouping the document tree into LaTeX files. List of tuples # (source start file, target name, title, # author, documentclass [howto, manual, or own class]). latex_documents = [ - (master_doc, 'PSL.tex', u'PSL Documentation', - u'Praveen Patil, Jithin BP', 'manual'), + (master_doc, 'pslab-python.tex', u'pslab-python documentation', + u'FOSSASIA Developers', 'manual'), ] # The name of an image file (relative to this directory) to place at the top of # the title page. -#latex_logo = None +# latex_logo = None # For "manual" documents, if this is true, then toplevel headings are parts, # not chapters. -#latex_use_parts = False +# latex_use_parts = False # If true, show page references after internal links. -#latex_show_pagerefs = False +# latex_show_pagerefs = False # If true, show URL addresses after external links. -#latex_show_urls = False +# latex_show_urls = False # Documents to append as an appendix to all manuals. -#latex_appendices = [] +# latex_appendices = [] # If false, no module index is generated. -#latex_domain_indices = True +# latex_domain_indices = True # -- Options for manual page output --------------------------------------- @@ -263,12 +263,12 @@ # One entry per manual page. List of tuples # (source start file, name, description, authors, manual section). man_pages = [ - (master_doc, 'psl', u'PSL Documentation', + (master_doc, 'pslab-python', u'pslab-python documentation', [author], 1) ] # If true, show URL addresses after external links. -#man_show_urls = False +# man_show_urls = False # -- Options for Texinfo output ------------------------------------------- @@ -277,22 +277,22 @@ # (source start file, target name, title, author, # dir menu entry, description, category) texinfo_documents = [ - (master_doc, 'PSL', u'PSL Documentation', - author, 'PSL', 'One line description of project.', + (master_doc, 'pslab-python', u'pslab-python documentation', + author, 'pslab-python', 'One line description of project.', 'Miscellaneous'), ] # Documents to append as an appendix to all manuals. -#texinfo_appendices = [] +# texinfo_appendices = [] # If false, no module index is generated. -#texinfo_domain_indices = True +# texinfo_domain_indices = True # How to display URL addresses: 'footnote', 'no', or 'inline'. -#texinfo_show_urls = 'footnote' +# texinfo_show_urls = 'footnote' # If true, do not generate a @detailmenu in the "Top" node's menu. -#texinfo_no_detailmenu = False +# texinfo_no_detailmenu = False # -- Options for Epub output ---------------------------------------------- @@ -304,62 +304,62 @@ epub_copyright = copyright # The basename for the epub file. It defaults to the project name. -#epub_basename = project +# epub_basename = project # The HTML theme for the epub output. Since the default themes are not # optimized for small screen space, using the same theme for HTML and epub # output is usually not wise. This defaults to 'epub', a theme designed to save # visual space. -#epub_theme = 'epub' +# epub_theme = 'epub' # The language of the text. It defaults to the language option # or 'en' if the language is not set. -#epub_language = '' +# epub_language = '' # The scheme of the identifier. Typical schemes are ISBN or URL. -#epub_scheme = '' +# epub_scheme = '' # The unique identifier of the text. This can be a ISBN number # or the project homepage. -#epub_identifier = '' +# epub_identifier = '' # A unique identification for the text. -#epub_uid = '' +# epub_uid = '' # A tuple containing the cover image and cover page html template filenames. -#epub_cover = () +# epub_cover = () # A sequence of (type, uri, title) tuples for the guide element of content.opf. -#epub_guide = () +# epub_guide = () # HTML files that should be inserted before the pages created by sphinx. # The format is a list of tuples containing the path and title. -#epub_pre_files = [] +# epub_pre_files = [] # HTML files that should be inserted after the pages created by sphinx. # The format is a list of tuples containing the path and title. -#epub_post_files = [] +# epub_post_files = [] # A list of files that should not be packed into the epub file. epub_exclude_files = ['search.html'] # The depth of the table of contents in toc.ncx. -#epub_tocdepth = 3 +# epub_tocdepth = 3 # Allow duplicate toc entries. -#epub_tocdup = True +# epub_tocdup = True # Choose between 'default' and 'includehidden'. -#epub_tocscope = 'default' +# epub_tocscope = 'default' # Fix unsupported image types using the Pillow. -#epub_fix_images = False +# epub_fix_images = False # Scale large images. -#epub_max_image_width = 0 +# epub_max_image_width = 0 # How to display URL addresses: 'footnote', 'no', or 'inline'. -#epub_show_urls = 'inline' +# epub_show_urls = 'inline' # If false, no index is generated. -#epub_use_index = True +# epub_use_index = True diff --git a/docs/external.rst b/docs/external.rst new file mode 100644 index 00000000..f5b9ed9c --- /dev/null +++ b/docs/external.rst @@ -0,0 +1,15 @@ +External devices +================ + +.. automodule:: pslab.external + :members: + :undoc-members: + :show-inheritance: + +motor +----- + +.. automodule:: pslab.external.motor + :members: + :undoc-members: + :show-inheritance: diff --git a/docs/images/IMG_20160618_011156_HDR.jpg b/docs/images/IMG_20160618_011156_HDR.jpg deleted file mode 100644 index 8ba281d1..00000000 Binary files a/docs/images/IMG_20160618_011156_HDR.jpg and /dev/null differ diff --git a/docs/images/SplashNotConnected.png b/docs/images/SplashNotConnected.png deleted file mode 100644 index 9a67e223..00000000 Binary files a/docs/images/SplashNotConnected.png and /dev/null differ diff --git a/docs/images/advanced controls.png b/docs/images/advanced controls.png deleted file mode 100644 index 41ca9b9b..00000000 Binary files a/docs/images/advanced controls.png and /dev/null differ diff --git a/docs/images/controlPanelNotConnected.png b/docs/images/controlPanelNotConnected.png deleted file mode 100644 index 38fc5b84..00000000 Binary files a/docs/images/controlPanelNotConnected.png and /dev/null differ diff --git a/docs/images/controlpanel.png b/docs/images/controlpanel.png deleted file mode 100644 index d1d73a04..00000000 Binary files a/docs/images/controlpanel.png and /dev/null differ diff --git a/docs/images/datastreaming.png b/docs/images/datastreaming.png deleted file mode 100644 index fa20f656..00000000 Binary files a/docs/images/datastreaming.png and /dev/null differ diff --git a/docs/images/lissajous1.png b/docs/images/lissajous1.png deleted file mode 100644 index 0b77ccbf..00000000 Binary files a/docs/images/lissajous1.png and /dev/null differ diff --git a/docs/images/lissajous2.png b/docs/images/lissajous2.png deleted file mode 100644 index 1483c087..00000000 Binary files a/docs/images/lissajous2.png and /dev/null differ diff --git a/docs/images/logicanalyzer.png b/docs/images/logicanalyzer.png deleted file mode 100644 index 4e63151d..00000000 Binary files a/docs/images/logicanalyzer.png and /dev/null differ diff --git a/docs/images/psl2.jpg b/docs/images/psl2.jpg deleted file mode 100644 index 306a0076..00000000 Binary files a/docs/images/psl2.jpg and /dev/null differ diff --git a/docs/images/pslab.png b/docs/images/pslab.png deleted file mode 100644 index 008cea20..00000000 Binary files a/docs/images/pslab.png and /dev/null differ diff --git a/docs/images/pslab.svg b/docs/images/pslab.svg deleted file mode 100644 index 9b1fc80e..00000000 --- a/docs/images/pslab.svg +++ /dev/null @@ -1,854 +0,0 @@ - - - -image/svg+xmlhttp://fossasia.orghttp://fossasia.orgPSLab -Pocket Science Lab From FOSSASIA -http://pslab.fossasia.org - \ No newline at end of file diff --git a/docs/images/pslaboscilloscope.png b/docs/images/pslaboscilloscope.png deleted file mode 100644 index dd45cfdc..00000000 Binary files a/docs/images/pslaboscilloscope.png and /dev/null differ diff --git a/docs/images/pslpcb.jpg b/docs/images/pslpcb.jpg deleted file mode 100644 index cd89a7e0..00000000 Binary files a/docs/images/pslpcb.jpg and /dev/null differ diff --git a/docs/images/sensordataloger.png b/docs/images/sensordataloger.png deleted file mode 100644 index 643b49cf..00000000 Binary files a/docs/images/sensordataloger.png and /dev/null differ diff --git a/docs/images/sinewaveonoscilloscope.png b/docs/images/sinewaveonoscilloscope.png deleted file mode 100644 index 1cc8d8d7..00000000 Binary files a/docs/images/sinewaveonoscilloscope.png and /dev/null differ diff --git a/docs/images/splash.png b/docs/images/splash.png deleted file mode 100644 index 05fb401f..00000000 Binary files a/docs/images/splash.png and /dev/null differ diff --git a/docs/images/squarewave.png b/docs/images/squarewave.png deleted file mode 100644 index d4c0a63a..00000000 Binary files a/docs/images/squarewave.png and /dev/null differ diff --git a/docs/images/wirelesssensordataloger.png b/docs/images/wirelesssensordataloger.png deleted file mode 100644 index 9ffe242a..00000000 Binary files a/docs/images/wirelesssensordataloger.png and /dev/null differ diff --git a/docs/images/with fossasia logo sticker .jpg b/docs/images/with fossasia logo sticker .jpg deleted file mode 100644 index 405b3562..00000000 Binary files a/docs/images/with fossasia logo sticker .jpg and /dev/null differ diff --git a/docs/index.rst b/docs/index.rst index ba4949b8..ad382937 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -2,20 +2,22 @@ You can adapt this file completely to your liking, but it should at least contain the root `toctree` directive. -Welcome to PSLab's documentation! -================================ +pslab-python +============ + +The pslab-python library provides a high-level interface for controlling a +PSLab device over a serial connection using Python. Contents: .. toctree:: :maxdepth: 4 - PSL.interface - PSL.Peripherals.I2C_class - PSL.Peripherals.SPI_class - PSL.Peripherals.MCP4728_class - PSL.Peripherals.NRF24L01_class - PSL.Peripherals.NRF_NODE + installation + api + cli + protocol + Contributing Indices and tables diff --git a/docs/installation.rst b/docs/installation.rst new file mode 100644 index 00000000..c239cba2 --- /dev/null +++ b/docs/installation.rst @@ -0,0 +1,21 @@ +Installation +============ + +``pslab-python`` can be installed from PyPI: +:: + + $ pip install pslab + +**Note:** Linux users must additionally install a udev rules file for +pslab-python to be able to communicate with the PSLab device. The file +[99-pslab.rules](https://github.com/fossasia/pslab-python/blob/development/99-pslab.rules) +should be copied to /etc/udev/rules.d/. + +**Note**: pslab-python does not provide a graphical user interface. If you want +a GUI, install the [pslab-desktop app](https://github.com/fossasia/pslab-desktop). + +Dependencies +------------ + +``pslab-python`` requires `Python `__ version +3.6 or later. \ No newline at end of file diff --git a/docs/instrument.rst b/docs/instrument.rst new file mode 100644 index 00000000..b2e3335f --- /dev/null +++ b/docs/instrument.rst @@ -0,0 +1,55 @@ +Instrument +========== + +.. automodule:: pslab.instrument + :members: + :undoc-members: + :show-inheritance: + +LogicAnalyzer +------------- + +.. autoclass:: pslab.instrument.logic_analyzer.LogicAnalyzer + :members: + :undoc-members: + :show-inheritance: + +Multimeter +---------- + +.. autoclass:: pslab.instrument.multimeter.Multimeter + :members: + :undoc-members: + :show-inheritance: + +Oscilloscope +------------ + +.. autoclass:: pslab.instrument.oscilloscope.Oscilloscope + :members: + :undoc-members: + :show-inheritance: + +PowerSupply +----------- + +.. autoclass:: pslab.instrument.power_supply.PowerSupply + :members: + :undoc-members: + :show-inheritance: + +PWMGenerator +------------ + +.. autoclass:: pslab.instrument.waveform_generator.PWMGenerator + :members: + :undoc-members: + :show-inheritance: + +WaveformGenerator +----------------- + +.. autoclass:: pslab.instrument.waveform_generator.WaveformGenerator + :members: + :undoc-members: + :show-inheritance: diff --git a/docs/internal.rst b/docs/internal.rst new file mode 100644 index 00000000..13d59590 --- /dev/null +++ b/docs/internal.rst @@ -0,0 +1,47 @@ +Internal API +============ + +Classes which regular users should not need to use directly are documented +here. + +Serial handler +--------------- + +.. automodule:: pslab.serial_handler + :members: + :undoc-members: + :show-inheritance: + +Analog channels +--------------- + +.. automodule:: pslab.instrument.analog + :members: + :undoc-members: + :show-inheritance: + +Digital channels +---------------- + +.. automodule:: pslab.instrument.digital + :members: + :undoc-members: + :show-inheritance: + +Sources +------- + +.. autoclass:: pslab.instrument.power_supply.Source + :members: + :undoc-members: + :show-inheritance: + +.. autoclass:: pslab.instrument.power_supply.VoltageSource + :members: + :undoc-members: + :show-inheritance: + +.. autoclass:: pslab.instrument.power_supply.CurrentSource + :members: + :undoc-members: + :show-inheritance: diff --git a/docs/protocol.rst b/docs/protocol.rst new file mode 100644 index 00000000..0ce51adb --- /dev/null +++ b/docs/protocol.rst @@ -0,0 +1,7 @@ +Serial protocol +=============== + +.. automodule:: pslab.protocol + :members: + :undoc-members: + :show-inheritance: \ No newline at end of file diff --git a/lint-requirements.txt b/lint-requirements.txt new file mode 100644 index 00000000..2bb38d5c --- /dev/null +++ b/lint-requirements.txt @@ -0,0 +1,4 @@ +black>=18.9b0 +flake8>=3.6.0 +pydocstyle>=2.1.1 +bandit>=1.5.1 diff --git a/pslab/99-pslab.rules b/pslab/99-pslab.rules new file mode 100644 index 00000000..e09d90d6 --- /dev/null +++ b/pslab/99-pslab.rules @@ -0,0 +1,4 @@ +# PSLab v5 +ACTION=="add", SUBSYSTEM=="tty", ATTRS{idVendor}=="04d8", ATTRS{idProduct}=="00df", MODE="666" +# PSLab v6 +ACTION=="add", SUBSYSTEM=="tty", ATTRS{idVendor}=="10c4", ATTRS{idProduct}=="ea60", MODE="666" diff --git a/pslab/__init__.py b/pslab/__init__.py new file mode 100644 index 00000000..fde015c0 --- /dev/null +++ b/pslab/__init__.py @@ -0,0 +1,20 @@ +"""Pocket Science Lab by FOSSASIA.""" + +from pslab.instrument.logic_analyzer import LogicAnalyzer +from pslab.instrument.multimeter import Multimeter +from pslab.instrument.oscilloscope import Oscilloscope +from pslab.instrument.power_supply import PowerSupply +from pslab.instrument.waveform_generator import PWMGenerator, WaveformGenerator +from pslab.sciencelab import ScienceLab + +__all__ = ( + "LogicAnalyzer", + "Multimeter", + "Oscilloscope", + "PowerSupply", + "PWMGenerator", + "WaveformGenerator", + "ScienceLab", +) + +__version__ = "4.0.1" diff --git a/pslab/bus/__init__.py b/pslab/bus/__init__.py new file mode 100644 index 00000000..a2354728 --- /dev/null +++ b/pslab/bus/__init__.py @@ -0,0 +1,31 @@ +"""Contains modules for interfacing with the PSLab's I2C, SPI, and UART buses.""" + +import sys + + +class classmethod_(classmethod): + """Support chaining classmethod and property.""" + + def __init__(self, f): + self.f = f + super().__init__(f) + + def __get__(self, obj, cls=None): + # classmethod() to support chained decorators; new in python 3.9. + if sys.version_info < (3, 9) and isinstance(self.f, property): + return self.f.__get__(cls) + else: + return super().__get__(obj, cls) + + +from pslab.bus.i2c import I2CMaster, I2CSlave # noqa: E402 +from pslab.bus.spi import SPIMaster, SPISlave # noqa: E402 +from pslab.bus.uart import UART # noqa: E402 + +__all__ = ( + "I2CMaster", + "I2CSlave", + "SPIMaster", + "SPISlave", + "UART", +) diff --git a/pslab/bus/busio.py b/pslab/bus/busio.py new file mode 100644 index 00000000..97771dca --- /dev/null +++ b/pslab/bus/busio.py @@ -0,0 +1,592 @@ +"""Circuitpython's busio compatibility layer for pslab-python. + +This module emulates the CircuitPython's busio API for devices or hosts running +CPython or MicroPython using pslab-python. +This helps to access many sensors using Adafruit's drivers via PSLab board. + +Notes +----- +Documentation: + https://circuitpython.readthedocs.io/en/6.3.x/shared-bindings/busio/index.html + +Examples +-------- +Get humidity from Si7021 temperature and humidity sensor using adafruit_si7021 module. + +>>> import adafruit_si7021 +>>> from pslab.bus import busio # import board, busio +>>> i2c = busio.I2C() # i2c = busio.I2C(board.SCL, board.SDA) +>>> sensor = adafruit_si7021.SI7021(i2c) +>>> print(sensor.relative_humidity) + +Get gyro reading from BNO055 using adafruit_bno055, board(just a wrapper for busio). + +>>> import adafruit_bno055 +>>> from pslab.bus import busio # import board +>>> i2c = busio.I2C() # i2c = board.I2C() +>>> sensor = adafruit_bno055.BNO055_I2C(i2c) +>>> print(sensor.gyro) +""" + +import time +from enum import Enum +from itertools import zip_longest +from typing import List, Union, Optional + +from pslab.bus.i2c import _I2CPrimitive +from pslab.bus.spi import _SPIPrimitive +from pslab.bus.uart import _UARTPrimitive +from pslab.connection import ConnectionHandler + +__all__ = ( + "I2C", + "SPI", + "UART", +) +ReadableBuffer = Union[bytes, bytearray, memoryview] +WriteableBuffer = Union[bytearray, memoryview] + + +class I2C(_I2CPrimitive): + """Busio I2C Class for CircuitPython Compatibility. + + Parameters + ---------- + device : :class:`SerialHandler`, optional + Serial connection to PSLab device. If not provided, a new one will be + created. + frequency : float, optional + Frequency of SCL in Hz. + """ + + def __init__( + self, + device: ConnectionHandler | None = None, + *, + frequency: int = 125e3, + ): + # 125 kHz is as low as the PSLab can go. + super().__init__(device) + self._init() + self._configure(frequency) + + def deinit(self) -> None: + """Just a dummy method.""" + pass + + def __enter__(self): + """Just a dummy context manager.""" + return self + + def __exit__(self, exc_type, exc_val, exc_tb) -> None: + """Call :meth:`deinit` on context exit.""" + self.deinit() + + def scan(self) -> List[int]: + """Scan all I2C addresses between 0x08 and 0x77 inclusive. + + Returns + ------- + addrs : list of int + List of 7-bit addresses on which slave devices replied. + """ + return self._scan(0x08, 0x77) + + def try_lock(self) -> bool: # pylint: disable=no-self-use + """Just a dummy method.""" + return True + + def unlock(self) -> None: + """Just a dummy method.""" + pass + + def readfrom_into( + self, address: int, buffer: WriteableBuffer, *, start: int = 0, end: int = None + ) -> None: + """Read from a device at specified address into a buffer. + + Parameters + ---------- + address : int + 7-bit I2C device address. + buffer : bytearray or memoryview + buffer to write into. + start : int + Index to start writing at. + end : int + Index to write up to but not include. Defaults to length of `buffer`. + """ + end = len(buffer) if end is None else end + bytes_to_read = end - start + self._start(address, 1) + buffer[start:end] = self._read(bytes_to_read) + self._stop() + + def writeto( + self, + address: int, + buffer: ReadableBuffer, + *, + start: int = 0, + end: int = None, + stop: bool = True, + ) -> None: + """Write to a device at specified address from a buffer. + + Parameters + ---------- + address : int + 7-bit I2C device address. + buffer : bytes or bytearray or memoryview + buffer containing the bytes to write. + start : int + Index to start writing from. + end : int + Index to read up to but not include. Defaults to length of `buffer`. + stop : bool + Enable to transmit a stop bit. Defaults to True. + """ + end = len(buffer) if end is None else end + + if stop: + self._write_bulk(address, buffer[start:end]) + else: + self._start(address, 0) + self._send(buffer[start:end]) + + def writeto_then_readfrom( + self, + address: int, + buffer_out: ReadableBuffer, + buffer_in: WriteableBuffer, + *, + out_start: int = 0, + out_end: int = None, + in_start: int = 0, + in_end: int = None, + ): + """Write to then read from a device at specified address. + + Parameters + ---------- + address : int + 7-bit I2C device address. + out_buffer : bytes or bytearray or memoryview + buffer containing the bytes to write. + in_buffer : bytearray or memoryview + buffer to write into. + out_start : int + Index to start writing from. + out_end : int + Index to read up to but not include. Defaults to length of `out_buffer`. + in_start : int + Index to start writing at. + in_end : int + Index to write up to but not include. Defaults to length of `in_buffer`. + """ + out_end = len(buffer_out) if out_end is None else out_end + in_end = len(buffer_in) if in_end is None else in_end + bytes_to_read = in_end - in_start + self._start(address, 0) + self._send(buffer_out[out_start:out_end]) + self._restart(address, 1) + buffer_in[in_start:in_end] = self._read(bytes_to_read) + self._stop() + + +class SPI(_SPIPrimitive): + """Busio SPI Class for CircuitPython Compatibility. + + Parameters + ---------- + device : :class:`SerialHandler`, optional + Serial connection to PSLab device. If not provided, a new one will be + created. + """ + + def __init__(self, device: ConnectionHandler | None = None): + super().__init__(device) + ppre, spre = self._get_prescaler(25e4) + self._set_parameters(ppre, spre, 1, 0, 1) + self._bits = 8 + + @property + def frequency(self) -> int: + """Get the actual SPI bus frequency (rounded). + + This may not match the frequency requested due to internal limitations. + """ + return round(self._frequency) + + def deinit(self) -> None: + """Just a dummy method.""" + pass + + def __enter__(self): + """Just a dummy context manager.""" + return self + + def __exit__(self, exc_type, exc_val, exc_tb) -> None: + """Call :meth:`deinit` on context exit.""" + self.deinit() + + def configure( + self, + *, + baudrate: int = 100000, + polarity: int = 0, + phase: int = 0, + bits: int = 8, + ) -> None: + """Configure the SPI bus. + + Parameters + ---------- + baudrate : int + The desired clock rate in Hertz. The actual clock rate may be + higher or lower due to the granularity of available clock settings. + Check the frequency attribute for the actual clock rate. + polarity : int + The base state of the clock line (0 or 1) + phase : int + The edge of the clock that data is captured. First (0) or second (1). + Rising or falling depends on clock polarity. + bits : int + The number of bits per word. + """ + if polarity not in (0, 1): + raise ValueError("Invalid polarity") + if phase not in (0, 1): + raise ValueError("Invalid phase") + if bits not in self._INTEGER_TYPE_MAP: + raise ValueError("Invalid number of bits") + + ppre, spre = self._get_prescaler(baudrate) + cke = (phase ^ 1) & 1 + self._set_parameters(ppre, spre, cke, polarity, 1) + self._bits = bits + + def try_lock(self) -> bool: # pylint: disable=no-self-use + """Just a dummy method.""" + return True + + def unlock(self) -> None: + """Just a dummy method.""" + pass + + def write( + self, + buffer: Union[ReadableBuffer, List[int]], + *, + start: int = 0, + end: int = None, + ) -> None: + """Write the data contained in buffer. If the buffer is empty, nothing happens. + + Parameters + ---------- + buffer : bytes or bytearray or memoryview or list_of_int (for bits >8) + Write out the data in this buffer. + start : int + Start of the slice of `buffer` to write out: `buffer[start:end]`. + end : int + End of the slice; this index is not included. Defaults to `len(buffer)`. + """ + end = len(buffer) if end is None else end + buffer = buffer[start:end] + + if not buffer: + return + + self._start() + self._write_bulk(buffer, self._bits) + self._stop() + + def readinto( + self, + buffer: Union[WriteableBuffer, List[int]], + *, + start: int = 0, + end: int = None, + write_value: int = 0, + ) -> None: + """Read into `buffer` while writing `write_value` for each byte read. + + If the number of bytes to read is 0, nothing happens. + + Parameters + ---------- + buffer : bytearray or memoryview or list_of_int (for bits >8) + Read data into this buffer. + start : int + Start of the slice of `buffer` to read into: `buffer[start:end]`. + end : int + End of the slice; this index is not included. Defaults to `len(buffer)`. + write_value : int + Value to write while reading. (Usually ignored.) + """ + end = len(buffer) if end is None else end + bytes_to_read = end - start + + if bytes_to_read == 0: + return + + self._start() + data = self._transfer_bulk([write_value] * bytes_to_read, self._bits) + self._stop() + + for i, v in zip(range(start, end), data): + buffer[i] = v + + def write_readinto( + self, + buffer_out: Union[ReadableBuffer, List[int]], + buffer_in: Union[WriteableBuffer, List[int]], + *, + out_start: int = 0, + out_end: int = None, + in_start: int = 0, + in_end: int = None, + ): + """Write out the data in buffer_out while simultaneously read into buffer_in. + + The lengths of the slices defined by buffer_out[out_start:out_end] and + buffer_in[in_start:in_end] must be equal. If buffer slice lengths are both 0, + nothing happens. + + Parameters + ---------- + buffer_out : bytes or bytearray or memoryview or list_of_int (for bits >8) + Write out the data in this buffer. + buffer_in : bytearray or memoryview or list_of_int (for bits >8) + Read data into this buffer. + out_start : int + Start of the slice of `buffer_out` to write out: + `buffer_out[out_start:out_end]`. + out_end : int + End of the slice; this index is not included. Defaults to `len(buffer_out)` + in_start : int + Start of the slice of `buffer_in` to read into:`buffer_in[in_start:in_end]` + in_end : int + End of the slice; this index is not included. Defaults to `len(buffer_in)` + """ + out_end = len(buffer_out) if out_end is None else out_end + in_end = len(buffer_in) if in_end is None else in_end + buffer_out = buffer_out[out_start:out_end] + bytes_to_read = in_end - in_start + + if len(buffer_out) != bytes_to_read: + raise ValueError("buffer slices must be of equal length") + if bytes_to_read == 0: + return + + self._start() + data = self._transfer_bulk(buffer_out, self._bits) + self._stop() + + for i, v in zip(range(in_start, in_end), data): + buffer_in[i] = v + + +class Parity(Enum): + EVEN = 1 + ODD = 2 + + +class UART(_UARTPrimitive): + """Busio UART Class for CircuitPython Compatibility. + + Parameters + ---------- + device : :class:`SerialHandler`, optional + Serial connection to PSLab device. If not provided, a new one will be + created. + baudrate : int, optional + The transmit and receive speed. Defaults to 9600. + bits : int, optional + The number of bits per byte, 8 or 9. Defaults to 8 bits. + parity : :class:`Parity`, optional + The parity used for error checking. Defaults to None. + Only 8 bits per byte supports parity. + stop : int, optional + The number of stop bits, 1 or 2. Defaults to 1. + timeout : float, optional + The timeout in seconds to wait for the first character and between + subsequent characters when reading. Defaults to 1. + """ + + def __init__( + self, + device: ConnectionHandler | None = None, + *, + baudrate: int = 9600, + bits: int = 8, + parity: Parity = None, + stop: int = 1, + timeout: float = 1, + ): + super().__init__(device) + self._set_uart_baud(baudrate) + + if bits == 8: + pd = 0 + elif bits == 9: + pd = 3 + else: + raise ValueError("Invalid number of bits") + + if bits == 9 and parity is not None: + raise ValueError("Invalid parity") + if stop not in (1, 2): + raise ValueError("Invalid number of stop bits") + + pd += parity.value + + try: + self._set_uart_mode(pd, stop - 1) + except RuntimeError: + pass + + self._timeout = timeout + + @property + def baudrate(self): + """Get the current baudrate.""" + return self._baudrate + + @property + def in_waiting(self): + """Get the number of bytes in the input buffer, available to be read. + + PSLab is limited to check, whether at least one byte in the buffer(1) or not(0). + """ + return self._read_uart_status() + + @property + def timeout(self): + """Get the current timeout, in seconds (float).""" + return self._timeout + + def deinit(self) -> None: + """Just a dummy method.""" + pass + + def __enter__(self): + """Just a dummy context manager.""" + return self + + def __exit__(self, exc_type, exc_val, exc_tb) -> None: + """Call :meth:`deinit` on context exit.""" + self.deinit() + + def _read_with_timeout(self, nbytes: int = None, *, line=False): + if nbytes == 0: + return None + + start_time = time.time() + data = bytearray() + total_read = 0 + + while (time.time() - start_time) <= self._timeout: + has_char = self._read_uart_status() + if has_char: + char = self._read_byte() + start_time = time.time() + + if line and char == 0xA: + break + + data.append(char) + total_read += 1 + + if nbytes and total_read == nbytes: + break + + time.sleep(0.01) + + return bytes(data) if data else None + + def read(self, nbytes: int = None) -> Optional[bytes]: + """Read characters. + + If `nbytes` is specified then read at most that many bytes. Otherwise, + read everything that arrives until the connection times out. + + Providing the number of bytes expected is highly recommended because + it will be faster. + + Parameters + ---------- + nbytes : int, optional + Number of bytes to read. Defaults to None. + + Returns + ------- + bytes or None + Data read. + """ + return self._read_with_timeout(nbytes) + + def readinto(self, buf: WriteableBuffer) -> int: + """Read bytes into the `buf`. Read at most `len(buf)` bytes. + + Parameters + ---------- + buf : bytearray or memoryview + Read data into this buffer. + + Returns + ------- + int + Number of bytes read and stored into `buf`. + """ + nbytes = len(buf) + data = self._read_with_timeout(nbytes) + + if data is None: + return 0 + else: + nbuf = len(data) + + for i, c in zip(range(nbuf), data): + buf[i] = c + + return nbuf + + def readline(self) -> Optional[bytes]: + """Read a line, ending in a newline character. + + return None if a timeout occurs sooner, or return everything readable + if no newline is found within timeout. + + Returns + ------- + bytes or None + Data read. + """ + return self._read_with_timeout(None, line=True) + + def write(self, buf: ReadableBuffer) -> int: + """Write the buffer of bytes to the bus. + + Parameters + ---------- + buf : bytes or bytearray or memoryview + Write out the char in this buffer. + + Returns + ------- + int + Number of bytes written. + """ + written = 0 + + for msb, lsb in zip_longest(buf[1::2], buf[::2]): + if msb is not None: + self._write_int((msb << 8) | lsb) + written += 2 + else: + self._write_byte(lsb) + written += 1 + + return written diff --git a/pslab/bus/i2c.py b/pslab/bus/i2c.py new file mode 100644 index 00000000..dd9b792b --- /dev/null +++ b/pslab/bus/i2c.py @@ -0,0 +1,665 @@ +"""Control the PSLab's I2C bus and devices connected on the bus. + +Examples +-------- +Set I2C bus speed to 400 kbit/s: + +>>> from pslab.bus.i2c import I2CMaster, I2CSlave +>>> bus = I2CMaster() +>>> bus.configure(frequency=4e5) + +Scan for connected devices: + +>>> bus.scan() +[96, 104] + +Connect to the PSLab's built-in DS1307 RTC: + +>>> rtc = I2CSlave(address=104) +""" + +import logging +from typing import List + +import pslab.protocol as CP +from pslab.connection import ConnectionHandler, autoconnect +from pslab.external.sensorlist import sensors + +__all__ = ( + "I2CMaster", + "I2CSlave", +) +logger = logging.getLogger(__name__) + + +class _I2CPrimitive: + """I2C primitive commands. + + Handles all the I2C subcommands coded in pslab-firmware. + + Parameters + ---------- + device : :class:`SerialHandler`, optional + Serial connection to PSLab device. If not provided, a new one will be + created. + """ + + _MIN_BRGVAL = 2 + _MAX_BRGVAL = 511 + + # Specs say typical delay is 110 ns to 130 ns; 150 ns from testing. + _SCL_DELAY = 150e-9 + + _ACK = 0 + _READ = 1 + _WRITE = 0 + + def __init__(self, device: ConnectionHandler | None = None): + self._device = device if device is not None else autoconnect() + self._running = False + self._mode = None + + def _init(self): + self._device.send_byte(CP.I2C_HEADER) + self._device.send_byte(CP.I2C_INIT) + self._device.get_ack() + + def _configure(self, frequency: float): + """Configure bus frequency. + + Parameters + ---------- + frequency : float + Frequency of SCL in Hz. + + Raises + ------ + ValueError + If given frequency is not supported by PSLab board. + """ + brgval = self._get_i2c_brgval(frequency) + + if self._MIN_BRGVAL <= brgval <= self._MAX_BRGVAL: + self._device.send_byte(CP.I2C_HEADER) + self._device.send_byte(CP.I2C_CONFIG) + self._device.send_int(brgval) + self._device.get_ack() + else: + min_frequency = self._get_i2c_frequency(self._MAX_BRGVAL) + max_frequency = self._get_i2c_frequency(self._MIN_BRGVAL) + e = f"Frequency must be between {min_frequency} and {max_frequency} Hz." + raise ValueError(e) + + @classmethod + def _get_i2c_brgval(cls, frequency: float) -> int: + return int((1 / frequency - cls._SCL_DELAY) * CP.CLOCK_RATE - 2) + + @classmethod + def _get_i2c_frequency(cls, brgval: int) -> float: + return 1 / ((brgval + 2) / CP.CLOCK_RATE + cls._SCL_DELAY) + + def _scan(self, start: int = 1, end: int = 128) -> List[int]: + """Scan I2C port for connected devices. + + Parameters + ---------- + start : int + Address to start scaning at. Defaults to 1(0 is the general call address). + end : int + Address to scan up to. Defaults to 128. + + Returns + ------- + addrs : list of int + List of 7-bit addresses on which slave devices replied. + """ + addrs = [] + + for address in range(start, end): + slave = I2CSlave(address, self._device) + + if slave.ping(): + addrs.append(address) + + return addrs + + def _start(self, address: int, mode: int) -> int: + """Initiate I2C transfer. + + Parameters + ---------- + address : int + 7-bit I2C device address. + mode : {0, 1} + 0: write + 1: read + + Returns + ------- + ackstat : int + ACK (0) or NACK (1) from addressed peripheral. + """ + if self._mode is not None: + msg = ( + f"An I2C transaction is already active on peripheral 0x{address:X}. " + "Use _restart instead." + ) + raise RuntimeError(msg) + + self._device.send_byte(CP.I2C_HEADER) + secondary = CP.I2C_START if not self._running else CP.I2C_RESTART + self._device.send_byte(secondary) + self._device.send_byte((address << 1) | mode) + response = self._device.get_ack() + ackstat = response >> 4 + self._running = True + self._mode = mode + + return ackstat + + def _restart(self, address: int, mode: int) -> int: + """Send repeated start. + + Reinitiate I2C transfer without stoping. + + Parameters + ---------- + address : int + 7-bit I2C device address. + mode : {0, 1} + 0: write + 1: read + + Returns + ------- + ackstat : int + ACK (0) or NACK (1) from addressed peripheral. + """ + self._device.send_byte(CP.I2C_HEADER) + self._device.send_byte(CP.I2C_RESTART) + self._device.send_byte((address << 1) | mode) + response = self._device.get_ack() + ackstat = response >> 4 + self._running = True + self._mode = mode + + return ackstat + + def _stop(self): + """Stop I2C transfer.""" + if self._running or self._mode: + self._device.send_byte(CP.I2C_HEADER) + self._device.send_byte(CP.I2C_STOP) + self._device.get_ack() + self._running = False + self._mode = None + + def _wait(self): + """Wait for I2C.""" + self._device.send_byte(CP.I2C_HEADER) + self._device.send_byte(CP.I2C_WAIT) + self._device.get_ack() + + @property + def _status(self): + """int: Contents of the I2C2STAT register. + + bit 15 ACKSTAT: Acknowledge Status + 1 = NACK received from slave. + 0 = ACK received from slave. + bit 14 TRSTAT: Transmit Status + 1 = Master transmit is in progress (8 bits + ACK). + 0 = Master transmit is not in progress. + bit 10 BCL: Bus Collision Detect + 1 = A bus collision has been detected. + 0 = No collision. + bit 7 IWCOL: I2C Write Collision Detect + 1 = An attempt to write to the I2C2TRN register failed because + the I2C module is busy. + 0 = No collision. + bit 6 I2COV: I2C Receive Overflow Flag + 1 = A byte is received while the I2C2RCV register is still + holding the previous byte. + 0 = No overflow. + bit 4 P: Stop + 1 = Indicates that a Stop bit has been detected last. + 0 = Stop bit was not detected last. + bit 3 S: Start + 1 = Indicates that a Start (or Repeated Start) bit has been + detected last. + 0 = Start bit was not detected last. + bit 1 RBF: Receive Buffer Full Status + 1 = Receive completes; the I2C2RCV register is full. + 0 = Receive is not complete; the I2C2RCV register is empty. + bit 0 TBF: Transmit Buffer Full Status + 1 = Transmit is in progress; the I2C2TRN register is full. + 0 = Transmit completes; the I2C2TRN register is empty. + """ + self._device.send_byte(CP.I2C_HEADER) + self._device.send_byte(CP.I2C_STATUS) + status = self._device.get_int() + self._device.get_ack() + + return status + + def _send_byte(self, data: int) -> int: + """Send data over I2C. + + The I2C bus needs to be initialized and set to the correct slave address first. + + It is a primitive I2C method, prefered to use :meth:`I2CSlave.read` and + :meth:`I2CSlave.write`. + + Parameters + ---------- + data : int + An integer that fits in a uint8. + + Returns + ------- + response : int + Response from I2C slave device. + """ + self._device.send_byte(CP.I2C_HEADER) + self._device.send_byte(CP.I2C_SEND) + self._device.send_byte(data) # data byte + response = self._device.get_ack() >> 4 # ACKSTAT + + return response + + def _send_byte_burst(self, data: int): + """Send data over I2C. + + The function does not wait for the I2C to finish before returning. + It is used for sending large packets quickly. + + The I2C bus needs to be initialized and set to the correct slave address first. + + It is a primitive I2C method, prefered to use :meth:`I2CSlave.read` and + :meth:`I2CSlave.write`. + + Parameters + ---------- + data : int + An integer that fits in a uint8. + """ + self._device.send_byte(CP.I2C_HEADER) + self._device.send_byte(CP.I2C_SEND_BURST) + self._device.send_byte(data) # data byte + # No handshake. for the sake of speed. + # e.g. loading a frame buffer onto an I2C display such as ssd1306 + + def _send(self, bytes_to_write: bytearray): + """Send data over I2C. + + This method uses :meth:`_send_byte_burst` to send all the bytes quickly. + + The I2C bus needs to be initialized and set to the correct slave address first. + + It is a primitive I2C method, prefered to use :meth:`I2CSlave.read` and + :meth:`I2CSlave.write`. + + Parameters + ---------- + bytes_to_write : bytearray + Send data as a bytearray. + """ + for byte in bytes_to_write: + self._send_byte_burst(byte) + + def _write_bulk(self, address: int, bytes_to_write: bytearray): + """Write data to I2C slave. + + It is a primitive I2C method, prefered to use :meth:`I2CSlave.read` and + :meth:`I2CSlave.write`. + + Parameters + ---------- + address : int + 7-bit I2C device address. + bytes_to_write : bytearray + Write data as a bytearray. + """ + self._device.send_byte(CP.I2C_HEADER) + self._device.send_byte(CP.I2C_WRITE_BULK) + self._device.send_byte(address) + self._device.send_byte(len(bytes_to_write)) + + for byte in bytes_to_write: + self._device.send_byte(byte) + + self._device.get_ack() + + def _read_more(self) -> int: + """Read data from I2C then send ACK. + + The I2C bus needs to be initialized and set to the correct slave address first. + + It is a primitive I2C method, prefered to use :meth:`I2CSlave.read` and + :meth:`I2CSlave.write`. + + Returns + ------- + data : int + A byte interpreted as a uint8. + """ + self._device.send_byte(CP.I2C_HEADER) + self._device.send_byte(CP.I2C_READ_MORE) + data = self._device.get_byte() + self._device.get_ack() + + return data + + def _read_end(self) -> int: + """Read data from I2C then send NACK. + + The I2C bus needs to be initialized and set to the correct slave address first. + + It is a primitive I2C method, prefered to use :meth:`I2CSlave.read` and + :meth:`I2CSlave.write`. + + Returns + ------- + data : int + A byte interpreted as a uint8. + """ + self._device.send_byte(CP.I2C_HEADER) + self._device.send_byte(CP.I2C_READ_END) + data = self._device.get_byte() + self._device.get_ack() + + return data + + def _read(self, bytes_to_read: int) -> bytearray: + """Read data from I2C. + + The I2C bus needs to be initialized and set to the correct slave address first. + + It is a primitive I2C method, prefered to use :meth:`I2CSlave.read` and + :meth:`I2CSlave.write`. + + Parameters + ---------- + bytes_to_read : int + Number of bytes to read from I2C. + + Returns + ------- + data : bytearray + Read data as a bytearray. + """ + data = bytearray() + + for _ in range(bytes_to_read - 1): + data.append(self._read_more()) + + data.append(self._read_end()) + + return data + + def _read_bulk( + self, address: int, bytes_to_read: int, register_address: int = 0x0 + ) -> bytearray: + """Read data from I2C device. + + This method relies on the slave device auto incrementing its internal + pointer after each read. Most devices do, but it is not part of the + I2C standard. Refer to the device's documentation. + + It is a primitive I2C method, prefered to use :meth:`I2CSlave.read` and + :meth:`I2CSlave.write`. + + Parameters + ---------- + address : int + 7-bit I2C device address. + bytes_to_read : int + Number of bytes to read from slave device. + register_address : int, optional + Slave device internal memory address to read from. The default + value is 0x0. + + Returns + ------- + data : bytearray + Read data as a bytearray. + """ + self._device.send_byte(CP.I2C_HEADER) + self._device.send_byte(CP.I2C_READ_BULK) + self._device.send_byte(address) + self._device.send_byte(register_address) + self._device.send_byte(bytes_to_read) + data = self._device.read(bytes_to_read) + self._device.get_ack() + + return bytearray(data) + + +class I2CMaster(_I2CPrimitive): + """I2C bus controller. + + Handles slave independent functionality with the I2C port. + + Parameters + ---------- + device : :class:`SerialHandler`, optional + Serial connection to PSLab device. If not provided, a new one will be + created. + """ + + def __init__(self, device: ConnectionHandler | None = None): + super().__init__(device) + self._init() + self.configure(125e3) # 125 kHz is as low as the PSLab can go. + + def configure(self, frequency: float): + """Configure bus frequency. + + Parameters + ---------- + frequency : float + Frequency of SCL in Hz. + + Raises + ------ + ValueError + If given frequency is not supported by PSLab board. + """ + self._configure(frequency) + + def scan(self) -> List[int]: + """Scan I2C port for connected devices. + + Returns + ------- + addrs : list of int + List of 7-bit addresses on which slave devices replied. + """ + addrs = self._scan(1, 128) + + for address in addrs: + logger.info( + f"Response from slave on {hex(address)} " + + f"({sensors.get(address, 'None')})." + ) + + return addrs + + +class I2CSlave(_I2CPrimitive): + """I2C slave device. + + Parameters + ---------- + address : int + 7-bit I2C device address. + device : :class:`SerialHandler`, optional + Serial interface for communicating with the PSLab device. If not + provided, a new one will be created. + + Attributes + ---------- + address : int + 7-bit I2C device address. + """ + + def __init__( + self, + address: int, + device: ConnectionHandler | None = None, + ): + super().__init__(device) + self.address = address + + def ping(self) -> bool: + """Ping the I2C slave. + + Returns + ------- + response : bool + True is slave responded, False otherwise. + """ + response = self._start(self.address, self._WRITE) + self._stop() + + return response == self._ACK + + def read(self, bytes_to_read: int, register_address: int = 0x0) -> bytearray: + """Read data from I2C device. + + This method relies on the slave device auto incrementing its internal + pointer after each read. Most devices do, but it is not part of the + I2C standard. Refer to the device's documentation. + + If the slave device does not auto increment, use one of + :meth:`read_byte`, :meth:`read_int`, or :meth:`read_long` instead, + depending on how wide the device registers are. + + Parameters + ---------- + bytes_to_read : int + Number of bytes to read from slave device. + register_address : int, optional + Slave device internal memory address to read from. The default + value is 0x0. + + Returns + ------- + data : bytearray + Read data as a bytearray. + """ + return self._read_bulk(self.address, bytes_to_read, register_address) + + def read_byte(self, register_address: int = 0x0) -> int: + """Read a single byte from the I2C slave. + + Parameters + ---------- + register_address : int, optional + Slave device internal memory address to read from. The default + value is 0x0. + + Returns + ------- + data : int + A byte interpreted as a uint8. + """ + return self.read(1, register_address)[0] + + def read_int(self, register_address: int = 0x0) -> int: + """Read a two byte value from the I2C slave. + + Parameters + ---------- + register_address : int, optional + Slave device internal memory address to read from. The default + value is 0x0. + + Returns + ------- + data : int + Two bytes interpreted as a uint16. + """ + data = self.read(2, register_address) + + return CP.ShortInt.unpack(data)[0] + + def read_long(self, register_address: int = 0x0) -> int: + """Read a four byte value from the I2C slave. + + Parameters + ---------- + register_address : int, optional + Slave device internal memory address to read from. The default + value is 0x0. + + Returns + ------- + data : int + Four bytes interpreted as a uint32. + """ + data = self.read(4, register_address) + + return CP.Integer.unpack(data)[0] + + def write(self, bytes_to_write: bytearray, register_address: int = 0x0): + """Write data to I2C slave. + + This method relies on the slave device auto incrementing its internal + pointer after each write. Most devices do, but it is not part of the + I2C standard. Refer to the device's documentation. + + If the slave device does not auto increment, use one of + :meth:`write_byte`, :meth:`write_int`, or :meth:`write_long` instead, + depending on how wide the device registers are. + + Parameters + ---------- + bytes_to_write : bytearray + Data to write to the slave. + register_address : int, optional + Slave device internal memory address to write to. The default + value is 0x0. + """ + register_address = bytearray(CP.Byte.pack(register_address)) + bytes_to_write = bytearray(bytes_to_write) + self._write_bulk(self.address, register_address + bytes_to_write) + + def write_byte(self, data: int, register_address: int = 0x0): + """Write a single byte to the I2C slave. + + Parameters + ---------- + data : int + An integer that fits in a uint8. + register_address : int, optional + Slave device internal memory address to write to. The default + value is 0x0. + """ + self.write(CP.Byte.pack(data), register_address) + + def write_int(self, data: int, register_address: int = 0x0): + """Write a two byte value to the I2C slave. + + Parameters + ---------- + data : int + An integer that fits in a uint16. + register_address : int, optional + Slave device internal memory address to write to. The default + value is 0x0. + """ + self.write(CP.ShortInt.pack(data), register_address) + + def write_long(self, data: int, register_address: int = 0x0): + """Write a four byte value to the I2C slave. + + Parameters + ---------- + data : int + An integer that fits in a uint32. + register_address : int, optional + Slave device internal memory address to write to. The default + value is 0x0. + """ + self.write(CP.Integer.pack(data), register_address) diff --git a/pslab/bus/spi.py b/pslab/bus/spi.py new file mode 100644 index 00000000..ad885ceb --- /dev/null +++ b/pslab/bus/spi.py @@ -0,0 +1,678 @@ +"""Control the PSLab's SPI bus and devices connected on the bus. + +Examples +-------- +Set SPI bus speed to 200 kbit/s: + +>>> from pslab.bus.spi import SPIMaster, SPISlave +>>> bus = SPIMaster() +>>> bus.set_parameters(primary_prescaler=0, secondary_prescaler=3) # 64e6/(64*5) + +Set SPI bus to mode 3 (1,1): + +>>> bus.set_parameters(0, 3, CKE=0, CKP=1) + +Transfer a random byte over SPI: + +>>> slave = SPISlave() +>>> slave.transfer8(0x55) +0 +""" + +from typing import List, Tuple + +import pslab.protocol as CP +from pslab.bus import classmethod_ +from pslab.connection import ConnectionHandler, autoconnect + +__all__ = ( + "SPIMaster", + "SPISlave", +) +# Default values, refer pslab-firmware. +_PPRE = 0 +_SPRE = 2 +# SPI mode 0 (0,0) +_CKP = 0 # Clock Polarity 0 +_CKE = 1 # Clock Phase 0 | Clock Edge 1 +_SMP = 1 + + +class _SPIPrimitive: + """SPI primitive commands. + + Handles all the SPI subcommands coded in pslab-firmware. + + Parameters + ---------- + device : :class:`SerialHandler`, optional + Serial connection to PSLab device. If not provided, a new one will be + created. + """ + + _TRANSFER_COMMANDS_MAP = { + 8: CP.SEND_SPI8, + 16: CP.SEND_SPI16, + } # PSLab only supports 8 and 16 bits. + _INTEGER_TYPE_MAP = { + 8: CP.Byte, + 16: CP.ShortInt, + } # Keys in `_INTEGER_TYPE_MAP` should match `_TRANSFER_COMMANDS_MAP`. + _PPRE_MAP = [64, 16, 4, 1] + _SPRE_MAP = [8, 7, 6, 5, 4, 3, 2, 1] + + _primary_prescaler = _PPRE + _secondary_prescaler = _SPRE + _clock_polarity = _CKP # Clock Polarity bit. + _clock_edge = _CKE # Clock Edge Select bit (inverse of Clock Phase bit). + _smp = _SMP # Data Input Sample Phase bit. + + def __init__(self, device: ConnectionHandler | None = None): + self._device = device if device is not None else autoconnect() + + @classmethod_ + @property + def _frequency(cls) -> float: + ppre = cls._PPRE_MAP[cls._primary_prescaler] + spre = cls._SPRE_MAP[cls._secondary_prescaler] + + return CP.CLOCK_RATE / (ppre * spre) + + @classmethod_ + @property + def _clock_phase(cls) -> int: + return (cls._clock_edge ^ 1) & 1 + + @classmethod + def _get_prescaler(cls, frequency: float) -> Tuple[int]: + min_diff = CP.CLOCK_RATE # highest + # minimum frequency + ppre = 0 + spre = 0 + + for p in range(len(cls._PPRE_MAP)): + for s in range(len(cls._SPRE_MAP)): + freq = CP.CLOCK_RATE / (cls._PPRE_MAP[p] * cls._SPRE_MAP[s]) + if frequency >= freq: + diff = frequency - freq + if min_diff > diff: + # better match + min_diff = diff + ppre = p + spre = s + + return ppre, spre + + @staticmethod + def _save_config( + primary_prescaler: int, + secondary_prescaler: int, + CKE: int, + CKP: int, + SMP: int, + ): + """Save the SPI parameters. + + See Also + -------- + _set_parameters : To set SPI parameters. + """ + _SPIPrimitive._primary_prescaler = primary_prescaler + _SPIPrimitive._secondary_prescaler = secondary_prescaler + _SPIPrimitive._clock_edge = CKE + _SPIPrimitive._clock_polarity = CKP + _SPIPrimitive._smp = SMP + + def _set_parameters( + self, + primary_prescaler: int, + secondary_prescaler: int, + CKE: int, + CKP: int, + SMP: int, + ): + """Set SPI parameters. + + It is a primitive SPI method, prefered to use :meth:`SPIMaster.set_parameters`. + + Parameters + ---------- + primary_prescaler : {0, 1, 2, 3} + Primary Prescaler for system clock :const:`CP.CLOCK_RATE`Hz. + (0,1,2,3) -> (64:1,16:1,4:1,1:1). + secondary_prescaler : {0, 1, 2, 3, 4, 5, 6, 7} + Secondary prescaler (0,1,..7) -> (8:1,7:1,..1:1). + CKE : {0, 1} + SPIx Clock Edge Select bit. Serial output data changes on transition + {0: from Idle clock state to active clock state, + 1: from active clock state to Idle clock state}. + CKP : {0, 1} + Clock Polarity Select bit. + Idle state for clock is a {0: low, 1: high} level. + SMP : {0, 1} + Input data is sampled at the {0: end, 1: middle} of data output time. + + Raises + ------ + ValueError + If any one of arguments is not in its shown range. + """ + error_message = [] + if primary_prescaler not in range(0, 4): + error_message.append("Primary Prescaler must be in 2-bits.") + if secondary_prescaler not in range(0, 8): + error_message.append("Secondary Prescale must be in 3-bits.") + if CKE not in (0, 1): + error_message.append("Clock Edge Select must be a bit.") + if CKP not in (0, 1): + error_message.append("Clock Polarity must be a bit.") + if SMP not in (0, 1): + error_message.append("SMP must be a bit.") + if error_message: + raise ValueError("\n".join(error_message)) + + self._device.send_byte(CP.SPI_HEADER) + self._device.send_byte(CP.SET_SPI_PARAMETERS) + # 0Bhgfedcba - > : modebit CKP,: modebit CKE, :primary prescaler, + # :secondary prescaler + self._device.send_int( + secondary_prescaler + | (primary_prescaler << 3) + | (CKE << 5) + | (CKP << 6) + | (SMP << 7) + ) + self._device.get_ack() + self._save_config(primary_prescaler, secondary_prescaler, CKE, CKP, SMP) + + @classmethod + def _get_parameters(cls) -> Tuple[int]: + """Get SPI parameters. + + Returns + ------- + primary_prescaler : {0, 1, 2, 3} + Primary Prescaler for system clock :const:`CP.CLOCK_RATE`Hz. + (0,1,2,3) -> (64:1,16:1,4:1,1:1). + secondary_prescaler : {0, 1, 2, 3, 4, 5, 6, 7} + Secondary prescaler (0,1,..7) -> (8:1,7:1,..1:1). + CKE : {0, 1} + SPIx Clock Edge Select bit. Serial output data changes on transition + {0: from Idle clock state to active clock state, + 1: from active clock state to Idle clock state}. + CKP : {0, 1} + Clock Polarity Select bit. + Idle state for clock is a {0: low, 1: high} level. + SMP : {0, 1} + Input data is sampled at the {0: end, 1: middle} of data output time. + """ + return ( + cls._primary_prescaler, + cls._secondary_prescaler, + cls._clock_edge, + cls._clock_polarity, + cls._smp, + ) + + def _start(self): + """Select SPI channel to enable. + + Basically sets the relevant chip select pin to LOW. + + External ChipSelect pins: + version < 5 : {6, 7} # RC5, RC4 (dropped support) + version == 5 : {} (don't have any external CS pins) + version == 6 : {7} # RC4 + """ + self._device.send_byte(CP.SPI_HEADER) + self._device.send_byte(CP.START_SPI) + self._device.send_byte(7) # SPI.CS v6 + # No ACK because `RESPONSE == DO_NOT_BOTHER` in firmware. + + def _stop(self): + """Select SPI channel to disable. + + Sets the relevant chip select pin to HIGH. + """ + self._device.send_byte(CP.SPI_HEADER) + self._device.send_byte(CP.STOP_SPI) + self._device.send_byte(7) # SPI.CS v6 + + def _transfer(self, data: int, bits: int) -> int: + """Send data over SPI and receive data from SPI simultaneously. + + Relevent SPI channel need to be enabled first. + + It is a primitive SPI method, prefered to use :meth:`SPISlave.transfer8` + and :meth:`SPISlave.transfer16`. + + Parameters + ---------- + data : int + Data to transmit. + bits : int + The number of bits per word. + + Returns + ------- + data_in : int + Data returned by slave device. + + Raises + ------ + ValueError + If given bits per word not supported by PSLab board. + """ + command = self._TRANSFER_COMMANDS_MAP.get(bits) + interger_type = self._INTEGER_TYPE_MAP.get(bits) + + if not command: + raise ValueError( + f"PSLab only supports {set(self._TRANSFER_COMMANDS_MAP.keys())}" + + " bits per word." + ) + + self._device.send_byte(CP.SPI_HEADER) + self._device.send_byte(command) + self._device.write(interger_type.pack(data)) + data_in = interger_type.unpack(self._device.read(bits))[0] + self._device.get_ack() + + return data_in + + def _transfer_bulk(self, data: List[int], bits: int) -> List[int]: + """Transfer data array over SPI. + + Relevent SPI channel need to be enabled first. + + It is a primitive SPI method, prefered to use :meth:`SPISlave.transfer8_bulk` + and :meth:`SPISlave.transfer16_bulk`. + + Parameters + ---------- + data : list of int + List of data to transmit. + bits : int + The number of bits per word. + + Returns + ------- + data_in : list of int + List of data returned by slave device. + + Raises + ------ + ValueError + If given bits per word not supported by PSLab board. + """ + data_in = [] + + for a in data: + data_in.append(self._transfer(a, bits)) + + return data_in + + def _read(self, bits: int) -> int: + """Read data while transmit zero. + + Relevent SPI channel need to be enabled first. + + It is a primitive SPI method, prefered to use :meth:`SPISlave.read8` + and :meth:`SPISlave.read16`. + + Parameters + ---------- + bits : int + The number of bits per word. + + Returns + ------- + int + Data returned by slave device. + + Raises + ------ + ValueError + If given bits per word not supported by PSLab board. + """ + return self._transfer(0, bits) + + def _read_bulk(self, data_to_read: int, bits: int) -> List[int]: + """Read data array while transmitting zeros. + + Relevent SPI channel need to be enabled first. + + It is a primitive SPI method, prefered to use :meth:`SPISlave.read8_bulk` + and :meth:`SPISlave.read16_bulk`. + + Parameters + ---------- + data_to_read : int + Number of data to read from slave device. + bits : int + The number of bits per word. + + Returns + ------- + list of int + List of data returned by slave device. + + Raises + ------ + ValueError + If given bits per word not supported by PSLab board. + """ + return self._transfer_bulk([0] * data_to_read, bits) + + def _write(self, data: int, bits: int): + """Send data over SPI. + + Relevent SPI channel need to be enabled first. + + It is a primitive SPI method, prefered to use :meth:`SPISlave.write8` + and :meth:`SPISlave.write16`. + + Parameters + ---------- + data : int + Data to transmit. + bits : int + The number of bits per word. + + Raises + ------ + ValueError + If given bits per word not supported by PSLab board. + """ + self._transfer(data, bits) + + def _write_bulk(self, data: List[int], bits: int): + """Send data array over SPI. + + Relevent SPI channel need to be enabled first. + + It is a primitive SPI method, prefered to use :meth:`SPISlave.write8_bulk` + and :meth:`SPISlave.write16_bulk`. + + Parameters + ---------- + data : list of int + List of data to transmit. + bits : int + The number of bits per word. + + Raises + ------ + ValueError + If given bits per word not supported by PSLab board. + """ + self._transfer_bulk(data, bits) + + +class SPIMaster(_SPIPrimitive): + """SPI bus controller. + + Parameters + ---------- + device : :class:`SerialHandler`, optional + Serial connection to PSLab device. If not provided, a new one will be + created. + """ + + def __init__(self, device: ConnectionHandler | None = None): + super().__init__(device) + # Reset config + self.set_parameters() + + def set_parameters( + self, + primary_prescaler: int = _PPRE, + secondary_prescaler: int = _SPRE, + CKE: int = _CKE, + CKP: int = _CKP, + SMP: int = _SMP, + ): + """Set SPI parameters. + + Parameters + ---------- + primary_prescaler : {0, 1, 2, 3} + Primary Prescaler for system clock :const:`CP.CLOCK_RATE`Hz. + (0,1,2,3) -> (64:1,16:1,4:1,1:1). + secondary_prescaler : {0, 1, 2, 3, 4, 5, 6, 7} + Secondary prescaler (0,1,..7) -> (8:1,7:1,..1:1). + CKE : {0, 1} + SPIx Clock Edge Select bit. Serial output data changes on transition + {0: from Idle clock state to active clock state, + 1: from active clock state to Idle clock state}. + CKP : {0, 1} + Clock Polarity Select bit. + Idle state for clock is a {0: low, 1: high} level. + SMP : {0, 1} + Input data is sampled at the {0: end, 1: middle} of data output time. + + Raises + ------ + ValueError + If any one of arguments is not in its shown range. + """ + self._set_parameters(primary_prescaler, secondary_prescaler, CKE, CKP, SMP) + + @classmethod + def get_parameters(cls) -> Tuple[int]: + """Get SPI parameters. + + Returns + ------- + primary_prescaler : {0, 1, 2, 3} + Primary Prescaler for system clock :const:`CP.CLOCK_RATE`Hz. + (0,1,2,3) -> (64:1,16:1,4:1,1:1). + secondary_prescaler : {0, 1, 2, 3, 4, 5, 6, 7} + Secondary prescaler (0,1,..7) -> (8:1,7:1,..1:1). + CKE : {0, 1} + SPIx Clock Edge Select bit. Serial output data changes on transition + {0: from Idle clock state to active clock state, + 1: from active clock state to Idle clock state}. + CKP : {0, 1} + Clock Polarity Select bit. + Idle state for clock is a {0: low, 1: high} level. + SMP : {0, 1} + Input data is sampled at the {0: end, 1: middle} of data output time. + """ + return cls._get_parameters() + + +class SPISlave(_SPIPrimitive): + """SPI slave device. + + Parameters + ---------- + device : :class:`SerialHandler`, optional + Serial connection to PSLab device. If not provided, a new one will be + created. + """ + + def __init__(self, device: ConnectionHandler | None = None): + super().__init__(device) + + def transfer8(self, data: int) -> int: + """Send 8-bit data over SPI and receive 8-bit data from SPI simultaneously. + + Parameters + ---------- + data : int + Data to transmit. + + Returns + ------- + data_in : int + Data returned by slave device. + """ + self._start() + data_in = self._transfer(data, 8) + self._stop() + + return data_in + + def transfer16(self, data: int) -> int: + """Send 16-bit data over SPI and receive 16-bit data from SPI simultaneously. + + Parameters + ---------- + data : int + Data to transmit. + + Returns + ------- + data_in : int + Data returned by slave device. + """ + self._start() + data_in = self._transfer(data, 16) + self._stop() + + return data_in + + def transfer8_bulk(self, data: List[int]) -> List[int]: + """Transfer 8-bit data array over SPI. + + Parameters + ---------- + data : list of int + List of 8-bit data to transmit. + + Returns + ------- + data_in : list of int + List of 8-bit data returned by slave device. + """ + self._start() + data_in = self._transfer_bulk(data, 8) + self._stop() + + return data_in + + def transfer16_bulk(self, data: List[int]) -> List[int]: + """Transfer 16-bit data array over SPI. + + Parameters + ---------- + data : list of int + List of 16-bit data to transmit. + + Returns + ------- + data_in : list of int + List of 16-bit data returned by slave device. + """ + self._start() + data_in = self._transfer_bulk(data, 16) + self._stop() + + return data_in + + def read8(self) -> int: + """Read 8-bit data while transmit zero. + + Returns + ------- + int + Data returned by slave device. + """ + self._start() + data_in = self._read(8) + self._stop() + + return data_in + + def read16(self) -> int: + """Read 16-bit data while transmit zero. + + Returns + ------- + int + Data returned by slave device. + """ + self._start() + data_in = self._read(16) + self._stop() + + return data_in + + def read8_bulk(self, data_to_read: int) -> List[int]: + """Read 8-bit data array while transmitting zeros. + + Parameters + ---------- + data_to_read : int + Number of 8-bit data to read from slave device. + + Returns + ------- + list of int + List of 8-bit data returned by slave device. + """ + self._start() + data_in = self._read_bulk(data_to_read, 8) + self._stop() + + return data_in + + def read16_bulk(self, data_to_read: int) -> List[int]: + """Read 16-bit data array while transmitting zeros. + + Parameters + ---------- + data_to_read : int + Number of 16-bit data to read from slave device. + + Returns + ------- + list of int + List of 16-bit date returned by slave device. + """ + self._start() + data_in = self._read_bulk(data_to_read, 16) + self._stop() + + return data_in + + def write8(self, data: int): + """Send 8-bit data over SPI. + + Parameters + ---------- + data : int + Data to transmit. + """ + self.transfer8(data) + + def write16(self, data: int): + """Send 16-bit data over SPI. + + Parameters + ---------- + data : int + Data to transmit. + """ + self.transfer16(data) + + def write8_bulk(self, data: List[int]): + """Send 8-bit data array over SPI. + + Parameters + ---------- + data : list of int + List of 8-bit data to transmit. + """ + self.transfer8_bulk(data) + + def write16_bulk(self, data: List[int]): + """Send 16-bit data array over SPI. + + Parameters + ---------- + data : list of int + List of 16-bit data to transmit. + """ + self.transfer16_bulk(data) diff --git a/pslab/bus/uart.py b/pslab/bus/uart.py new file mode 100644 index 00000000..a3bbf527 --- /dev/null +++ b/pslab/bus/uart.py @@ -0,0 +1,294 @@ +"""Control the PSLab's UART bus and devices connected on the bus. + +Examples +-------- +Set UART2 bus baudrate to 1000000: + +>>> from pslab.bus.uart improt UART +>>> bus = UART() +>>> bus.configure(1e6) + +Send a byte over UART: + +>>> bus.write_byte(0x55) +""" + +from typing import Tuple + +import pslab.protocol as CP +from pslab.bus import classmethod_ +from pslab.connection import ConnectionHandler, autoconnect + +__all__ = "UART" +_BRGVAL = 0x22 # BaudRate = 460800. +_MODE = (0, 0) # 8-bit data and no parity, 1 stop bit. + + +class _UARTPrimitive: + """UART primitive commands. + + Handles all the UART subcommands coded in pslab-firmware. + + Parameters + ---------- + device : :class:`SerialHandler`, optional + Serial connection to PSLab device. If not provided, a new one will be created. + """ + + _MIN_BRGVAL = 0 + _MAX_BRGVAL = 2**16 - 1 + + _brgval = _BRGVAL + _mode = _MODE + + def __init__(self, device: ConnectionHandler | None = None): + self._device = device if device is not None else autoconnect() + + @classmethod_ + @property + def _baudrate(cls) -> float: + return cls._get_uart_baudrate(cls._brgval) + + @staticmethod + def _get_uart_brgval(baudrate: float, BRGH: int = 1) -> int: + return round(((CP.CLOCK_RATE / baudrate) / (4 if BRGH else 16)) - 1) + + @staticmethod + def _get_uart_baudrate(brgval: int, BRGH: int = 1) -> float: + return (CP.CLOCK_RATE / (brgval + 1)) / (4 if BRGH else 16) + + @staticmethod + def _save_config(brgval: int = None, mode: Tuple[int] = None): + """Save the UART barval and mode bits. + + Parameters + ---------- + brgval : int, optional + Set value to `_UARTPrimitive._brgval`. Will be skipped if None. + Defaults to None. + mode : tuple of int, optional + Set value to `_UARTPrimitive._mode`. Will be skipped if None. + Defaults to None. + """ + if brgval is not None: + _UARTPrimitive._brgval = brgval + if mode is not None: + _UARTPrimitive._mode = mode + + def _set_uart_baud(self, baudrate: int): + """Set the baudrate of the UART bus. + + It is a primitive UART method, prefered to use :meth:`UART.configure`. + + Parameters + ---------- + baudrate : int + Baudrate to set on the UART bus. + + Raises + ------ + ValueError + If given baudrate in not supported by PSLab board. + """ + brgval = self._get_uart_brgval(baudrate) + + if self._MIN_BRGVAL <= brgval <= self._MAX_BRGVAL: + self._device.send_byte(CP.UART_2) + self._device.send_byte(CP.SET_BAUD) + self._device.send_int(brgval) + self._device.get_ack() + self._save_config(brgval=brgval) + else: + min_baudrate = self._get_uart_baudrate(self._MIN_BRGVAL) + max_baudrate = self._get_uart_baudrate(self._MAX_BRGVAL) + e = f"Baudrate must be between {min_baudrate} and {max_baudrate}." + raise ValueError(e) + + def _set_uart_mode(self, pd: int, st: int): + """Set UART mode. + + Parameters + ---------- + pd : {0, 1, 2, 3} + Parity and data selection bits. + {0: 8-bit data and no parity, + 1: 8-bit data and even parity, + 2: 8-bit data and odd parity, + 3: 9-bit data and no parity} + st : {0, 1} + Selects number of stop bits for each one-byte UART transmission. + {0: one stop bit, + 1: two stop bits} + + Raises + ------ + ValueError + If any one of arguments is not in its shown range. + RuntimeError + If this functionality is not supported by the firmware. + Since it is newly implemented, earlier firmware version don't support. + """ + error_message = [] + if pd not in range(0, 4): + error_message.append("Parity and data selection bits must be 2-bits.") + if st not in (0, 1): + error_message.append("Stop bits select must be a bit.") + # Verifying whether the firmware support current subcommand. + if self._device.version not in ["PSLab V6"]: + raise RuntimeError( + "This firmware version doesn't support this functionality." + ) + + self._device.send_byte(CP.UART_2) + self._device.send_byte(CP.SET_MODE) + self._device.send_byte((pd << 1) | st) + self._device.get_ack() + self._save_config(mode=(pd, st)) + + def _read_uart_status(self) -> int: + """Return whether receive buffer has data. + + Returns + ------- + status : int + 1 if at least one more character can be read else 0. + """ + self._device.send_byte(CP.UART_2) + self._device.send_byte(CP.READ_UART2_STATUS) + return self._device.get_byte() + + def _write_byte(self, data: int): + """Write a single byte to the UART bus. + + It is a primitive UART method, prefered to use :meth:`UART.write_byte`. + + Parameters + ---------- + data : int + Byte value to write to the UART bus. + """ + self._device.send_byte(CP.UART_2) + self._device.send_byte(CP.SEND_BYTE) + self._device.send_byte(data) + + if self._device.firmware.major < 3: + self._device.get_ack() + + def _write_int(self, data: int): + """Write a single int to the UART bus. + + It is a primitive UART method, prefered to use :meth:`UART.write_int`. + + Parameters + ---------- + data : int + Int value to write to the UART bus. + """ + self._device.send_byte(CP.UART_2) + self._device.send_byte(CP.SEND_INT) + self._device.send_int(data) + self._device.get_ack() + + def _read_byte(self) -> int: + """Read a single byte from the UART bus. + + It is a primitive UART method, prefered to use :meth:`UART.read_byte`. + + Returns + ------- + data : int + A Byte interpreted as a uint8 read from the UART bus. + """ + self._device.send_byte(CP.UART_2) + self._device.send_byte(CP.READ_BYTE) + return self._device.get_byte() + + def _read_int(self) -> int: + """Read a two byte value from the UART bus. + + It is a primitive UART method, prefered to use :meth:`UART.read_int`. + + Returns + ------- + data : int + Two bytes interpreted as a uint16 read from the UART bus. + """ + self._device.send_byte(CP.UART_2) + self._device.send_byte(CP.READ_INT) + return self._device.get_int() + + +class UART(_UARTPrimitive): + """UART2 bus. + + Parameters + ---------- + device : :class:`SerialHandler`, optional + Serial connection to PSLab device. If not provided, a new one will be created. + """ + + def __init__(self, device: ConnectionHandler | None = None): + super().__init__(device) + # Reset baudrate and mode + self.configure(self._get_uart_baudrate(_BRGVAL)) + + try: + self._set_uart_mode(*_MODE) + except RuntimeError: + pass + + def configure(self, baudrate: float): + """Configure UART bus baudrate. + + Parameters + ---------- + baudrate : float + + Raises + ------ + ValueError + If given baudrate is not supported by PSLab board. + """ + self._set_uart_baud(baudrate) + + def write_byte(self, data: int): + """Write a single byte to the UART bus. + + Parameters + ---------- + data : int + Byte value to write to the UART bus. + """ + self._write_byte(data) + + def write_int(self, data: int): + """Write a single int to the UART bus. + + Parameters + ---------- + data : int + Int value to write to the UART bus. + """ + self._write_int(data) + + def read_byte(self) -> int: + """Read a single byte from the UART bus. + + Returns + ------- + data : int + A Byte interpreted as a uint8 read from the UART bus. + """ + return self._read_byte() + + def read_int(self) -> int: + """Read a two byte value from the UART bus. + + It is a primitive UART method, prefered to use :meth:`UART.read_int`. + + Returns + ------- + data : int + Two bytes interpreted as a uint16 read from the UART bus. + """ + return self._read_int() diff --git a/pslab/cli.py b/pslab/cli.py new file mode 100644 index 00000000..5ba833bd --- /dev/null +++ b/pslab/cli.py @@ -0,0 +1,571 @@ +"""Functions related to CLI for PSLab. + +Example +------- +>>> from pslab import cli +>>> parser, subparser = cli.get_parser() +>>> cli.add_collect_args(subparser) +>>> cli.add_wave_args(subparser) +>>> cli.add_pwm_args(subparser) +>>> parser.parse_args(["collect","-i","logic_analyzer"]) +Namespace(channels=1, duration=1, file_path=None, function='collect', +instrument='logic_analyzer', json=False, port=None) +""" + +import argparse +import csv +import json +import platform +import os.path +import shutil +import struct +import sys +import time +from itertools import zip_longest +from typing import List, Tuple + +import numpy as np +import mcbootflash + +import pslab +import pslab.protocol as CP +from pslab.instrument.logic_analyzer import LogicAnalyzer +from pslab.instrument.oscilloscope import Oscilloscope +from pslab.instrument.waveform_generator import WaveformGenerator, PWMGenerator +from pslab.serial_handler import SerialHandler + + +def logic_analyzer( + device: SerialHandler, channels: int, duration: float +) -> Tuple[List[str], List[np.ndarray]]: + """Capture logic events on up to four channels simultaneously. + + Parameters + ---------- + device : :class:`Handler` + Serial interface for communicating with the PSLab device. + channels : {1, 2, 3, 4} + Number of channels to capture events on. Events will be captured on LA1, + LA2, LA3, and LA4, in that order. + duration : float + Duration in seconds up to which events will be captured. + + Returns + ------- + list of str + Name of active channels. + list of numpy.ndarray + List of numpy.ndarrays holding timestamps in microseconds when logic events + were detected. The length of the list is equal to the number of channels + that were used to capture events, and each list element corresponds to a + channel. + + Warnings + -------- + This cannot be used at the same time as the oscilloscope. + """ + la = LogicAnalyzer(device) + la.capture(channels, block=False) + time.sleep(duration) + la.stop() + timestamps = la.fetch_data() + channels_name = [la._channel_one_map, la._channel_two_map, "LA3", "LA4"] + + return channels_name[:channels], timestamps + + +def oscilloscope( + device: SerialHandler, channels: int, duration: float +) -> Tuple[List[str], List[np.ndarray]]: + """Capture varying voltage signals on up to four channels simultaneously. + + Parameters + ---------- + device : :class:`Handler` + Serial interface for communicating with the PSLab device. + channels : {1, 2, 4} + Number of channels to sample from simultaneously. By default, samples are + captured from CH1, CH2, CH3 and MIC. + duration : float + Duration in seconds up to which samples will be captured. + + Returns + ------- + list of str + "Timestamp", Name of active channels. + list of numpy.ndarray + List of numpy.ndarrays with timestamps in the first index and corresponding + voltages in the following index. The length of the list is equal to one + additional to the number of channels that were used to capture samples. + """ + scope = Oscilloscope(device) + max_samples = CP.MAX_SAMPLES // channels + min_timegap = scope._lookup_mininum_timegap(channels) + max_duration = max_samples * min_timegap * 1e-6 + active_channels = ([scope._channel_one_map] + scope._CH234)[:channels] + xy = [np.array([]) for _ in range(1 + channels)] + + while duration > 0: + if duration >= max_duration: + samples = max_samples + else: + samples = round((duration * 1e6) / min_timegap) + + st = time.time() + xy = np.append(xy, scope.capture(channels, samples, min_timegap), axis=1) + duration -= time.time() - st + + return ["Timestamp"] + active_channels, xy + + +INSTRUMENTS = { + "logic_analyzer": logic_analyzer, + "oscilloscope": oscilloscope, +} + + +def collect(handler: SerialHandler, args: argparse.Namespace): + """Collect data from instruments, and write it in file or stdout. + + Parameters + ---------- + handler : :class:`Handler` + Serial interface for communicating with the PSLab device. + args : :class:`argparse.Namespace` + Parsed arguments. + + Raises + ------ + LookupError + If the given instrument not available. + """ + instrument = INSTRUMENTS.get(args.instrument) + + if instrument is None: + raise LookupError(args.instrument + " not available") + + output = instrument(handler, args.channels, args.duration) + + if args.file_path is not None: + file = open(args.file_path, "w") + else: + file = sys.stdout + + if not args.json: + csv_file = csv.writer(file) + csv_file.writerow(output[0]) + for row in zip_longest(*output[1]): + csv_file.writerow(row) + else: + output_dict = dict() + for key, val in zip_longest(*output): + output_dict[key] = val.tolist() + json.dump(output_dict, file) + + if args.file_path is not None: + file.close() + + +def wave(handler: SerialHandler, args: argparse.Namespace): + """Generate or load wave. + + Parameters + ---------- + handler : :class:`Handler` + Serial interface for communicating with the PSLab device. + args : :class:`argparse.Namespace` + Parsed arguments. + """ + waveform_generator = WaveformGenerator(handler) + + if args.wave_function == "gen": + waveform_generator.generate( + channels=args.channel, + frequency=args.frequency, + phase=args.phase, + ) + elif args.wave_function == "load": + if args.table is not None: + table = args.table + elif args.table_file is not None: + with open(args.table_file) as table_file: + table = json.load(table_file) + + x = np.arange(0, len(table), len(table) / 512) + y = [table[int(i)] for i in x] + waveform_generator.load_table(channel=args.channel, points=y) + + +def pwm(handler: SerialHandler, args: argparse.Namespace): + """Generate PWM. + + Parameters + ---------- + handler : :class:`Handler` + Serial interface for communicating with the PSLab device. + args : :class:`argparse.Namespace` + Parsed arguments. + """ + pwm_generator = PWMGenerator(handler) + + if args.pwm_function == "gen": + pwm_generator.generate( + channels=args.channel, + frequency=args.frequency, + duty_cycles=args.duty_cycles, + phases=args.phases, + ) + elif args.pwm_function == "map": + pwm_generator.map_reference_clock( + channels=args.channel, + prescaler=args.prescaler, + ) + + +def main(args: argparse.Namespace): + """Perform the given function on PSLab. + + Parameters + ---------- + args : :class:`argparse.Namespace` + Parsed arguments. + """ + if args.function == "install": + install(args) + return + + handler = SerialHandler(port=args.port) + + if args.function == "flash": + flash(pslab.ScienceLab(args.port), args.hexfile) + return + + if args.function == "collect": + collect(handler, args) + elif args.function == "wave": + wave(handler, args) + elif args.function == "pwm": + pwm(handler, args) + + +def get_parser() -> Tuple[argparse.ArgumentParser, argparse._SubParsersAction]: + """Parser for CLI. + + Returns + ------- + parser : :class:`argparse.ArgumentParser` + Arqument parser for CLI. + functions : :class:`argparse._SubParsersAction` + SubParser to add other arguments related to different function. + """ + parser = argparse.ArgumentParser() + parser.add_argument( + "-p", + "--port", + type=str, + default=None, + required=False, + help="The name of the port to which the PSLab is connected", + ) + functions = parser.add_subparsers( + title="Functions", dest="function", description="Functions to perform on PSLab." + ) + + return parser, functions + + +def add_collect_args(subparser: argparse._SubParsersAction): + """Add arguments for collect function to ArgumentParser. + + Parameters + ---------- + subparser : :class:`argparse._SubParsersAction` + SubParser to add other arguments related to collect function. + """ + description = "Available Instruments: " + ", ".join(INSTRUMENTS) + "." + collect = subparser.add_parser("collect", description=description) + collect.add_argument( + "instrument", + type=str, + help="The name of the instrument to use", + ) + collect.add_argument( + "-c", + "--channels", + type=int, + default=1, + required=False, + help="Number of channels to capture", + ) + collect.add_argument( + "-d", + "--duration", + type=float, + default=1, + required=False, + help="Duration for capturing (in seconds)", + ) + collect.add_argument( + "-o", + "--output", + type=str, + default=None, + required=False, + dest="file_path", + help="File name to write data, otherwise in stdout", + ) + collect.add_argument( + "-j", + "--json", + action="store_true", + default=False, + help="Enable it to write data in json format", + ) + + +def add_wave_args(subparser: argparse._SubParsersAction): + """Add arguments for wave {gen,load} function to ArgumentParser. + + Parameters + ---------- + subparser : :class:`argparse._SubParsersAction` + SubParser to add other arguments related to wave_gen function. + """ + wave = subparser.add_parser("wave") + wave_functions = wave.add_subparsers( + title="Wave Functions", + dest="wave_function", + ) + wave_gen = wave_functions.add_parser("gen") + wave_gen.add_argument( + "channel", + nargs="+", + choices=["SI1", "SI2"], + help="Pin(s) on which to generate a waveform", + ) + wave_gen.add_argument( + "-f", + "--frequency", + nargs="+", + type=float, + required=True, + help="Frequency in Hz", + ) + wave_gen.add_argument( + "-p", + "--phase", + type=float, + default=0, + required=False, + help="Phase between waveforms in degrees", + ) + description = """ + TABLE: + JSON array of voltage values which make up the waveform. Array length + must be 512. If the array length less than 512, then the array will be + expanded in length of 512. Values outside the range -3.3 V to 3.3 V + will be clipped. + + examples: + [1,0] or [1,...,0,...], + [0,1,0,-1,0,1,0,-1,...], + [0,.025,.05,.075,.1,.125,.15,...] + """ + load = wave_functions.add_parser("load", description=description) + load.add_argument( + "channel", + choices=["SI1", "SI2"], + help="Pin(s) on which to load a table", + ) + load_table = load.add_mutually_exclusive_group(required=True) + load_table.add_argument( + "--table", + type=json.loads, + default=None, + help="Table to load in pin SI1 as json", + ) + load_table.add_argument( + "--table-file", + nargs="?", + type=str, + const=0, + default=None, + help="Table to load in pin SI1 as json file. Default is stdin", + ) + + +def add_pwm_args(subparser: argparse._SubParsersAction): + """Add arguments for pwm {gen,map,set} function to ArgumentParser. + + Parameters + ---------- + subparser : :class:`argparse._SubParsersAction` + SubParser to add other arguments related to pwm_gen function. + """ + pwm = subparser.add_parser("pwm") + pwm_functions = pwm.add_subparsers( + title="PWM Functions", + dest="pwm_function", + ) + pwm_gen = pwm_functions.add_parser("gen") + pwm_gen.add_argument( + "channel", + nargs="+", + choices=["SQ1", "SQ2", "SQ3", "SQ4"], + help="Pin(s) on which to generate a PWM signals", + ) + pwm_gen.add_argument( + "-f", + "--frequency", + type=float, + required=True, + help="Frequency in Hz. Shared by all outputs", + ) + pwm_gen.add_argument( + "-d", + "--duty-cycles", + nargs="+", + type=float, + required=True, + help="Duty cycle between 0 and 1", + ) + pwm_gen.add_argument( + "-p", + "--phases", + nargs="+", + type=float, + default=0, + required=False, + help="Phase between 0 and 1", + ) + map_ = pwm_functions.add_parser("map") + map_.add_argument( + "channel", + nargs="+", + choices=["SQ1", "SQ2", "SQ3", "SQ4"], + help="Digital output pin(s) to which to map the internal oscillator", + ) + map_.add_argument( + "-p", + "--prescaler", + type=int, + required=True, + help="Prescaler value in interval [0, 15]." + + "The output frequency is 128 / (1 << prescaler) MHz", + ) + + +def cmdline(args: List[str] = None): + """Command line for pslab. + + Parameters + ---------- + args : list of strings. + Arguments to parse. + """ + if args is None: + args = sys.argv[1:] + + parser, subparser = get_parser() + add_collect_args(subparser) + add_wave_args(subparser) + add_pwm_args(subparser) + add_install_args(subparser) + add_flash_args(subparser) + main(parser.parse_args(args)) + + +def install(args: argparse.Namespace): + """Install udev rule on Linux. + + Parameters + ---------- + args : :class:`argparse.Namespace` + Parsed arguments. + """ + if not platform.system() == "Linux": + print(f"Installation not required on {platform.system()}.") + return + else: + try: + SerialHandler.check_serial_access_permission() + except OSError: + _install() + return + + if args.force: + _install() + return + + print("User is in dialout/uucp group or udev rule is already installed.") + + +def _install(): + udev_rules = os.path.join(pslab.__path__[0], "99-pslab.rules") + target = "/etc/udev/rules.d/99-pslab.rules" + shutil.copyfile(udev_rules, target) + return + + +def add_install_args(subparser: argparse._SubParsersAction): + """Add arguments for install function to ArgumentParser. + + Parameters + ---------- + subparser : :class:`argparse._SubParsersAction` + SubParser to add other arguments related to install function. + """ + install = subparser.add_parser("install") + install.add_argument( + "-f", + "--force", + action="store_true", + default=False, + help="Overwrite existing udev rule.", + ) + + +def flash(psl: pslab.ScienceLab, hexfile: str): + """Flash firmware over USB. + + PSLab must be in bootloader mode. + """ + if psl.interface.baudrate == 1000000: + psl.interface.timeout = 5 + psl.enter_bootloader() + + try: + bootattrs = mcbootflash.get_boot_attrs(psl) + except struct.error: + print("Flashing failed: PSLab is not in bootloader mode.") + return + + mcbootflash.erase_flash(psl, bootattrs.memory_range, bootattrs.erase_size) + total_bytes, chunks = mcbootflash.chunked(hexfile, bootattrs) + written = 0 + + for chunk in chunks: + mcbootflash.write_flash(psl, chunk) + mcbootflash.checksum(psl, chunk) + written += len(chunk.data) + print(f"{written}/{total_bytes} bytes flashed.", end="\r") + + print("", end="\n") + mcbootflash.self_verify(psl) + mcbootflash.reset(psl) + + +def add_flash_args(subparser: argparse._SubParsersAction): + """Add arguments for flash function to ArgumentParser. + + Parameters + ---------- + subparser : :class:`argparse._SubParsersAction` + SubParser to add other arguments related to flash function. + """ + flash = subparser.add_parser("flash") + flash.add_argument( + "hexfile", + type=str, + help="an Intel HEX file containing application firmware", + ) diff --git a/pslab/connection/__init__.py b/pslab/connection/__init__.py new file mode 100644 index 00000000..fe7b50a0 --- /dev/null +++ b/pslab/connection/__init__.py @@ -0,0 +1,74 @@ +"""Interfaces for communicating with PSLab devices.""" + +from serial.tools import list_ports + +from .connection import ConnectionHandler +from ._serial import SerialHandler +from .wlan import WLANHandler + + +def detect() -> list[ConnectionHandler]: + """Detect PSLab devices. + + Returns + ------- + devices : list[ConnectionHandler] + Handlers for all detected PSLabs. The returned handlers are disconnected; call + .connect() before use. + """ + regex = [] + + for vid, pid in zip(SerialHandler._USB_VID, SerialHandler._USB_PID): + regex.append(f"{vid:04x}:{pid:04x}") + + regex = "(" + "|".join(regex) + ")" + port_info_generator = list_ports.grep(regex) + pslab_devices = [] + + for port_info in port_info_generator: + device = SerialHandler(port=port_info.device, baudrate=1000000, timeout=1) + + try: + device.connect() + except Exception: + pass # nosec + else: + pslab_devices.append(device) + finally: + device.disconnect() + + try: + device = WLANHandler() + device.connect() + except Exception: + pass # nosec + else: + pslab_devices.append(device) + finally: + device.disconnect() + + return pslab_devices + + +def autoconnect() -> ConnectionHandler: + """Automatically connect when exactly one device is present. + + Returns + ------- + device : ConnectionHandler + A handler connected to the detected PSLab device. The handler is connected; it + is not necessary to call .connect before use(). + """ + devices = detect() + + if not devices: + msg = "device not found" + raise ConnectionError(msg) + + if len(devices) > 1: + msg = f"autoconnect failed, multiple devices detected: {devices}" + raise ConnectionError(msg) + + device = devices[0] + device.connect() + return device diff --git a/pslab/connection/_serial.py b/pslab/connection/_serial.py new file mode 100644 index 00000000..d7bf0061 --- /dev/null +++ b/pslab/connection/_serial.py @@ -0,0 +1,158 @@ +"""Serial interface for communicating with PSLab devices.""" + +import os +import platform + +import serial + +import pslab +from pslab.connection.connection import ConnectionHandler + + +def _check_serial_access_permission(): + """Check that we have permission to use the tty on Linux.""" + if platform.system() == "Linux": + import grp + + if os.geteuid() == 0: # Running as root? + return + + for group in os.getgroups(): + if grp.getgrgid(group).gr_name in ( + "dialout", + "uucp", + ): + return + + udev_paths = [ + "/run/udev/rules.d/", + "/etc/udev/rules.d/", + "/lib/udev/rules.d/", + ] + for p in udev_paths: + udev_rules = os.path.join(p, "99-pslab.rules") + if os.path.isfile(udev_rules): + return + else: + raise PermissionError( + "The current user does not have permission to access " + "the PSLab device. To solve this, either:" + "\n\n" + "1. Add the user to the 'dialout' (on Debian-based " + "systems) or 'uucp' (on Arch-based systems) group." + "\n" + "2. Install a udev rule to allow any user access to the " + "device by running 'pslab install' as root, or by " + "manually copying " + f"{pslab.__path__[0]}/99-pslab.rules into {udev_paths[1]}." + "\n\n" + "You may also need to reboot the system for the " + "permission changes to take effect." + ) + + +class SerialHandler(ConnectionHandler): + """Interface for controlling a PSLab over a serial port. + + Parameters + ---------- + port : str + baudrate : int, default 1 MBd + timeout : float, default 1 s + """ + + # V5 V6 + _USB_VID = [0x04D8, 0x10C4] + _USB_PID = [0x00DF, 0xEA60] + + def __init__( + self, + port: str, + baudrate: int = 1000000, + timeout: float = 1.0, + ): + self._port = port + self._ser = serial.Serial( + baudrate=baudrate, + timeout=timeout, + write_timeout=timeout, + ) + _check_serial_access_permission() + + @property + def port(self) -> str: + """Serial port.""" + return self._port + + @property + def baudrate(self) -> int: + """Symbol rate.""" + return self._ser.baudrate + + @baudrate.setter + def baudrate(self, value: int) -> None: + self._ser.baudrate = value + + @property + def timeout(self) -> float: + """Timeout in seconds.""" + return self._ser.timeout + + @timeout.setter + def timeout(self, value: float) -> None: + self._ser.timeout = value + self._ser.write_timeout = value + + def connect(self) -> None: + """Connect to PSLab.""" + self._ser.port = self.port + self._ser.open() + + try: + self.get_version() + except Exception: + self._ser.close() + raise + + def disconnect(self): + """Disconnect from PSLab.""" + self._ser.close() + + def read(self, number_of_bytes: int) -> bytes: + """Read bytes from serial port. + + Parameters + ---------- + number_of_bytes : int + Number of bytes to read from the serial port. + + Returns + ------- + bytes + Bytes read from the serial port. + """ + return self._ser.read(number_of_bytes) + + def write(self, data: bytes) -> int: + """Write bytes to serial port. + + Parameters + ---------- + data : int + Bytes to write to the serial port. + + Returns + ------- + int + Number of bytes written. + """ + return self._ser.write(data) + + def __repr__(self) -> str: # noqa + return ( + f"{self.__class__.__name__}" + "[" + f"{self.port}, " + f"{self.baudrate} baud" + "]" + ) diff --git a/pslab/connection/connection.py b/pslab/connection/connection.py new file mode 100644 index 00000000..d87fd187 --- /dev/null +++ b/pslab/connection/connection.py @@ -0,0 +1,199 @@ +"""Interface objects common to all types of connections.""" + +from abc import ABC, abstractmethod +from dataclasses import dataclass + +import pslab.protocol as CP + + +@dataclass(frozen=True) +class FirmwareVersion: + """Version of pslab-firmware running on connected device. + + Uses semantic versioning conventions. + + Attributes + ---------- + major : int + Major version. Incremented when backward imcompatible changes are made. + minor : int + Minor version. Incremented when new functionality is added, or existing + functionality is changed in a backward compatible manner. + patch : int + Patch version. Incremented when bug fixes are made with do not change the + PSLab's documented behavior. + """ + + major: int + minor: int + patch: int + + +class ConnectionHandler(ABC): + """Abstract base class for PSLab control interfaces.""" + + @abstractmethod + def connect(self) -> None: + """Connect to PSLab.""" + ... + + @abstractmethod + def disconnect(self) -> None: + """Disconnect PSLab.""" + ... + + @abstractmethod + def read(self, numbytes: int) -> bytes: + """Read data from PSLab. + + Parameters + ---------- + numbytes : int + + Returns + ------- + data : bytes + """ + ... + + @abstractmethod + def write(self, data: bytes) -> int: + """Write data to PSLab. + + Parameters + ---------- + data : bytes + + Returns + ------- + numbytes : int + """ + ... + + def get_byte(self) -> int: + """Read a single one-byte of integer value. + + Returns + ------- + int + """ + return int.from_bytes(self.read(1), byteorder="little") + + def get_int(self) -> int: + """Read a single two-byte integer value. + + Returns + ------- + int + """ + return int.from_bytes(self.read(2), byteorder="little") + + def get_long(self) -> int: + """Read a single four-byte integer value. + + Returns + ------- + int + """ + return int.from_bytes(self.read(4), byteorder="little") + + def send_byte(self, data: int | bytes) -> None: + """Write a single one-byte integer value. + + Parameters + ---------- + data : int + """ + if isinstance(data, int): + data = data.to_bytes(length=1, byteorder="little") + self.write(data) + + def send_int(self, data: int | bytes) -> None: + """Write a single two-byte integer value. + + Parameters + ---------- + data : int | bytes + """ + if isinstance(data, int): + data = data.to_bytes(length=2, byteorder="little") + self.write(data) + + def send_long(self, data: int | bytes) -> None: + """Write a single four-byte integer value. + + Parameters + ---------- + data : int | bytes + """ + if isinstance(data, int): + data = data.to_bytes(length=4, byteorder="little") + self.write(data) + + def get_ack(self) -> int: + """Get response code from PSLab. + + Returns + ------- + int + Response code. Meanings: + 0x01 ACK + 0x10 I2C ACK + 0x20 I2C bus collision + 0x10 Radio max retransmits + 0x20 Radio not present + 0x40 Radio reply timout + """ + response = self.read(1) + + if not response: + raise TimeoutError + + ack = CP.Byte.unpack(response)[0] + + if not (ack & 0x01): + raise RuntimeError("Received non ACK byte while waiting for ACK.") + + return ack + + def get_version(self) -> str: + """Query PSLab for its version and return it as a decoded string. + + Returns + ------- + str + Version string. + """ + self.send_byte(CP.COMMON) + self.send_byte(CP.GET_VERSION) + version_length = 9 + version = self.read(version_length) + + try: + if b"PSLab" not in version: + msg = f"got unexpected hardware version: {version}" + raise ConnectionError(msg) + except Exception as exc: + msg = "device not found" + raise ConnectionError(msg) from exc + + return version.decode("utf-8") + + def get_firmware_version(self) -> FirmwareVersion: + """Get firmware version. + + Returns + ------- + tuple[int, int, int] + major, minor, patch. + + """ + self.send_byte(CP.COMMON) + self.send_byte(CP.GET_FW_VERSION) + + # Firmware version query was added in firmware version 3.0.0. + major = self.get_byte() + minor = self.get_byte() + patch = self.get_byte() + + return FirmwareVersion(major, minor, patch) diff --git a/pslab/connection/wlan.py b/pslab/connection/wlan.py new file mode 100644 index 00000000..79cde7ac --- /dev/null +++ b/pslab/connection/wlan.py @@ -0,0 +1,116 @@ +"""Wireless interface for communicating with PSLab devices equiped with ESP8266.""" + +import socket + +from pslab.connection.connection import ConnectionHandler + + +class WLANHandler(ConnectionHandler): + """Interface for controlling a PSLab over WLAN. + + Paramaters + ---------- + host : str, default 192.168.4.1 + Network address of the PSLab. + port : int, default 80 + timeout : float, default 1 s + """ + + def __init__( + self, + host: str = "192.168.4.1", + port: int = 80, + timeout: float = 1.0, + ) -> None: + self._host = host + self._port = port + self._timeout = timeout + self._sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + self._sock.settimeout(timeout) + + @property + def host(self) -> int: + """Network address of the PSLab.""" + return self._host + + @property + def port(self) -> int: + """TCP port number.""" + return self._port + + @property + def timeout(self) -> float: + """Timeout in seconds.""" + return self._timeout + + @timeout.setter + def timeout(self, value: float) -> None: + self._sock.settimeout(value) + + def connect(self) -> None: + """Connect to PSLab.""" + if self._sock.fileno() == -1: + # Socket has been closed. + self._sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + self._sock.settimeout(self.timeout) + + self._sock.connect((self.host, self.port)) + + try: + self.get_version() + except Exception: + self._sock.close() + raise + + def disconnect(self) -> None: + """Disconnect from PSLab.""" + self._sock.close() + + def read(self, numbytes: int) -> bytes: + """Read data over WLAN. + + Parameters + ---------- + numbytes : int + Number of bytes to read. + + Returns + ------- + data : bytes + """ + received = b"" + buf_size = 4096 + remaining = numbytes + + while remaining > 0: + chunk = self._sock.recv(min(remaining, buf_size)) + received += chunk + remaining -= len(chunk) + + return received + + def write(self, data: bytes) -> int: + """Write data over WLAN. + + Parameters + ---------- + data : bytes + + Returns + ------- + numbytes : int + Number of bytes written. + """ + buf_size = 4096 + remaining = len(data) + sent = 0 + + while remaining > 0: + chunk = data[sent : sent + min(remaining, buf_size)] + sent += self._sock.send(chunk) + remaining -= len(chunk) + + return sent + + def __repr__(self) -> str: # noqa + return f"{self.__class__.__name__}[{self.host}:{self.port}]" diff --git a/PSL/SENSORS/AD7718_class.py b/pslab/external/AD7718_class.py similarity index 99% rename from PSL/SENSORS/AD7718_class.py rename to pslab/external/AD7718_class.py index d90df406..4adec5bb 100644 --- a/PSL/SENSORS/AD7718_class.py +++ b/pslab/external/AD7718_class.py @@ -1,5 +1,7 @@ from __future__ import print_function -import time, importlib + +import time + import numpy as np ''' @@ -12,8 +14,6 @@ ''' -import struct - def _bv(x): return 1 << x diff --git a/PSL/SENSORS/AD9833.py b/pslab/external/AD9833.py similarity index 98% rename from PSL/SENSORS/AD9833.py rename to pslab/external/AD9833.py index e8ddad5a..be8556c2 100644 --- a/PSL/SENSORS/AD9833.py +++ b/pslab/external/AD9833.py @@ -1,5 +1,4 @@ -import time, sys -import numpy as np +import sys class AD9833: diff --git a/pslab/external/ADS1115.py b/pslab/external/ADS1115.py new file mode 100644 index 00000000..276c3b14 --- /dev/null +++ b/pslab/external/ADS1115.py @@ -0,0 +1,209 @@ +# -*- coding: utf-8; mode: python; indent-tabs-mode: t; tab-width:4 -*- +from __future__ import print_function + +import time + +from numpy import int16 + +try: + from collections import OrderedDict +except ImportError: + # fallback: try to use the ordereddict backport when using python 2.6 + from ordereddict import OrderedDict + + +def connect(route, **args): + return ADS1115(route, **args) + + +class ADS1115: + ADDRESS = 0x48 # addr pin grounded. floating + + REG_POINTER_MASK = 0x3 + REG_POINTER_CONVERT = 0 + REG_POINTER_CONFIG = 1 + REG_POINTER_LOWTHRESH = 2 + REG_POINTER_HITHRESH = 3 + + REG_CONFIG_OS_MASK = 0x8000 + REG_CONFIG_OS_SINGLE = 0x8000 + REG_CONFIG_OS_BUSY = 0x0000 + REG_CONFIG_OS_NOTBUSY = 0x8000 + + REG_CONFIG_MUX_MASK = 0x7000 + REG_CONFIG_MUX_DIFF_0_1 = 0x0000 # Differential P = AIN0, N = AIN1 =default) + REG_CONFIG_MUX_DIFF_0_3 = 0x1000 # Differential P = AIN0, N = AIN3 + REG_CONFIG_MUX_DIFF_1_3 = 0x2000 # Differential P = AIN1, N = AIN3 + REG_CONFIG_MUX_DIFF_2_3 = 0x3000 # Differential P = AIN2, N = AIN3 + REG_CONFIG_MUX_SINGLE_0 = 0x4000 # Single-ended AIN0 + REG_CONFIG_MUX_SINGLE_1 = 0x5000 # Single-ended AIN1 + REG_CONFIG_MUX_SINGLE_2 = 0x6000 # Single-ended AIN2 + REG_CONFIG_MUX_SINGLE_3 = 0x7000 # Single-ended AIN3 + + REG_CONFIG_PGA_MASK = 0x0E00 # bits 11:9 + REG_CONFIG_PGA_6_144V = (0 << 9) # +/-6.144V range = Gain 2/3 + REG_CONFIG_PGA_4_096V = (1 << 9) # +/-4.096V range = Gain 1 + REG_CONFIG_PGA_2_048V = (2 << 9) # +/-2.048V range = Gain 2 =default) + REG_CONFIG_PGA_1_024V = (3 << 9) # +/-1.024V range = Gain 4 + REG_CONFIG_PGA_0_512V = (4 << 9) # +/-0.512V range = Gain 8 + REG_CONFIG_PGA_0_256V = (5 << 9) # +/-0.256V range = Gain 16 + + REG_CONFIG_MODE_MASK = 0x0100 # bit 8 + REG_CONFIG_MODE_CONTIN = (0 << 8) # Continuous conversion mode + REG_CONFIG_MODE_SINGLE = (1 << 8) # Power-down single-shot mode =default) + + REG_CONFIG_DR_MASK = 0x00E0 + REG_CONFIG_DR_8SPS = (0 << 5) # 8 SPS + REG_CONFIG_DR_16SPS = (1 << 5) # 16 SPS + REG_CONFIG_DR_32SPS = (2 << 5) # 32 SPS + REG_CONFIG_DR_64SPS = (3 << 5) # 64 SPS + REG_CONFIG_DR_128SPS = (4 << 5) # 128 SPS + REG_CONFIG_DR_250SPS = (5 << 5) # 260 SPS + REG_CONFIG_DR_475SPS = (6 << 5) # 475 SPS + REG_CONFIG_DR_860SPS = (7 << 5) # 860 SPS + + REG_CONFIG_CMODE_MASK = 0x0010 + REG_CONFIG_CMODE_TRAD = 0x0000 + REG_CONFIG_CMODE_WINDOW = 0x0010 + + REG_CONFIG_CPOL_MASK = 0x0008 + REG_CONFIG_CPOL_ACTVLOW = 0x0000 + REG_CONFIG_CPOL_ACTVHI = 0x0008 + + REG_CONFIG_CLAT_MASK = 0x0004 + REG_CONFIG_CLAT_NONLAT = 0x0000 + REG_CONFIG_CLAT_LATCH = 0x0004 + + REG_CONFIG_CQUE_MASK = 0x0003 + REG_CONFIG_CQUE_1CONV = 0x0000 + REG_CONFIG_CQUE_2CONV = 0x0001 + REG_CONFIG_CQUE_4CONV = 0x0002 + REG_CONFIG_CQUE_NONE = 0x0003 + gains = OrderedDict([('GAIN_TWOTHIRDS', REG_CONFIG_PGA_6_144V), ('GAIN_ONE', REG_CONFIG_PGA_4_096V), + ('GAIN_TWO', REG_CONFIG_PGA_2_048V), ('GAIN_FOUR', REG_CONFIG_PGA_1_024V), + ('GAIN_EIGHT', REG_CONFIG_PGA_0_512V), ('GAIN_SIXTEEN', REG_CONFIG_PGA_0_256V)]) + gain_scaling = OrderedDict( + [('GAIN_TWOTHIRDS', 0.1875), ('GAIN_ONE', 0.125), ('GAIN_TWO', 0.0625), ('GAIN_FOUR', 0.03125), + ('GAIN_EIGHT', 0.015625), ('GAIN_SIXTEEN', 0.0078125)]) + type_selection = OrderedDict( + [('UNI_0', 0), ('UNI_1', 1), ('UNI_2', 2), ('UNI_3', 3), ('DIFF_01', '01'), ('DIFF_23', '23')]) + sdr_selection = OrderedDict( + [(8, REG_CONFIG_DR_8SPS), (16, REG_CONFIG_DR_16SPS), (32, REG_CONFIG_DR_32SPS), (64, REG_CONFIG_DR_64SPS), + (128, REG_CONFIG_DR_128SPS), (250, REG_CONFIG_DR_250SPS), (475, REG_CONFIG_DR_475SPS), + (860, REG_CONFIG_DR_860SPS)]) # sampling data rate + + NUMPLOTS = 1 + PLOTNAMES = ['mV'] + + def __init__(self, I2C, **args): + self.ADDRESS = args.get('address', self.ADDRESS) + self.I2C = I2C + self.channel = 'UNI_0' + self.gain = 'GAIN_ONE' + self.rate = 128 + + self.setGain('GAIN_ONE') + self.setChannel('UNI_0') + self.setDataRate(128) + self.conversionDelay = 8 + self.name = 'ADS1115 16-bit ADC' + self.params = {'setGain': self.gains.keys(), 'setChannel': self.type_selection.keys(), + 'setDataRate': self.sdr_selection.keys()} + + def __readInt__(self, addr): + return int16(self.__readUInt__(addr)) + + def __readUInt__(self, addr): + vals = self.I2C.readBulk(self.ADDRESS, addr, 2) + v = 1. * ((vals[0] << 8) | vals[1]) + return v + + def initTemperature(self): + self.I2C.writeBulk(self.ADDRESS, [self.REG_CONTROL, self.CMD_TEMP]) + time.sleep(0.005) + + def readRegister(self, register): + vals = self.I2C.readBulk(self.ADDRESS, register, 2) + return (vals[0] << 8) | vals[1] + + def writeRegister(self, reg, value): + self.I2C.writeBulk(self.ADDRESS, [reg, (value >> 8) & 0xFF, value & 0xFF]) + + def setGain(self, gain): + ''' + options : 'GAIN_TWOTHIRDS','GAIN_ONE','GAIN_TWO','GAIN_FOUR','GAIN_EIGHT','GAIN_SIXTEEN' + ''' + self.gain = gain + + def setChannel(self, channel): + ''' + options 'UNI_0','UNI_1','UNI_2','UNI_3','DIFF_01','DIFF_23' + ''' + self.channel = channel + + def setDataRate(self, rate): + ''' + data rate options 8,16,32,64,128,250,475,860 SPS + ''' + self.rate = rate + + def readADC_SingleEnded(self, chan): + if chan > 3: return None + # start with default values + config = (self.REG_CONFIG_CQUE_NONE # Disable the comparator (default val) + | self.REG_CONFIG_CLAT_NONLAT # Non-latching (default val) + | self.REG_CONFIG_CPOL_ACTVLOW # Alert/Rdy active low (default val) + | self.REG_CONFIG_CMODE_TRAD # Traditional comparator (default val) + | self.sdr_selection[self.rate] # 1600 samples per second (default) + | self.REG_CONFIG_MODE_SINGLE) # Single-shot mode (default) + + # Set PGA/voltage range + config |= self.gains[self.gain] + + if chan == 0: + config |= self.REG_CONFIG_MUX_SINGLE_0 + elif chan == 1: + config |= self.REG_CONFIG_MUX_SINGLE_1 + elif chan == 2: + config |= self.REG_CONFIG_MUX_SINGLE_2 + elif chan == 3: + config |= self.REG_CONFIG_MUX_SINGLE_3 + # Set 'start single-conversion' bit + config |= self.REG_CONFIG_OS_SINGLE + self.writeRegister(self.REG_POINTER_CONFIG, config); + time.sleep(1. / self.rate + .002) # convert to mS to S + return self.readRegister(self.REG_POINTER_CONVERT) * self.gain_scaling[self.gain] + + def readADC_Differential(self, chan='01'): + # start with default values + config = (self.REG_CONFIG_CQUE_NONE # Disable the comparator (default val) + | self.REG_CONFIG_CLAT_NONLAT # Non-latching (default val) + | self.REG_CONFIG_CPOL_ACTVLOW # Alert/Rdy active low (default val) + | self.REG_CONFIG_CMODE_TRAD # Traditional comparator (default val) + | self.sdr_selection[self.rate] # samples per second + | self.REG_CONFIG_MODE_SINGLE) # Single-shot mode (default) + + # Set PGA/voltage range + config |= self.gains[self.gain] + if chan == '01': + config |= self.REG_CONFIG_MUX_DIFF_0_1 + elif chan == '23': + config |= self.REG_CONFIG_MUX_DIFF_2_3 + # Set 'start single-conversion' bit + config |= self.REG_CONFIG_OS_SINGLE + self.writeRegister(self.REG_POINTER_CONFIG, config); + time.sleep(1. / self.rate + .002) # convert to mS to S + return int16(self.readRegister(self.REG_POINTER_CONVERT)) * self.gain_scaling[self.gain] + + def getLastResults(self): + return int16(self.readRegister(self.REG_POINTER_CONVERT)) * self.gain_scaling[self.gain] + + def getRaw(self): + ''' + return values in mV + ''' + chan = self.type_selection[self.channel] + if self.channel[:3] == 'UNI': + return [self.readADC_SingleEnded(chan)] + elif self.channel[:3] == 'DIF': + return [self.readADC_Differential(chan)] diff --git a/PSL/SENSORS/BH1750.py b/pslab/external/BH1750.py similarity index 100% rename from PSL/SENSORS/BH1750.py rename to pslab/external/BH1750.py diff --git a/PSL/SENSORS/ComplementaryFilter.py b/pslab/external/ComplementaryFilter.py similarity index 100% rename from PSL/SENSORS/ComplementaryFilter.py rename to pslab/external/ComplementaryFilter.py diff --git a/PSL/SENSORS/HMC5883L.py b/pslab/external/HMC5883L.py similarity index 98% rename from PSL/SENSORS/HMC5883L.py rename to pslab/external/HMC5883L.py index 8128a8c8..7f299291 100644 --- a/PSL/SENSORS/HMC5883L.py +++ b/pslab/external/HMC5883L.py @@ -1,6 +1,3 @@ -from numpy import int16 - - def connect(route, **args): return HMC5883L(route, **args) @@ -63,7 +60,7 @@ def __writeCONFB__(self): def __writeCONFA__(self): self.I2C.writeBulk(self.ADDRESS, [self.CONFA, (self.dataOutputRate << 2) | (self.samplesToAverage << 5) | ( - self.measurementConf)]) + self.measurementConf)]) def setSamplesToAverage(self, num): self.samplesToAverage = self.samplesToAverage_choices.index(num) diff --git a/PSL/SENSORS/Kalman.py b/pslab/external/Kalman.py similarity index 100% rename from PSL/SENSORS/Kalman.py rename to pslab/external/Kalman.py diff --git a/PSL/SENSORS/MF522.py b/pslab/external/MF522.py similarity index 99% rename from PSL/SENSORS/MF522.py rename to pslab/external/MF522.py index 1e64ca52..1b304547 100644 --- a/PSL/SENSORS/MF522.py +++ b/pslab/external/MF522.py @@ -1,12 +1,7 @@ # -*- coding: utf-8 -*- # MF522 - Software stack to access the MF522 RFID reader via FOSSASIA PSLab # - - - from __future__ import print_function -from PSL import sciencelab -import time def connect(I, cs): diff --git a/PSL/SENSORS/MLX90614.py b/pslab/external/MLX90614.py similarity index 56% rename from PSL/SENSORS/MLX90614.py rename to pslab/external/MLX90614.py index ce26342c..6b963676 100644 --- a/PSL/SENSORS/MLX90614.py +++ b/pslab/external/MLX90614.py @@ -1,46 +1,40 @@ -from __future__ import print_function +from pslab.bus import I2CSlave - -def connect(route, **args): - return MLX90614(route, **args) - - -class MLX90614(): +class MLX90614(I2CSlave): + _ADDRESS = 0x5A + _OBJADDR = 0x07 + _AMBADDR = 0x06 NUMPLOTS = 1 PLOTNAMES = ['Temp'] - ADDRESS = 0x5A name = 'PIR temperature' - def __init__(self, I2C, **args): - self.I2C = I2C - self.ADDRESS = args.get('address', self.ADDRESS) - self.OBJADDR = 0x07 - self.AMBADDR = 0x06 + def __init__(self): + super().__init__(self._ADDRESS) - self.source = self.OBJADDR + self.source = self._OBJADDR self.name = 'Passive IR temperature sensor' - self.params = {'readReg': {'dataType':'integer','min':0,'max':0x20,'prefix':'Addr: '} , + self.params = {'readReg': {'dataType': 'integer', 'min': 0, 'max': 0x20, 'prefix': 'Addr: '}, 'select_source': ['object temperature', 'ambient temperature']} - try: - print('switching baud to 100k') - self.I2C.configI2C(100e3) - except Exception as e: - print('FAILED TO CHANGE BAUD RATE',e.message) + # try: + # print('switching baud to 100k') + # self.I2C.configI2C(100e3) + # except Exception as e: + # print('FAILED TO CHANGE BAUD RATE', e.message) def select_source(self, source): if source == 'object temperature': - self.source = self.OBJADDR + self.source = self._OBJADDR elif source == 'ambient temperature': - self.source = self.AMBADDR + self.source = self._AMBADDR def readReg(self, addr): x = self.getVals(addr, 2) print(hex(addr), hex(x[0] | (x[1] << 8))) def getVals(self, addr, numbytes): - vals = self.I2C.readBulk(self.ADDRESS, addr, numbytes) + vals = self.read(numbytes, addr) return vals def getRaw(self): @@ -54,7 +48,7 @@ def getRaw(self): return False def getObjectTemperature(self): - self.source = self.OBJADDR + self.source = self._OBJADDR val = self.getRaw() if val: return val[0] @@ -62,7 +56,7 @@ def getObjectTemperature(self): return False def getAmbientTemperature(self): - self.source = self.AMBADDR + self.source = self._AMBADDR val = self.getRaw() if val: return val[0] diff --git a/PSL/SENSORS/MPU6050.py b/pslab/external/MPU6050.py similarity index 96% rename from PSL/SENSORS/MPU6050.py rename to pslab/external/MPU6050.py index c2f56578..bf6beb76 100644 --- a/PSL/SENSORS/MPU6050.py +++ b/pslab/external/MPU6050.py @@ -33,8 +33,8 @@ def __init__(self, I2C, **args): self.I2C = I2C self.ADDRESS = args.get('address', self.ADDRESS) self.name = 'Accel/gyro' - self.params = {'powerUp': None, 'setGyroRange': [250, 500, 1000, 2000], 'setAccelRange' : [2, 4, 8, 16], - 'KalmanFilter': {'dataType':'double','min':0,'max':1000,'prefix':'value: '} } + self.params = {'powerUp': None, 'setGyroRange': [250, 500, 1000, 2000], 'setAccelRange': [2, 4, 8, 16], + 'KalmanFilter': {'dataType': 'double', 'min': 0, 'max': 1000, 'prefix': 'value: '}} self.setGyroRange(2000) self.setAccelRange(16) self.powerUp() diff --git a/pslab/external/MPU925x.py b/pslab/external/MPU925x.py new file mode 100644 index 00000000..93cf4ff3 --- /dev/null +++ b/pslab/external/MPU925x.py @@ -0,0 +1,199 @@ +# -*- coding: utf-8; mode: python; indent-tabs-mode: t; tab-width:4 -*- +from Kalman import KalmanFilter + + +def connect(route, **args): + return MPU925x(route, **args) + + +class MPU925x(): + ''' + Mandatory members: + GetRaw : Function called by Graphical apps. Must return values stored in a list + NUMPLOTS : length of list returned by GetRaw. Even single datapoints need to be stored in a list before returning + PLOTNAMES : a list of strings describing each element in the list returned by GetRaw. len(PLOTNAMES) = NUMPLOTS + name : the name of the sensor shown to the user + params: + A dictionary of function calls(single arguments only) paired with list of valid argument values. (Primitive. I know.) + These calls can be used for one time configuration settings + + ''' + INT_PIN_CFG = 0x37 + GYRO_CONFIG = 0x1B + ACCEL_CONFIG = 0x1C + GYRO_SCALING = [131, 65.5, 32.8, 16.4] + ACCEL_SCALING = [16384, 8192, 4096, 2048] + AR = 3 + GR = 3 + NUMPLOTS = 7 + PLOTNAMES = ['Ax', 'Ay', 'Az', 'Temp', 'Gx', 'Gy', 'Gz'] + ADDRESS = 0x68 + AK8963_ADDRESS = 0x0C + AK8963_CNTL = 0x0A + name = 'Accel/gyro' + + def __init__(self, I2C, **args): + self.I2C = I2C + self.ADDRESS = args.get('address', self.ADDRESS) + self.name = 'Accel/gyro' + self.params = {'powerUp': None, 'setGyroRange': [250, 500, 1000, 2000], 'setAccelRange': [2, 4, 8, 16], + 'KalmanFilter': [.01, .1, 1, 10, 100, 1000, 10000, 'OFF']} + self.setGyroRange(2000) + self.setAccelRange(16) + self.powerUp() + self.K = None + + def KalmanFilter(self, opt): + if opt == 'OFF': + self.K = None + return + noise = [[]] * self.NUMPLOTS + for a in range(500): + vals = self.getRaw() + for b in range(self.NUMPLOTS): noise[b].append(vals[b]) + + self.K = [None] * 7 + for a in range(self.NUMPLOTS): + sd = std(noise[a]) + self.K[a] = KalmanFilter(1. / opt, sd ** 2) + + def getVals(self, addr, numbytes): + return self.I2C.readBulk(self.ADDRESS, addr, numbytes) + + def powerUp(self): + self.I2C.writeBulk(self.ADDRESS, [0x6B, 0]) + + def setGyroRange(self, rs): + self.GR = self.params['setGyroRange'].index(rs) + self.I2C.writeBulk(self.ADDRESS, [self.GYRO_CONFIG, self.GR << 3]) + + def setAccelRange(self, rs): + self.AR = self.params['setAccelRange'].index(rs) + self.I2C.writeBulk(self.ADDRESS, [self.ACCEL_CONFIG, self.AR << 3]) + + def getRaw(self): + ''' + This method must be defined if you want GUIs to use this class to generate plots on the fly. + It must return a set of different values read from the sensor. such as X,Y,Z acceleration. + The length of this list must not change, and must be defined in the variable NUMPLOTS. + + GUIs will generate as many plots, and the data returned from this method will be appended appropriately + ''' + vals = self.getVals(0x3B, 14) + if vals: + if len(vals) == 14: + raw = [0] * 7 + for a in range(3): raw[a] = 1. * int16(vals[a * 2] << 8 | vals[a * 2 + 1]) / self.ACCEL_SCALING[self.AR] + for a in range(4, 7): raw[a] = 1. * int16(vals[a * 2] << 8 | vals[a * 2 + 1]) / self.GYRO_SCALING[ + self.GR] + raw[3] = int16(vals[6] << 8 | vals[7]) / 340. + 36.53 + if not self.K: + return raw + else: + for b in range(self.NUMPLOTS): + self.K[b].input_latest_noisy_measurement(raw[b]) + raw[b] = self.K[b].get_latest_estimated_measurement() + return raw + + else: + return False + else: + return False + + def getAccel(self): + ''' + Return a list of 3 values for acceleration vector + + ''' + vals = self.getVals(0x3B, 6) + ax = int16(vals[0] << 8 | vals[1]) + ay = int16(vals[2] << 8 | vals[3]) + az = int16(vals[4] << 8 | vals[5]) + return [ax / 65535., ay / 65535., az / 65535.] + + def getTemp(self): + ''' + Return temperature + ''' + vals = self.getVals(0x41, 6) + t = int16(vals[0] << 8 | vals[1]) + return t / 65535. + + def getGyro(self): + ''' + Return a list of 3 values for angular velocity vector + + ''' + vals = self.getVals(0x43, 6) + ax = int16(vals[0] << 8 | vals[1]) + ay = int16(vals[2] << 8 | vals[3]) + az = int16(vals[4] << 8 | vals[5]) + return [ax / 65535., ay / 65535., az / 65535.] + + def getMag(self): + ''' + Return a list of 3 values for magnetic field vector + + ''' + vals = self.I2C.readBulk(self.AK8963_ADDRESS, 0x03, + 7) # 6+1 . 1(ST2) should not have bit 4 (0x8) true. It's ideally 16 . overflow bit + ax = int16(vals[0] << 8 | vals[1]) + ay = int16(vals[2] << 8 | vals[3]) + az = int16(vals[4] << 8 | vals[5]) + if not vals[6] & 0x08: + return [ax / 65535., ay / 65535., az / 65535.] + else: + return None + + def WhoAmI(self): + ''' + Returns the ID. + It is 71 for MPU9250. + ''' + v = self.I2C.readBulk(self.ADDRESS, 0x75, 1)[0] + if v not in [0x71, 0x73]: return 'Error %s' % hex(v) + + if v == 0x73: + return 'MPU9255 %s' % hex(v) + elif v == 0x71: + return 'MPU9250 %s' % hex(v) + + def WhoAmI_AK8963(self): + ''' + Returns the ID fo magnetometer AK8963 if found. + It should be 0x48. + ''' + self.initMagnetometer() + v = self.I2C.readBulk(self.AK8963_ADDRESS, 0, 1)[0] + if v == 0x48: + return 'AK8963 at %s' % hex(v) + else: + return 'AK8963 not found. returned :%s' % hex(v) + + def initMagnetometer(self): + ''' + For MPU925x with integrated magnetometer. + It's called a 10 DoF sensor, but technically speaking , + the 3-axis Accel , 3-Axis Gyro, temperature sensor are integrated in one IC, and the 3-axis magnetometer is implemented in a + separate IC which can be accessed via an I2C passthrough. + Therefore , in order to detect the magnetometer via an I2C scan, the passthrough must first be enabled on IC#1 (Accel,gyro,temp) + ''' + self.I2C.writeBulk(self.ADDRESS, [self.INT_PIN_CFG, 0x22]) # I2C passthrough + self.I2C.writeBulk(self.AK8963_ADDRESS, [self.AK8963_CNTL, 0]) # power down mag + self.I2C.writeBulk(self.AK8963_ADDRESS, + [self.AK8963_CNTL, (1 << 4) | 6]) # mode (0=14bits,1=16bits) <<4 | (2=8Hz , 6=100Hz) + + +if __name__ == "__main__": + from PSL import sciencelab + + I = sciencelab.connect() + A = connect(I.I2C) + t, x, y, z = I.I2C.capture(A.ADDRESS, 0x43, 6, 5000, 1000, 'int') + # print (t,x,y,z) + from pylab import * + + plot(t, x) + plot(t, y) + plot(t, z) + show() diff --git a/PSL/SENSORS/SHT21.py b/pslab/external/SHT21.py similarity index 80% rename from PSL/SENSORS/SHT21.py rename to pslab/external/SHT21.py index 76285c8b..c514e662 100644 --- a/PSL/SENSORS/SHT21.py +++ b/pslab/external/SHT21.py @@ -8,27 +8,28 @@ def connect(route, **args): ''' return SHT21(route, **args) -def rawToTemp( vals): - if vals: - if len(vals): - v = (vals[0] << 8) | (vals[1] & 0xFC) # make integer & remove status bits - v *= 175.72 - v /= (1 << 16) - v -= 46.85 - return [v] - return False -def rawToRH( vals): - if vals: - if len(vals): - v = (vals[0] << 8) | (vals[1] & 0xFC) # make integer & remove status bits - v *= 125. - v /= (1 << 16) - v -= 6 - return [v] - return False +def rawToTemp(vals): + if vals: + if len(vals): + v = (vals[0] << 8) | (vals[1] & 0xFC) # make integer & remove status bits + v *= 175.72 + v /= (1 << 16) + v -= 46.85 + return [v] + return False +def rawToRH(vals): + if vals: + if len(vals): + v = (vals[0] << 8) | (vals[1] & 0xFC) # make integer & remove status bits + v *= 125. + v /= (1 << 16) + v -= 6 + return [v] + return False + class SHT21(): RESET = 0xFE @@ -51,14 +52,13 @@ def __init__(self, I2C, **args): except: print ('FAILED TO CHANGE BAUD RATE') ''' - self.params = {'selectParameter': ['temperature', 'humidity'],'init':None} + self.params = {'selectParameter': ['temperature', 'humidity'], 'init': None} self.init() def init(self): self.I2C.writeBulk(self.ADDRESS, [self.RESET]) # soft reset time.sleep(0.1) - @staticmethod def _calculate_checksum(data, number_of_bytes): """5.7 CRC Checksum using the polynomial given in the datasheet diff --git a/pslab/external/Sx1276.py b/pslab/external/Sx1276.py new file mode 100644 index 00000000..ffa5fecc --- /dev/null +++ b/pslab/external/Sx1276.py @@ -0,0 +1,347 @@ +# Registers adapted from sample code for SEMTECH SX1276 +from __future__ import print_function + +import time + + +def connect(SPI, frq, **kwargs): + return SX1276(SPI, frq, **kwargs) + + +class SX1276(): + name = 'SX1276' + # registers + REG_FIFO = 0x00 + REG_OP_MODE = 0x01 + REG_FRF_MSB = 0x06 + REG_FRF_MID = 0x07 + REG_FRF_LSB = 0x08 + REG_PA_CONFIG = 0x09 + REG_LNA = 0x0c + REG_FIFO_ADDR_PTR = 0x0d + REG_FIFO_TX_BASE_ADDR = 0x0e + REG_FIFO_RX_BASE_ADDR = 0x0f + REG_FIFO_RX_CURRENT_ADDR = 0x10 + REG_IRQ_FLAGS = 0x12 + REG_RX_NB_BYTES = 0x13 + REG_PKT_RSSI_VALUE = 0x1a + REG_PKT_SNR_VALUE = 0x1b + REG_MODEM_CONFIG_1 = 0x1d + REG_MODEM_CONFIG_2 = 0x1e + REG_PREAMBLE_MSB = 0x20 + REG_PREAMBLE_LSB = 0x21 + REG_PAYLOAD_LENGTH = 0x22 + REG_MODEM_CONFIG_3 = 0x26 + REG_RSSI_WIDEBAND = 0x2c + REG_DETECTION_OPTIMIZE = 0x31 + REG_DETECTION_THRESHOLD = 0x37 + REG_SYNC_WORD = 0x39 + REG_DIO_MAPPING_1 = 0x40 + REG_VERSION = 0x42 + REG_PA_DAC = 0x4D + # modes + MODE_LONG_RANGE_MODE = 0x80 + MODE_SLEEP = 0x00 + MODE_STDBY = 0x01 + MODE_TX = 0x03 + MODE_RX_CONTINUOUS = 0x05 + MODE_RX_SINGLE = 0x06 + + # PA config + PA_BOOST = 0x80 + + # IRQ masks + IRQ_TX_DONE_MASK = 0x08 + IRQ_PAYLOAD_CRC_ERROR_MASK = 0x20 + IRQ_RX_DONE_MASK = 0x40 + + MAX_PKT_LENGTH = 255 + + PA_OUTPUT_RFO_PIN = 0 + PA_OUTPUT_PA_BOOST_PIN = 1 + _onReceive = 0 + _frequency = 10 + _packetIndex = 0 + packetLength = 0 + + def __init__(self, SPI, frq, **kwargs): + self.SPI = SPI + self.SPI.set_parameters(2, 6, 1, 0) + self.name = 'SX1276' + self.frequency = frq + + self.reset() + self.version = self.SPIRead(self.REG_VERSION, 1)[0] + if self.version != 0x12: + print('version error', self.version) + self.sleep() + self.setFrequency(self.frequency) + + # set base address + self.SPIWrite(self.REG_FIFO_TX_BASE_ADDR, [0]) + self.SPIWrite(self.REG_FIFO_RX_BASE_ADDR, [0]) + + # set LNA boost + self.SPIWrite(self.REG_LNA, [self.SPIRead(self.REG_LNA)[0] | 0x03]) + + # set auto ADC + self.SPIWrite(self.REG_MODEM_CONFIG_3, [0x04]) + + # output power 17dbm + self.setTxPower(kwargs.get('power', 17), + self.PA_OUTPUT_PA_BOOST_PIN if kwargs.get('boost', True) else self.PA_OUTPUT_RFO_PIN) + self.idle() + + # set bandwidth + self.setSignalBandwidth(kwargs.get('BW', 125e3)) + self.setSpreadingFactor(kwargs.get('SF', 12)) + self.setCodingRate4(kwargs.get('CF', 5)) + + def beginPacket(self, implicitHeader=False): + self.idle() + if implicitHeader: + self.implicitHeaderMode() + else: + self.explicitHeaderMode() + + # reset FIFO & payload length + self.SPIWrite(self.REG_FIFO_ADDR_PTR, [0]) + self.SPIWrite(self.REG_PAYLOAD_LENGTH, [0]) + + def endPacket(self): + # put in TX mode + self.SPIWrite(self.REG_OP_MODE, [self.MODE_LONG_RANGE_MODE | self.MODE_TX]) + while 1: # Wait for TX done + if self.SPIRead(self.REG_IRQ_FLAGS, 1)[0] & self.IRQ_TX_DONE_MASK: + break + else: + print('wait...') + time.sleep(0.1) + self.SPIWrite(self.REG_IRQ_FLAGS, [self.IRQ_TX_DONE_MASK]) + + def parsePacket(self, size=0): + self.packetLength = 0 + irqFlags = self.SPIRead(self.REG_IRQ_FLAGS, 1)[0] + if size > 0: + self.implicitHeaderMode() + self.SPIWrite(self.REG_PAYLOAD_LENGTH, [size & 0xFF]) + else: + self.explicitHeaderMode() + self.SPIWrite(self.REG_IRQ_FLAGS, [irqFlags]) + if (irqFlags & self.IRQ_RX_DONE_MASK) and (irqFlags & self.IRQ_PAYLOAD_CRC_ERROR_MASK) == 0: + self._packetIndex = 0 + if self._implicitHeaderMode: + self.packetLength = self.SPIRead(self.REG_PAYLOAD_LENGTH, 1)[0] + else: + self.packetLength = self.SPIRead(self.REG_RX_NB_BYTES, 1)[0] + self.SPIWrite(self.REG_FIFO_ADDR_PTR, self.SPIRead(self.REG_FIFO_RX_CURRENT_ADDR, 1)) + self.idle() + elif self.SPIRead(self.REG_OP_MODE)[0] != (self.MODE_LONG_RANGE_MODE | self.MODE_RX_SINGLE): + self.SPIWrite(self.REG_FIFO_ADDR_PTR, [0]) + self.SPIWrite(self.REG_OP_MODE, [self.MODE_LONG_RANGE_MODE | self.MODE_RX_SINGLE]) + return self.packetLength + + def packetRssi(self): + return self.SPIRead(self.REG_PKT_RSSI_VALUE)[0] - (164 if self._frequency < 868e6 else 157) + + def packetSnr(self): + return self.SPIRead(self.REG_PKT_SNR_VALUE)[0] * 0.25 + + def write(self, byteArray): + size = len(byteArray) + currentLength = self.SPIRead(self.REG_PAYLOAD_LENGTH)[0] + if (currentLength + size) > self.MAX_PKT_LENGTH: + size = self.MAX_PKT_LENGTH - currentLength + self.SPIWrite(self.REG_FIFO, byteArray[:size]) + self.SPIWrite(self.REG_PAYLOAD_LENGTH, [currentLength + size]) + return size + + def available(self): + return self.SPIRead(self.REG_RX_NB_BYTES)[0] - self._packetIndex + + def checkRx(self): + irqFlags = self.SPIRead(self.REG_IRQ_FLAGS, 1)[0] + if (irqFlags & self.IRQ_RX_DONE_MASK) and (irqFlags & self.IRQ_PAYLOAD_CRC_ERROR_MASK) == 0: + return 1 + return 0; + + def read(self): + if not self.available(): return -1 + self._packetIndex += 1 + return self.SPIRead(self.REG_FIFO)[0] + + def readAll(self): + p = [] + while self.available(): + p.append(self.read()) + return p + + def peek(self): + if not self.available(): return -1 + self.currentAddress = self.SPIRead(self.REG_FIFO_ADDR_PTR) + val = self.SPIRead(self.REG_FIFO)[0] + self.SPIWrite(self.REG_FIFO_ADDR_PTR, self.currentAddress) + return val + + def flush(self): + pass + + def receive(self, size): + if size > 0: + self.implicitHeaderMode() + self.SPIWrite(self.REG_PAYLOAD_LENGTH, [size & 0xFF]) + else: + self.explicitHeaderMode() + + self.SPIWrite(self.REG_OP_MODE, [self.MODE_LONG_RANGE_MODE | self.MODE_RX_SINGLE]) + + def reset(self): + pass + + def idle(self): + self.SPIWrite(self.REG_OP_MODE, [self.MODE_LONG_RANGE_MODE | self.MODE_STDBY]) + + def sleep(self): + self.SPIWrite(self.REG_OP_MODE, [self.MODE_LONG_RANGE_MODE | self.MODE_SLEEP]) + + def setTxPower(self, level, pin): + if pin == self.PA_OUTPUT_RFO_PIN: + if level < 0: + level = 0 + elif level > 14: + level = 14 + self.SPIWrite(self.REG_PA_CONFIG, [0x70 | level]) + else: + if level < 2: + level = 2 + elif level > 17: + level = 17 + if level == 17: + print('max power output') + self.SPIWrite(self.REG_PA_DAC, [0x87]) + else: + self.SPIWrite(self.REG_PA_DAC, [0x84]) + self.SPIWrite(self.REG_PA_CONFIG, [self.PA_BOOST | 0x70 | (level - 2)]) + + print('power', hex(self.SPIRead(self.REG_PA_CONFIG)[0])) + + def setFrequency(self, frq): + self._frequency = frq + frf = (int(frq) << 19) / 32000000 + print('frf', frf) + print('freq', (frf >> 16) & 0xFF, (frf >> 8) & 0xFF, (frf) & 0xFF) + self.SPIWrite(self.REG_FRF_MSB, [(frf >> 16) & 0xFF]) + self.SPIWrite(self.REG_FRF_MID, [(frf >> 8) & 0xFF]) + self.SPIWrite(self.REG_FRF_LSB, [frf & 0xFF]) + + def setSpreadingFactor(self, sf): + if sf < 6: + sf = 6 + elif sf > 12: + sf = 12 + + if sf == 6: + self.SPIWrite(self.REG_DETECTION_OPTIMIZE, [0xc5]) + self.SPIWrite(self.REG_DETECTION_THRESHOLD, [0x0c]) + else: + self.SPIWrite(self.REG_DETECTION_OPTIMIZE, [0xc3]) + self.SPIWrite(self.REG_DETECTION_THRESHOLD, [0x0a]) + self.SPIWrite(self.REG_MODEM_CONFIG_2, [(self.SPIRead(self.REG_MODEM_CONFIG_2)[0] & 0x0F) | ((sf << 4) & 0xF0)]) + + def setSignalBandwidth(self, sbw): + bw = 9 + num = 0 + for a in [7.8e3, 10.4e3, 15.6e3, 20.8e3, 31.25e3, 41.7e3, 62.5e3, 125e3, 250e3]: + if sbw <= a: + bw = num + break + num += 1 + print('bandwidth: ', bw) + self.SPIWrite(self.REG_MODEM_CONFIG_1, [(self.SPIRead(self.REG_MODEM_CONFIG_1)[0] & 0x0F) | (bw << 4)]) + + def setCodingRate4(self, denominator): + if denominator < 5: + denominator = 5 + elif denominator > 8: + denominator = 8 + self.SPIWrite(self.REG_MODEM_CONFIG_1, + [(self.SPIRead(self.REG_MODEM_CONFIG_1)[0] & 0xF1) | ((denominator - 4) << 4)]) + + def setPreambleLength(self, length): + self.SPIWrite(self.REG_PREAMBLE_MSB, [(length >> 8) & 0xFF]) + self.SPIWrite(self.REG_PREAMBLE_LSB, [length & 0xFF]) + + def setSyncWord(self, sw): + self.SPIWrite(self.REG_SYNC_WORD, [sw]) + + def crc(self): + self.SPIWrite(self.REG_MODEM_CONFIG_2, [self.SPIRead(self.REG_MODEM_CONFIG_2)[0] | 0x04]) + + def noCrc(self): + self.SPIWrite(self.REG_MODEM_CONFIG_2, [self.SPIRead(self.REG_MODEM_CONFIG_2)[0] & 0xFB]) + + def random(self): + return self.SPIRead(self.REG_RSSI_WIDEBAND)[0] + + def explicitHeaderMode(self): + self._implicitHeaderMode = 0 + self.SPIWrite(self.REG_MODEM_CONFIG_1, [self.SPIRead(self.REG_MODEM_CONFIG_1)[0] & 0xFE]) + + def implicitHeaderMode(self): + self._implicitHeaderMode = 1 + self.SPIWrite(self.REG_MODEM_CONFIG_1, [self.SPIRead(self.REG_MODEM_CONFIG_1)[0] | 0x01]) + + def handleDio0Rise(self): + irqFlags = self.SPIRead(self.REG_IRQ_FLAGS, 1)[0] + self.SPIWrite(self.REG_IRQ_FLAGS, [irqFlags]) + + if (irqFlags & self.IRQ_PAYLOAD_CRC_ERROR_MASK) == 0: + self._packetIndex = 0 + if self._implicitHeaderMode: + self.packetLength = self.SPIRead(self.REG_PAYLOAD_LENGTH, 1)[0] + else: + self.packetLength = self.SPIRead(self.REG_RX_NB_BYTES, 1)[0] + + self.SPIWrite(self.REG_FIFO_ADDR_PTR, self.SPIRead(self.REG_FIFO_RX_CURRENT_ADDR, 1)) + if self._onReceive: + print(self.packetLength) + # self._onReceive(self.packetLength) + + self.SPIWrite(self.REG_FIFO_ADDR_PTR, [0]) + + def SPIWrite(self, adr, byteArray): + return self.SPI.xfer('CS1', [0x80 | adr] + byteArray)[1:] + + def SPIRead(self, adr, total_bytes=1): + return self.SPI.xfer('CS1', [adr] + [0] * total_bytes)[1:] + + def getRaw(self): + val = self.SPIRead(0x02, 1) + return val + + +if __name__ == "__main__": + RX = 0; + TX = 1 + mode = RX + from PSL import sciencelab + + I = sciencelab.connect() + lora = SX1276(I.SPI, 434e6, boost=True, power=17, BW=125e3, SF=12, CR=5) # settings for maximum range + lora.crc() + cntr = 0 + while 1: + time.sleep(0.01) + if mode == TX: + lora.beginPacket() + lora.write([cntr]) + # lora.write([ord(a) for a in ":"]+[cntr]) + print(time.ctime(), [ord(a) for a in ":"] + [cntr], hex(lora.SPIRead(lora.REG_OP_MODE)[0])) + lora.endPacket() + cntr += 1 + if cntr == 255: cntr = 0 + elif mode == RX: + packet_size = lora.parsePacket() + if packet_size: + print('data', lora.readAll()) + print('Rssi', lora.packetRssi(), lora.packetSnr()) diff --git a/PSL/SENSORS/TSL2561.py b/pslab/external/TSL2561.py similarity index 99% rename from PSL/SENSORS/TSL2561.py rename to pslab/external/TSL2561.py index f4a878f4..6c9cfb6a 100644 --- a/PSL/SENSORS/TSL2561.py +++ b/pslab/external/TSL2561.py @@ -5,9 +5,11 @@ from __future__ import print_function import time + def connect(route, **args): return TSL2561(route, **args) + class TSL2561: VISIBLE = 2 # channel 0 - channel 1 INFRARED = 1 # channel 1 diff --git a/pslab/external/__init__.py b/pslab/external/__init__.py new file mode 100644 index 00000000..2452070f --- /dev/null +++ b/pslab/external/__init__.py @@ -0,0 +1 @@ +"""Contains modules which require extra hardware other than the PSLab itself.""" \ No newline at end of file diff --git a/pslab/external/bmp180.py b/pslab/external/bmp180.py new file mode 100644 index 00000000..54598c27 --- /dev/null +++ b/pslab/external/bmp180.py @@ -0,0 +1,186 @@ +"""BMP180 Altimeter.""" + +import time +import logging +import struct +from pslab.bus import I2CSlave + +# BMP180 default address +_ADDRESS = 0x77 + +# Operating Modes +_ULTRALOWPOWER = 0 +_STANDARD = 1 +_HIGHRES = 2 +_ULTRAHIGHRES = 3 + +# BMP180 Registers +_CAL_AC1 = 0xAA # R Calibration data (16 bits) +_CAL_AC2 = 0xAC # R Calibration data (16 bits) +_CAL_AC3 = 0xAE # R Calibration data (16 bits) +_CAL_AC4 = 0xB0 # R Calibration data (16 bits) +_CAL_AC5 = 0xB2 # R Calibration data (16 bits) +_CAL_AC6 = 0xB4 # R Calibration data (16 bits) +_CAL_B1 = 0xB6 # R Calibration data (16 bits) +_CAL_B2 = 0xB8 # R Calibration data (16 bits) +_CAL_MB = 0xBA # R Calibration data (16 bits) +_CAL_MC = 0xBC # R Calibration data (16 bits) +_CAL_MD = 0xBE # R Calibration data (16 bits) +_CONTROL = 0xF4 +_TEMPDATA = 0xF6 +_PRESSUREDATA = 0xF6 + +# Commands +_READTEMPCMD = 0x2E +_READPRESSURECMD = 0x34 + +_logger = logging.getLogger(__name__) + + +class BMP180(I2CSlave): + """Class to interface with the BMP180 Altimeter. + + Parameters + ---------- + mode : int, optional + The mode of operation for the sensor, determining the oversampling setting. + The default mode is `_HIGHRES`. This parameter affects the precision and speed + of the temperature and pressure measurements. + + **kwargs : dict, optional + Additional keyword arguments, such as: + - address (int): The I2C address of the BMP180 sensor. Default is `_ADDRESS`. + + Attributes + ---------- + temperature : float + The measured temperature in degrees Celsius. + + pressure : float + The measured pressure in Pa (Pascals). + + altitude : float + The calculated altitude in meters based on the current pressure reading + and a reference sea level pressure. + """ + + NUMPLOTS = 3 + PLOTNAMES = ["Temperature", "Pressure", "Altitude"] + name = "BMP180 Altimeter" + + def __init__(self, mode=_HIGHRES, **kwargs): + self._ADDRESS = kwargs.get("address", _ADDRESS) + super().__init__(self._ADDRESS) + self._mode = mode + + # Load calibration values + self._ac1 = self._read_int16(_CAL_AC1) + self._ac2 = self._read_int16(_CAL_AC2) + self._ac3 = self._read_int16(_CAL_AC3) + self._ac4 = self._read_uint16(_CAL_AC4) + self._ac5 = self._read_uint16(_CAL_AC5) + self._ac6 = self._read_uint16(_CAL_AC6) + self._b1 = self._read_int16(_CAL_B1) + self._b2 = self._read_int16(_CAL_B2) + self._mb = self._read_int16(_CAL_MB) + self._mc = self._read_int16(_CAL_MC) + self._md = self._read_int16(_CAL_MD) + + _logger.debug(f"ac1: {self._ac1}") + _logger.debug(f"ac2: {self._ac2}") + _logger.debug(f"ac3: {self._ac3}") + _logger.debug(f"ac4: {self._ac4}") + _logger.debug(f"ac5: {self._ac5}") + _logger.debug(f"ac6: {self._ac6}") + _logger.debug(f"b1: {self._b1}") + _logger.debug(f"b2: {self._b2}") + _logger.debug(f"mb: {self._mb}") + _logger.debug(f"mc: {self._mc}") + _logger.debug(f"md: {self._md}") + + def _read_int16(self, addr): + BE_INT16 = struct.Struct(">h") # signed short, big endian + return BE_INT16.unpack(self.read(2, addr))[0] + + def _read_uint16(self, addr): + BE_UINT16 = struct.Struct(">H") # unsigned short, big endian + return BE_UINT16.unpack(self.read(2, addr))[0] + + def _read_raw_temperature(self): + """Read the raw temperature from the sensor.""" + self.write([_READTEMPCMD], _CONTROL) + time.sleep(0.005) + raw = self._read_uint16(_TEMPDATA) + return raw + + @property + def temperature(self): + """Get the actual temperature in degrees celsius.""" + ut = self._read_raw_temperature() + # Calculations from section 3.5 of the datasheet + x1 = ((ut - self._ac6) * self._ac5) >> 15 + x2 = (self._mc << 11) // (x1 + self._md) + b5 = x1 + x2 + temp = ((b5 + 8) >> 4) / 10.0 + return temp + + @property + def oversampling(self): + """oversampling : int + The oversampling setting used by the sensor. This attribute is settable and + determines the trade-off between measurement accuracy and speed. Possible values + include `_ULTRALOWPOWER`, `_STANDARD`, `_HIGHRES`, and `_ULTRAHIGHRES`. + """ + return self._mode + + @oversampling.setter + def oversampling(self, value): + self._mode = value + + def _read_raw_pressure(self): + """Read the raw pressure level from the sensor.""" + delays = [0.005, 0.008, 0.014, 0.026] + self.write([_READPRESSURECMD + (self._mode << 6)], _CONTROL) + time.sleep(delays[self._mode]) + msb = self.read_byte(_PRESSUREDATA) & 0xFF + lsb = self.read_byte(_PRESSUREDATA + 1) & 0xFF + xlsb = self.read_byte(_PRESSUREDATA + 2) & 0xFF + raw = ((msb << 16) + (lsb << 8) + xlsb) >> (8 - self._mode) + return raw + + @property + def pressure(self): + """Get the actual pressure in Pascals.""" + ut = self._read_raw_temperature() + up = self._read_raw_pressure() + # Calculations from section 3.5 of the datasheet + x1 = ((ut - self._ac6) * self._ac5) >> 15 + x2 = (self._mc << 11) // (x1 + self._md) + b5 = x1 + x2 + # Pressure Calculations + b6 = b5 - 4000 + x1 = (self._b2 * (b6 * b6) >> 12) >> 11 + x2 = (self._ac2 * b6) >> 11 + x3 = x1 + x2 + b3 = (((self._ac1 * 4 + x3) << self._mode) + 2) // 4 + x1 = (self._ac3 * b6) >> 13 + x2 = (self._b1 * ((b6 * b6) >> 12)) >> 16 + x3 = ((x1 + x2) + 2) >> 2 + b4 = (self._ac4 * (x3 + 32768)) >> 15 + b7 = (up - b3) * (50000 >> self._mode) + if b7 < 0x80000000: + p = (b7 * 2) // b4 + else: + p = (b7 // b4) * 2 + x1 = (p >> 8) * (p >> 8) + x1 = (x1 * 3038) >> 16 + x2 = (-7357 * p) >> 16 + pres = p + ((x1 + x2 + 3791) >> 4) + return pres + + @property + def altitude(self): + # Calculation from section 3.6 of datasheet + pressure = float(self.pressure) + alt = 44330.0 * (1.0 - pow(pressure / 101325.0, (1.0 / 5.255))) + return alt diff --git a/pslab/external/ccs811.py b/pslab/external/ccs811.py new file mode 100644 index 00000000..b7cedfe5 --- /dev/null +++ b/pslab/external/ccs811.py @@ -0,0 +1,164 @@ +import time +from pslab.bus import I2CSlave + + +class CCS811(I2CSlave): + MODE_IDLE = 0 # Idle (Measurements are disabled in this mode) + MODE_CONTINUOUS = 1 # Constant power mode, IAQ measurement every 1s + MODE_PULSE = 2 # Pulse heating mode IAQ measurement every 10 seconds + MODE_LOW_POWER = 3 # Low power pulse heating mode IAQ measurement every 60 seconds + MODE_CONTINUOUS_FAST = 4 # Constant power mode, sensor measurement every 250ms + + _ADDRESS = 0x5A + + # Figure 14: CCS811 Application Register Map + _STATUS = 0x00 # STATUS # R 1 byte Status register + # MEAS_MODE # R/W 1 byte Measurement mode and conditions register Algorithm result. + # The most significant 2 bytes contain a up to ppm estimate of the equivalent + # CO2 (eCO2) level, and + _MEAS_MODE = 0x01 + # ALG_RESULT_DATA # R 8 bytes the next two bytes contain appb estimate of the total + # VOC level. Raw ADC data values for resistance and current source. + _ALG_RESULT_DATA = 0x02 + # RAW_DATA # R 2 bytes used. Temperature and humidity data can be written to + _RAW_DATA = 0x03 + # ENV_DATA # W 4 bytes enable compensation Thresholds for operation when interrupts + # are only + _ENV_DATA = 0x05 + # THRESHOLDS # W 4 bytes generated when eCO2 ppm crosses a threshold The encoded + # current baseline value can be read. A + _THRESHOLDS = 0x10 + # BASELINE # R/W 2 bytes previously saved encoded baseline can be written. + _BASELINE = 0x11 + _HW_ID = 0x20 # HW_ID # R 1 byte Hardware ID. The value is 0x81 + # HW Version # R 1 byte Hardware Version. The value is 0x1X FirmwareBoot Version. + # The first 2 bytes contain the + _HW = 0x21 + # FW_Boot_Version # R 2 bytes firmware version number for the boot code. Firmware + # Application Version. The first 2 bytes contain + _FW_BOOT_VERSION = 0x23 + # FW_App_Version # R 2 bytes the firmware version number for the application code + _FW_APP_VERSION = 0x24 + # Internal_State # R 1 byte Internal Status register Error ID. When the status + # register reports an error its + _INTERNAL_STATE = 0xA0 + # ERROR_ID # R 1 byte source is located in this register If the correct 4 bytes + # ( 0x11 0xE5 0x72 0x8A) are written + _ERROR_ID = 0xE0 + # SW_RESET # W 4 bytes to this register in a single sequence the device will reset + # and return to BOOT mode. + _SW_RESET = 0xFF + + # Figure 25: CCS811 Bootloader Register Map + # Address Register R/W Size Description + _STATUS = 0x00 + _HW_ID = 0x20 + _HW_Version = 0x21 + _APP_ERASE = 0xF1 + _APP_DATA = 0xF2 + _APP_VERIFY = 0xF3 + _APP_START = 0xF4 + _SW_RESET = 0xFF + + def __init__(self, address=None, device=None): + super().__init__(address or self._ADDRESS, device) + self.fetchID() + self.softwareReset() + + def softwareReset(self): + self.write([0x11, 0xE5, 0x72, 0x8A], self._SW_RESET) + + def fetchID(self): + hardware_id = (self.read(1, self._HW_ID))[0] + time.sleep(0.02) # 20ms + hardware_version = (self.read(1, self._HW_Version))[0] + time.sleep(0.02) # 20ms + boot_version = (self.read(2, self._FW_BOOT_VERSION))[0] + time.sleep(0.02) # 20ms + app_version = (self.read(2, self._FW_APP_VERSION))[0] + + return { + "hardware_id": hardware_id, + "hardware_version": hardware_version, + "boot_version": boot_version, + "app_version": app_version, + } + + def app_erase(self): + self.write([0xE7, 0xA7, 0xE6, 0x09], self._APP_ERASE) + time.sleep(0.3) + + def app_start(self): + self.write([], self._APP_START) + + def set_measure_mode(self, mode): + self.write([mode << 4], self._MEAS_MODE) + + def get_measure_mode(self): + print(self.read(10, self._MEAS_MODE)) + + def get_status(self): + status = (self.read(1, self._STATUS))[0] + return status + + def decode_status(self, status): + s = "" + if (status & (1 << 7)) > 0: + s += "Sensor is in application mode" + else: + s += "Sensor is in boot mode" + if (status & (1 << 6)) > 0: + s += ", APP_ERASE" + if (status & (1 << 5)) > 0: + s += ", APP_VERIFY" + if (status & (1 << 4)) > 0: + s += ", APP_VALID" + if (status & (1 << 3)) > 0: + s += ", DATA_READY" + if (status & 1) > 0: + s += ", ERROR" + return s + + def decode_error(self, error_id): + s = "" + if (error_id & (1 << 0)) > 0: + s += ( + ", The CCS811 received an I²C write request addressed to this station " + "but with invalid register address ID" + ) + if (error_id & (1 << 1)) > 0: + s += ( + ", The CCS811 received an I²C read request to a mailbox ID that is " + "invalid" + ) + if (error_id & (1 << 2)) > 0: + s += ( + ", The CCS811 received an I²C request to write an unsupported mode to " + "MEAS_MODE" + ) + if (error_id & (1 << 3)) > 0: + s += ( + ", The sensor resistance measurement has reached or exceeded the " + "maximum range" + ) + if (error_id & (1 << 4)) > 0: + s += ", The Heater current in the CCS811 is not in range" + if (error_id & (1 << 5)) > 0: + s += ", The Heater voltage is not being applied correctly" + return "Error: " + s[2:] + + def measure(self): + data = self.read(8, self._ALG_RESULT_DATA) + eCO2 = data[0] * 256 + data[1] + eTVOC = data[2] * 256 + data[3] + status = data[4] + error_id = data[5] + # raw_data = 256 * data[6] + data[7] + # raw_current = raw_data >> 10 + # raw_voltage = (raw_data & ((1 << 10) - 1)) * (1.65 / 1023) + + result = {"eCO2": eCO2, "eTVOC": eTVOC, "status": status, "error_id": error_id} + + if error_id > 0: + raise RuntimeError(self.decodeError(error_id)) + return result diff --git a/pslab/external/display.py b/pslab/external/display.py new file mode 100644 index 00000000..05483e2a --- /dev/null +++ b/pslab/external/display.py @@ -0,0 +1,468 @@ +"""Control display devices such as OLED screens. + +Example +------- +>>> from pslab.bus import I2CMaster +>>> from pslab.external.display import SSD1306 +>>> i2c = I2CMaster() # Initialize bus +>>> i2c.configure(1e6) # Set bus speed to 1 MHz. +>>> oled = SSD1306("fast") +>>> oled.clear() +>>> oled.write_string("Hello world!") +>>> oled.scroll("topright") +>>> time.sleep(2.8) +>>> oled.scroll("stop") +""" +import json +import os.path + +try: + from PIL import Image + + HASPIL = True +except ImportError: + HASPIL = False + +from pslab.bus import I2CSlave + +__location__ = os.path.realpath(os.path.join(os.getcwd(), os.path.dirname(__file__))) + + +class SSD1306(I2CSlave): + """Interface to a monochrome OLED display driven by an SSD1306 chip. + + Parameters + ---------- + speed : {'slow' ,'medium', 'fast'}, optional + Controls how many bytes of data are written to the display at once. + More bytes written at once means faster draw rate, but requires that + the baudrate of the underlying I2C bus is high enough to avoid timeout. + The default value is 'slow'. + sh1106 : bool, optional + Set this to True if the OLED is driven by a SH1106 chip rather than a + SSD1306. + """ + + _COPYRIGHT = """ + Copyright (c) 2021, FOSSASIA + Copyright (c) 2012, Adafruit Industries + All rights reserved. + + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions are met: + 1. Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + 2. Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. + 3. Neither the name of the copyright holders nor the + names of its contributors may be used to endorse or promote products + derived from this software without specific prior written permission. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS ''AS IS'' AND ANY + EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER BE LIABLE FOR ANY + DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND + ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + """ + _ADDRESS = 0x3C + _WIDTH = 128 + _HEIGHT = 64 + _LOWCOLUMNOFFSET = 0 + + _SETCONTRAST = 0x81 + _DISPLAYALLON_RESUME = 0xA4 + _DISPLAYALLON = 0xA5 + _NORMALDISPLAY = 0xA6 + _INVERTDISPLAY = 0xA7 + _DISPLAYOFF = 0xAE + _DISPLAYON = 0xAF + _SETDISPLAYOFFSET = 0xD3 + _SETCOMPINS = 0xDA + _SETVCOMDETECT = 0xDB + _SETDISPLAYCLOCKDIV = 0xD5 + _SETPRECHARGE = 0xD9 + _SETMULTIPLEX = 0xA8 + _SETLOWCOLUMN = 0x00 + _SETHIGHCOLUMN = 0x10 + _SETSTARTLINE = 0x40 + _MEMORYMODE = 0x20 + _SETPAGESTART = 0xB0 + _COMSCANINC = 0xC0 + _COMSCANDEC = 0xC8 + _SEGREMAP = 0xA0 + _CHARGEPUMP = 0x8D + _EXTERNALVCC = 0x1 + _SWITCHCAPVCC = 0x2 + + # fmt: off + _INIT_DATA = [ + _DISPLAYOFF, + _SETDISPLAYCLOCKDIV, 0x80, # Default aspect ratio. + _SETMULTIPLEX, 0x3F, + _SETDISPLAYOFFSET, 0x0, # No offset. + _SETSTARTLINE | 0x0, # Line #0. + _CHARGEPUMP, 0x14, + _MEMORYMODE, 0x02, # Page addressing + _SEGREMAP | 0x1, + _COMSCANDEC, + _SETCOMPINS, 0x12, + _SETCONTRAST, 0xFF, + _SETPRECHARGE, 0xF1, + _SETVCOMDETECT, 0x40, + _DISPLAYALLON_RESUME, + _NORMALDISPLAY, + _DISPLAYON, + ] + # fmt: on + + def __init__(self, device=None, speed="slow"): + super().__init__(device=device, address=self._ADDRESS) + self._buffer = [0 for a in range(1024)] + self.cursor = [0, 0] + self.textsize = 1 + self.textcolor = 1 + self.textbgcolor = 0 + self.wrap = True + self._contrast = 0xFF + self.speed = speed + + with open(os.path.join(__location__, "ssd1306_gfx.json"), "r") as f: + gfx = json.load(f) + + self._logo = gfx["logo"] + self._font = gfx["font"] + + for command in self._INIT_DATA: + self._write_command(command) + + self.display_logo() + + def display_logo(self): + """Display pslab.io logo.""" + self.scroll("stop") + self._buffer = self._logo[:] + self.update() + + def _write_command(self, command: int): + self.write_byte(command) + + def _write_data(self, data: list): + self.write(data, register_address=0x40) + + def clear(self): + """Clear the display.""" + self.cursor = [0, 0] + self._buffer = [0] * (self._WIDTH * self._HEIGHT // 8) + self.update() + + def update(self): + """Redraw display.""" + for i in range(8): + self._write_command(self._SETLOWCOLUMN | self._LOWCOLUMNOFFSET) + self._write_command(self._SETHIGHCOLUMN | 0) + self._write_command(self._SETPAGESTART | i) + + if self.speed == "slow": + for j in range(self._WIDTH): + self._write_data([self._buffer[i * self._WIDTH + j]]) + elif self.speed == "medium": + for j in range(self._WIDTH // 8): + self._write_data( + self._buffer[ + i * self._WIDTH + j * 8 : i * self._WIDTH + 8 * (j + 1) + ] + ) + else: + self._write_data(self._buffer[self._WIDTH * i : self._WIDTH * (i + 1)]) + + @property + def contrast(self) -> int: + """int: Screen contrast.""" + return self._contrast + + @contrast.setter + def contrast(self, value: int): + self._write_command(self._SETCONTRAST) + self._write_command(value) + self._contrast = value + + def _draw(self, update: bool = True): + if update: + self.update() + + def draw_pixel(self, x: int, y: int, color: int, update: bool = True): + """Draw a single pixel.""" + if color == 1: + self._buffer[x + (y // 8) * self._WIDTH] |= 1 << (y % 8) + else: + self._buffer[x + (y // 8) * self._WIDTH] &= ~(1 << (y % 8)) + self._draw(update) + + def draw_circle(self, x0, y0, r, color, update: bool = True): + """Draw a circle.""" + f = 1 - r + ddF_x = 1 + ddF_y = -2 * r + x = 0 + y = r + self.draw_pixel(x0, y0 + r, color, False) + self.draw_pixel(x0, y0 - r, color, False) + self.draw_pixel(x0 + r, y0, color, False) + self.draw_pixel(x0 - r, y0, color, False) + + while x < y: + if f >= 0: + y -= 1 + ddF_y += 2 + f += ddF_y + + x += 1 + ddF_x += 2 + f += ddF_x + self.draw_pixel(x0 + x, y0 + y, color, False) + self.draw_pixel(x0 - x, y0 + y, color, False) + self.draw_pixel(x0 + x, y0 - y, color, False) + self.draw_pixel(x0 - x, y0 - y, color, False) + self.draw_pixel(x0 + y, y0 + x, color, False) + self.draw_pixel(x0 - y, y0 + x, color, False) + self.draw_pixel(x0 + y, y0 - x, color, False) + self.draw_pixel(x0 - y, y0 - x, color, False) + + self._draw(update) + + def draw_line(self, x0, y0, x1, y1, color, update: bool = True): + """Draw a line.""" + is_steep = abs(y1 - y0) > abs(x1 - x0) + + if is_steep: + tmp = y0 + y0 = x0 + x0 = tmp + tmp = y1 + y1 = x1 + x1 = tmp + + if x0 > x1: + tmp = x1 + x1 = x0 + x0 = tmp + tmp = y1 + y1 = y0 + y0 = tmp + + dx = x1 - x0 + dy = abs(y1 - y0) + err = dx / 2 + + if y0 < y1: + ystep = 1 + else: + ystep = -1 + + while x0 <= x1: + if is_steep: + self.draw_pixel(y0, x0, color, False) + else: + self.draw_pixel(x0, y0, color, False) + err -= dy + + if err < 0: + y0 += ystep + err += dx + x0 += 1 + + self._draw(update) + + def draw_rectangle(self, x0, y0, width, height, color, update: bool = True): + """Draw a rectangle.""" + self.draw_horizontal_line(x0, y0, width, color, False) + self.draw_horizontal_line(x0, y0 + height - 1, width, color, False) + self.draw_vertical_line(x0, y0, height, color, False) + self.draw_vertical_line(x0 + width - 1, y0, height, color, False) + self._draw(update) + + def draw_vertical_line(self, x0, y0, length, color, update: bool = True): + """Draw a vertical line.""" + self.draw_line(x0, y0, x0, y0 + length - 1, color, update) + + def draw_horizontal_line(self, x0, y0, length, color, update: bool = True): + """Draw a horizontal line.""" + self.draw_line(x0, y0, x0 + length - 1, y0, color, update) + + def fill_rectangle(self, x0, y0, width, height, color, update: bool = True): + """Draw a filled rectangle.""" + for i in range(x0, x0 + width): + self.draw_vertical_line(i, y0, height, color, False) + self._draw(update) + + def write_string(self, s: str, update: bool = True): + """Write a string on screen.""" + for a in s: + self._write_char(ord(a), False) + self._draw(update) + + def _write_char(self, c: int, update: bool = True): + """Write a single character on screen.""" + if c == "\n": + self.cursor[1] += self.textsize * 8 + self.cursor[0] = 0 + elif c == "\r": + pass + else: + self._draw_char( + self.cursor[0], + self.cursor[1], + c, + self.textcolor, + self.textbgcolor, + self.textsize, + ) + self.cursor[0] += self.textsize * 6 + + if self.wrap and (self.cursor[0] > (self._WIDTH - self.textsize * 6)): + self.cursor[1] += self.textsize * 8 + self.cursor[0] = 0 + + self._draw(update) + + def _draw_char(self, x0, y0, c, color, bg, size): + if ( + (x0 >= self._WIDTH) + or (y0 >= self._HEIGHT) + or ((x0 + 5 * size - 1) < 0) + or ((y0 + 8 * size - 1) < 0) + ): + return + for i in range(6): + if i == 5: + line = 0x0 + else: + line = self._font[c * 5 + i] + for j in range(8): + if line & 0x1: + if size == 1: + self.draw_pixel(x0 + i, y0 + j, color, False) + else: + self.fill_rectangle( + x0 + (i * size), y0 + (j * size), size, size, color, False + ) + elif bg != color: + if size == 1: + self.draw_pixel(x0 + i, y0 + j, bg, False) + else: + self.fill_rectangle( + x0 + i * size, y0 + j * size, size, size, bg, False + ) + line >>= 1 + + def scroll(self, direction: str): + """Scroll the screen contents. + + Parameters + ---------- + direction : {'left', 'right', 'stop'} + Scrolling direction. + """ + if direction == "left": + self._write_command(0x27) # up-0x29 ,2A left-0x27 right0x26 + if direction == "right": + self._write_command(0x26) # up-0x29 ,2A left-0x27 right0x26 + if direction in ["topright", "bottomright"]: + self._write_command(0x29) # up-0x29 ,2A left-0x27 right0x26 + if direction in ["topleft", "bottomleft"]: + self._write_command(0x2A) # up-0x29 ,2A left-0x27 right0x26 + + if direction in [ + "left", + "right", + "topright", + "topleft", + "bottomleft", + "bottomright", + ]: + self._write_command(0x0) # dummy + self._write_command(0x0) # start page + self._write_command(0x7) # time interval 0b100 - 3 frames + self._write_command(0xF) # end page + if direction in ["topleft", "topright"]: + self._write_command(0x02) + elif direction in ["bottomleft", "bottomright"]: + self._write_command(0xFE) + + if direction in ["left", "right"]: + self._write_command(0x02) + self._write_command(0xFF) + + self._write_command(0x2F) + + if direction == "stop": + self._write_command(0x2E) + + def poweron(self): + """Turn the display on.""" + self._write_command(self._DISPLAYON) + + def poweroff(self): + """Turn the display off.""" + self._write_command(self._DISPLAYOFF) + + def display(self, image: Image): + """Display an image. + + Parameters + ---------- + image : Image + A PIL.Image instance. + """ + if not HASPIL: + raise ImportError( + "Displaying images requires PIL, but it is not installed." + ) + + if not image.size == (128, 64): + image = image.resize((128, 64)) + + if not image.mode == "1": + image = image.convert("1") + + image_data = image.getdata() + pixels_per_page = self._WIDTH * 8 + buf = bytearray(self._WIDTH) + buffer = [] + + for y in range(0, int(8 * pixels_per_page), pixels_per_page): + offsets = [y + self._WIDTH * i for i in range(8)] + + for x in range(self._WIDTH): + buf[x] = ( + (image_data[x + offsets[0]] and 0x01) + | (image_data[x + offsets[1]] and 0x02) + | (image_data[x + offsets[2]] and 0x04) + | (image_data[x + offsets[3]] and 0x08) + | (image_data[x + offsets[4]] and 0x10) + | (image_data[x + offsets[5]] and 0x20) + | (image_data[x + offsets[6]] and 0x40) + | (image_data[x + offsets[7]] and 0x80) + ) + + buffer += list(buf) + + self._buffer = buffer + self.update() + + +class SH1106(SSD1306): + """Interface to a monochrome OLED display driven by an SH1106 chip. + + SH1106 is a common OLED driver which is almost identical to the SSD1306. + OLED displays are sometimes advertised as using SSD1306 when they in fact + use SH1106. + """ + + _LOWCOLUMNOFFSET = 2 diff --git a/pslab/external/gas_sensor.py b/pslab/external/gas_sensor.py new file mode 100644 index 00000000..9e3f6867 --- /dev/null +++ b/pslab/external/gas_sensor.py @@ -0,0 +1,158 @@ +"""Gas sensors can be used to measure the concentration of certain gases.""" + +from typing import Callable, Union + +from pslab import Multimeter +from pslab.serial_handler import SerialHandler + + +class MQ135: + """MQ135 is a cheap gas sensor that can detect several harmful gases. + + The MQ135 is most suitable for detecting flammable gases, but can also be + used to measure CO2. + + Parameters + ---------- + gas : {CO2, CO, EtOH, NH3, Tol, Ace} + The gas to be detected: + CO2: Carbon dioxide + CO: Carbon monoxide + EtOH: Ethanol + NH3: Ammonia + Tol: Toluene + Ace: Acetone + r_load : float + Load resistance in ohm. + device : :class:`SerialHandler`, optional + Serial connection to PSLab device. If not provided, a new one will be + created. + channel : str, optional + Analog input on which to monitor the sensor output voltage. The default + value is CH1. Be aware that the sensor output voltage can be as high + as 5 V, depending on load resistance and gas concentration. + r0 : float, optional + The sensor resistance when exposed to 100 ppm NH3 at 20 degC and 65% + RH. Varies between individual sensors. Optional, but gas concentration + cannot be measured unless R0 is known. If R0 is not known, + :meth:`measure_r0` to find it. + temperature : float or Callable, optional + Ambient temperature in degC. The default value is 20. A callback can + be provided in place of a fixed value. + humidity : float or Callable, optional + Relative humidity between 0 and 1. The default value is 0.65. A + callback can be provided in place of a fixed value. + """ + + # Parameters manually extracted from data sheet. + # ppm = A * (Rs/R0) ^ B + _PARAMS = { + "CO2": [109, -2.88], + "CO": [583, -3.93], + "EtOH": [76.4, -3.18], + "NH3": [102, -2.49], + "Tol": [44.6, -3.45], + "Ace": [33.9, -3.42], + } + + # Assuming second degree temperature dependence and linear humidity dependence. + _TEMPERATURE_CORRECTION = [3.28e-4, -2.55e-2, 1.38] + _HUMIDITY_CORRECTION = -2.24e-1 + + def __init__( + self, + gas: str, + r_load: float, + device: SerialHandler = None, + channel: str = "CH1", + r0: float = None, + temperature: Union[float, Callable] = 20, + humidity: Union[float, Callable] = 0.65, + ): + self._multimeter = Multimeter(device) + self._params = self._PARAMS[gas] + self.channel = channel + self.r_load = r_load + self.r0 = r0 + self.vcc = 5 + + if isinstance(temperature, Callable): + self._temperature = temperature + else: + + def _temperature(): + return temperature + + self._temperature = _temperature + + if isinstance(humidity, Callable): + self._humidity = humidity + else: + + def _humidity(): + return humidity + + self._humidity = _humidity + + @property + def _voltage(self): + return self._multimeter.measure_voltage(self.channel) + + @property + def _correction(self): + """Correct sensor resistance for temperature and humidity. + + Coefficients are averages of curves fitted to temperature data for 33% + and 85% relative humidity extracted manually from the data sheet. + Humidity dependence is assumed to be linear, and is centered on 65% RH. + """ + t = self._temperature() + h = self._humidity() + a, b, c, d = *self._TEMPERATURE_CORRECTION, self._HUMIDITY_CORRECTION + return a * t**2 + b * t + c + d * (h - 0.65) + + @property + def _sensor_resistance(self): + return (self.vcc / self._voltage - 1) * self.r_load / self._correction + + def measure_concentration(self): + """Measure the concentration of the configured gas. + + Returns + ------- + concentration : float + Gas concentration in ppm. + """ + try: + return ( + self._params[0] * (self._sensor_resistance / self.r0) ** self._params[1] + ) + except TypeError: + raise TypeError("r0 is not set.") + + def measure_r0(self, gas_concentration: float): + """Determine sensor resistance at 100 ppm NH3 in otherwise clean air. + + For best results, monitor R0 over several hours and use the average + value. + + The sensor resistance at 100 ppm NH3 (R0) is used as a reference + against which the present sensor resistance must be compared in order + to calculate gas concentration. + + R0 can be determined by calibrating the sensor at any known gas + concentration. + + Parameters + ---------- + gas_concentration : float + A known concentration of the configured gas in ppm. + + Returns + ------- + r0 : float + The sensor resistance at 100 ppm NH3 in ohm. + """ + return self._sensor_resistance * (gas_concentration / self._params[0]) ** ( + 1 / -self._params[1] + ) diff --git a/pslab/external/hcsr04.py b/pslab/external/hcsr04.py new file mode 100644 index 00000000..602173df --- /dev/null +++ b/pslab/external/hcsr04.py @@ -0,0 +1,119 @@ +"""Ultrasonic distance sensors.""" + +import time + +from pslab.instrument.logic_analyzer import LogicAnalyzer +from pslab.instrument.waveform_generator import PWMGenerator +from pslab.serial_handler import SerialHandler + + +class HCSR04: + """Read data from ultrasonic distance sensor HC-SR04/HC-SR05. + + These sensors can measure distances between 2 cm to 4 m (SR04) / 4.5 m + (SR05). + + Sensors must have separate trigger and echo pins. First a 10 µs pulse is + output on the trigger pin. The trigger pin must be connected to the TRIG + pin on the sensor prior to use. + + Upon receiving this pulse, the sensor emits a sequence of sound pulses, and + the logic level of its echo pin is also set high. The logic level goes LOW + when the sound packet returns to the sensor, or when a timeout occurs. + Timeout occurs if no echo is received within the time slot determinded by + the sensor's maximum range. + + The ultrasound sensor outputs a series of eight sound pulses at 40 kHz, + which corresponds to a time period of 25 µs per pulse. These pulses reflect + off of the nearest object in front of the sensor, and return to it. The + time between sending and receiving of the pulse packet is used to estimate + the distance. If the reflecting object is either too far away or absorbs + sound, less than eight pulses may be received, and this can cause a + measurement error of 25 µs which corresponds to 8 mm. + + Parameters + ---------- + device : :class:`SerialHandler` + Serial connection to PSLab device. + trig : str, optional + Name of the trigger pin. Defaults to SQ1. + echo : str, optional + Name of the echo pin. Defaults to LA1. + + Example + ------- + In this example the sensor's Vcc pin is connected to the PSLab's PV1 pin, + the Trig pin is connected to SQ1, Echo to LA1, and Gnd to GND. + + >>> import pslab + >>> from pslab.external.hcsr04 import HCSR04 + >>> psl = pslab.ScienceLab() + >>> distance_sensor = HCSR04(psl) + >>> psl.power_supply.pv1 = 5 # Power the sensor. + >>> distance_sensor.estimate_distance() + 2.36666667 + """ + + def __init__( + self, + device: SerialHandler, + trig: str = "SQ1", + echo: str = "LA1", + ): + self._device = device + self._la = LogicAnalyzer(self._device) + self._pwm = PWMGenerator(self._device) + self._trig = trig + self._echo = echo + self._measure_period = 60e-3 # Minimum recommended by datasheet. + self._trigger_pulse_length = 10e-6 + + def estimate_distance( + self, + average: int = 10, + speed_of_sound: float = 340, + ) -> float: + """Estimate distance to an object. + + Parameters + ---------- + average : int, optional + Number of times to repeat the measurement and average the results. + Defaults to 10. + speed_of_sound : float, optional + Speed of sound in air. Defaults to 340 m/s. + + Returns + ------- + distance : float + Distance to object in meters. + + Raises + ------ + RuntimeError if the ECHO pin is not LOW at start of measurement. + TimeoutError if the end of the ECHO pulse is not detected (i.e. the + object is too far away). + """ + self._la.capture( + channels=self._echo, + events=2 * average, + block=False, + ) + self._pwm.generate( + channels=self._trig, + frequency=self._measure_period**-1, + duty_cycles=self._trigger_pulse_length / self._measure_period, + ) + # Wait one extra period to make sure we don't miss the final edge. + time.sleep(self._measure_period * (average + 1)) + self._pwm.set_state(**{self._trig.lower(): 0}) + (t,) = self._la.fetch_data() + self._sanity_check(len(t), 2 * average) + high_times = t[1::2] - t[::2] + return speed_of_sound * high_times.mean() / 2 * 1e-6 + + def _sanity_check(self, events: int, expected_events: int): + if self._la.get_initial_states()[self._echo]: + raise RuntimeError("ECHO pin was HIGH when measurement started.") + if events < expected_events: + raise TimeoutError diff --git a/pslab/external/motor.py b/pslab/external/motor.py new file mode 100644 index 00000000..1fba065b --- /dev/null +++ b/pslab/external/motor.py @@ -0,0 +1,169 @@ +"""Motor control related classes. + +Examples +-------- +>>> from pslab.external.motor import Servo +>>> servo = Servo("SQ1") +>>> servo.angle = 30 # Turn motor to 30 degrees position. +""" + +import time +from typing import List +from typing import Union +import csv +import os + +from pslab.instrument.waveform_generator import PWMGenerator +from datetime import datetime + +MICROSECONDS = 1e6 + + +class Servo: + """Control servo motors on SQ1-4. + + Parameters + ---------- + pin : {"SQ1", "SQ2", "SQ3", "SQ4"} + Name of the digital output on which to generate the control signal. + pwm_generator : :class:`PWMGenerator`, optional + PWMGenerator instance with which to generate the control signal. + Created automatically if not specified. When contolling multiple + servos, they should all use the same PWMGenerator instance. + min_angle_pulse : int, optional + Pulse length in microseconds corresponding to the minimum (0 degree) + angle of the servo. The default value is 500. + max_angle_pulse : int, optional + Pulse length in microseconds corresponding to the maximum (180 degree) + angle of the servo. The default value is 2500. + angle_range : int + Range of the servo in degrees. The default value is 180. + frequency : float, optional + Frequency of the control signal in Hz. The default value is 50. + """ + + def __init__( + self, + pin: str, + pwm_generator: PWMGenerator = None, + min_angle_pulse: int = 500, + max_angle_pulse: int = 2500, + angle_range: int = 180, + frequency: float = 50, + ): + self._pwm = PWMGenerator() if pwm_generator is None else pwm_generator + self._pin = pin + self._angle = None + self._min_angle_pulse = min_angle_pulse + self._max_angle_pulse = max_angle_pulse + self._angle_range = angle_range + self._frequency = frequency + + @property + def angle(self) -> Union[int, None]: + """:obj:`int` or :obj:`None`: Angle of the servo in degrees.""" + return self._angle + + @angle.setter + def angle(self, value: int): + duty_cycle = self._get_duty_cycle(value) + self._pwm.generate(self._pin, self._frequency, duty_cycle) + self._angle = value + + def _get_duty_cycle(self, angle): + angle /= self._angle_range # Normalize + angle *= self._max_angle_pulse - self._min_angle_pulse # Scale + angle += self._min_angle_pulse # Offset + return angle / (self._frequency**-1 * MICROSECONDS) + + +class RoboticArm: + """Robotic arm controller for up to 4 servos.""" + + MAX_SERVOS = 4 + + def __init__(self, servos: List[Servo]) -> None: + if len(servos) > RoboticArm.MAX_SERVOS: + raise ValueError( + f"At most {RoboticArm.MAX_SERVOS} servos can be used, got {len(servos)}" + ) + self.servos = servos + + def run_schedule(self, timeline: List[List[int]], time_step: float = 1.0) -> None: + """Run a time-based schedule to move servos. + + Parameters + ---------- + timeline : List[List[int]] + A list of timesteps,where each sublist represents one timestep, + with angles corresponding to each servo. + + time_step : float, optional + Delay in seconds between each timestep. Default is 1.0. + """ + if len(timeline[0]) != len(self.servos): + raise ValueError("Each timestep must specify an angle for every servo") + + tl_len = len(timeline[0]) + if not all(len(tl) == tl_len for tl in timeline): + raise ValueError("All timeline entries must have the same length") + + for tl in timeline: + for i, s in enumerate(self.servos): + if tl[i] is not None: + s.angle = tl[i] + time.sleep(time_step) + + def import_timeline_from_csv(self, filepath: str) -> List[List[int]]: + """Import timeline from a CSV file. + + Parameters + ---------- + filepath : str + Absolute or relative path to the CSV file to be read. + + Returns + ------- + List[List[int]] + A timeline consisting of servo angle values per timestep. + """ + timeline = [] + + with open(filepath, mode="r", newline="") as csvfile: + reader = csv.DictReader(csvfile) + for row in reader: + angles = [] + for key in ["Servo1", "Servo2", "Servo3", "Servo4"]: + value = row[key] + if value == "null": + angles.append(None) + else: + angles.append(int(value)) + timeline.append(angles) + + return timeline + + def export_timeline_to_csv( + self, timeline: List[List[Union[int, None]]], folderpath: str + ) -> None: + """Export timeline to a CSV file. + + Parameters + ---------- + timeline : List[List[Union[int, None]]] + A list of timesteps where each sublist contains servo angles. + + folderpath : str + Directory path where the CSV file will be saved. The filename + will include a timestamp to ensure uniqueness. + """ + timestamp = datetime.now().strftime("%Y-%m-%d_%H-%M-%S") + filename = f"Robotic_Arm{timestamp}.csv" + filepath = os.path.join(folderpath, filename) + + with open(filepath, mode="w", newline="") as csvfile: + writer = csv.writer(csvfile) + writer.writerow(["Timestep", "Servo1", "Servo2", "Servo3", "Servo4"]) + for i, row in enumerate(timeline): + pos = ["null" if val is None else val for val in row] + writer.writerow([i] + pos) diff --git a/pslab/external/sensorlist.py b/pslab/external/sensorlist.py new file mode 100644 index 00000000..420cb7ab --- /dev/null +++ b/pslab/external/sensorlist.py @@ -0,0 +1,83 @@ +# I2C Address list from Adafruit : https://learn.adafruit.com/i2c-addresses/the-list + +sensors = { + 0x00: ['Could be MLX90614. Try 0x5A'], + 0x10: ['VEML6075', 'VEML7700'], + 0x11: ['Si4713'], + 0x13: ['VCNL40x0'], + 0x18: ['MCP9808', 'LIS3DH'], + 0x19: ['MCP9808', 'LIS3DH', 'LSM303 accelerometer & magnetometer'], + 0x1A: ['MCP9808'], + 0x1B: ['MCP9808'], + 0x1C: ['MCP9808', 'MMA845x', 'FXOS8700'], + 0x1D: ['MCP9808', 'MMA845x', 'FXOS8700','ADXL345', 'MMA7455L', 'LSM9DSO'], + 0x1E: ['MCP9808', 'FXOS8700', 'LSM303', 'LSM9DSO', 'HMC5883'], + 0x1F: ['MCP9808', 'FXOS8700'], + 0x20: ['MCP23008', 'MCP23017', 'FXAS21002', 'Chirp! Water Sensor'], + 0x21: ['MCP23008', 'MCP23017', 'FXAS21002'], + 0x22: ['MCP23008', 'MCP23017'], + 0x23: ['MCP23008', 'MCP23017'], + 0x24: ['MCP23008', 'MCP23017'], + 0x25: ['MCP23008', 'MCP23017'], + 0x26: ['MCP23008', 'MCP23017', 'MSA301'], + 0x27: ['MCP23008', 'MCP23017'], + 0x28: ['BNO055', 'CAP1188', 'TSL2591'], + 0x29: ['TSL2561', 'BNO055', 'TCS34725', 'TSL2591', 'VL53L0x', 'VL6180X', 'CAP1188'], + 0x2A: ['CAP1188'], + 0x2B: ['CAP1188'], + 0x2C: ['CAP1188'], + 0x2D: ['CAP1188'], + 0x38: ['VEML6070','FT6206 touch controller'], + 0x39: ['TSL2561', 'VEML6070', 'APDS-9960'], + 0x3c: ['SSD1306 monochrome OLED', 'SSD1305 monochrome OLED'], + 0x3d: ['SSD1306 monochrome OLED', 'SSD1305 monochrome OLED'], + 0x40: ['Si7021', 'HTU21D-F', 'HDC1008','TMP007', 'TMP006', 'PCA9685', 'INA219', 'INA260'], + 0x41: ['HDC1008','TMP007', 'TMP006', 'INA219', 'INA260', 'STMPE610/STMPE811'], + 0x42: ['HDC1008','TMP007', 'TMP006', 'INA219'], + 0x43: ['HDC1008','TMP007', 'TMP006', 'INA219', 'INA260'], + 0x44: ['SHT31', 'TMP007', 'TMP006', 'ISL29125', 'INA219', 'INA260', 'STMPE610/STMPE811'], + 0x45: ['SHT31', 'TMP007', 'TMP006', 'INA219', 'INA260'], + 0x46: ['TMP007', 'TMP006', 'INA219', 'INA260'], + 0x47: ['TMP007', 'TMP006', 'INA219', 'INA260'], + 0x48: ['TMP102', 'PN532 NFS/RFID', 'ADS1115', 'INA219', 'INA260'], + 0x49: ['TSL2561', 'TMP102', 'ADS1115', 'INA219', 'INA260'], + 0x4A: ['TMP102', 'ADS1115', 'INA219', 'INA260'], + 0x4B: ['TMP102', 'ADS1115', 'INA219', 'INA260'], + 0x4C: ['INA219', 'INA260'], + 0x4D: ['INA219', 'INA260'], + 0x4E: ['INA219', 'INA260'], + 0x4F: ['INA219', 'INA260'], + 0x50: ['MB85RC'], + 0x51: ['MB85RC'], + 0x52: ['MB85RC', 'Nintendo Nunchuck controller'], + 0x53: ['MB85RC', 'ADXL345'], + 0x54: ['MB85RC'], + 0x55: ['MB85RC'], + 0x56: ['MB85RC'], + 0x57: ['MB85RC', 'MAX3010x'], + 0x58: ['TPA2016','SGP30'], + 0x5A: ['MPR121', 'CCS811', 'MLX9061x', 'DRV2605'], + 0x5B: ['MPR121', 'CCS811'], + 0x5C: ['AM2315', 'MPR121'], + 0x5D: ['MPR121'], + 0x60: ['MPL115A2','MPL3115A2', 'Si5351A', 'Si1145', 'MCP4725A0', 'TEA5767', 'VCNL4040'], + 0x61: ['Si5351A', 'MCP4725A0'], + 0x62: ['MCP4725A0'], + 0x63: ['Si4713', 'MCP4725A1'], + 0x64: ['MCP4725A2'], + 0x65: ['MCP4725A2'], + 0x66: ['MCP4725A3'], + 0x67: ['MCP4725A3'], + 0x68: ['AMG8833', 'DS1307', 'PCF8523', 'DS3231', 'MPU-9250', 'MPU-60X0', 'ITG3200'], + 0x69: ['AMG8833', 'MPU-9250', 'MPU-60X0', 'ITG3200'], + 0x6A: ['L3GD20H', 'LSM9DS0'], + 0x6B: ['L3GD20H', 'LSM9DS0'], + 0x70: ['TCA9548', 'HT16K33'], + 0x71: ['TCA9548', 'HT16K33'], + 0x72: ['TCA9548', 'HT16K33'], + 0x73: ['TCA9548', 'HT16K33'], + 0x74: ['IS31FL3731', 'TCA9548', 'HT16K33'], + 0x75: ['IS31FL3731', 'TCA9548', 'HT16K33'], + 0x76: ['BME280 Temp/Barometric', 'IS31FL3731', 'TCA9548', 'HT16K33', 'MS5607/MS5611'], + 0x77: ['BME280 Temp/Barometric/Humidity', 'BMP180 Temp/Barometric', 'BMP085 Temp/Barometric', 'BMA180 Accelerometer', 'IS31FL3731', 'TCA9548', 'HT16K33', 'MS5607/MS5611'], +} diff --git a/pslab/external/ssd1306_gfx.json b/pslab/external/ssd1306_gfx.json new file mode 100644 index 00000000..efd75748 --- /dev/null +++ b/pslab/external/ssd1306_gfx.json @@ -0,0 +1 @@ +{"logo": [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 170, 254, 254, 254, 254, 254, 254, 254, 254, 254, 254, 254, 254, 254, 254, 254, 254, 254, 254, 254, 254, 254, 254, 254, 254, 254, 254, 254, 254, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 240, 224, 240, 240, 224, 112, 224, 240, 224, 224, 224, 192, 0, 0, 0, 128, 192, 224, 240, 240, 112, 112, 112, 240, 224, 224, 32, 0, 0, 0, 0, 240, 224, 240, 240, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 240, 240, 248, 208, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 170, 255, 239, 143, 143, 143, 223, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 255, 255, 255, 255, 224, 240, 224, 112, 253, 127, 127, 63, 7, 0, 0, 7, 31, 63, 63, 60, 120, 120, 120, 240, 240, 240, 225, 0, 0, 0, 0, 255, 255, 255, 255, 0, 0, 0, 0, 0, 0, 0, 0, 0, 132, 206, 198, 239, 198, 231, 206, 239, 254, 254, 248, 160, 0, 0, 0, 255, 255, 255, 253, 14, 6, 14, 15, 190, 254, 252, 248, 64, 0, 0, 0, 192, 192, 224, 224, 224, 224, 192, 128, 0, 0, 0, 170, 255, 85, 1, 1, 0, 119, 255, 255, 15, 7, 3, 1, 225, 241, 241, 241, 225, 1, 3, 7, 31, 255, 255, 255, 255, 255, 255, 255, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 63, 63, 63, 63, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 14, 28, 60, 56, 56, 56, 56, 60, 61, 31, 31, 15, 1, 0, 0, 0, 63, 63, 63, 63, 60, 56, 60, 60, 56, 60, 60, 0, 0, 31, 31, 63, 61, 56, 56, 24, 14, 63, 63, 63, 18, 0, 0, 0, 63, 63, 63, 31, 28, 24, 56, 56, 62, 63, 31, 15, 0, 0, 0, 7, 31, 31, 63, 63, 63, 63, 31, 15, 0, 0, 0, 170, 255, 213, 192, 192, 192, 247, 255, 255, 252, 240, 224, 192, 195, 199, 199, 199, 195, 192, 224, 240, 252, 255, 255, 255, 255, 255, 255, 255, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 170, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], "font": [0, 0, 0, 0, 0, 62, 91, 79, 91, 62, 62, 107, 79, 107, 62, 28, 62, 124, 62, 28, 24, 60, 126, 60, 24, 28, 87, 125, 87, 28, 28, 94, 127, 94, 28, 0, 24, 60, 24, 0, 255, 231, 195, 231, 255, 0, 24, 36, 24, 0, 255, 231, 219, 231, 255, 48, 72, 58, 6, 14, 38, 41, 121, 41, 38, 64, 127, 5, 5, 7, 64, 127, 5, 37, 63, 90, 60, 231, 60, 90, 127, 62, 28, 28, 8, 8, 28, 28, 62, 127, 20, 34, 127, 34, 20, 95, 95, 0, 95, 95, 6, 9, 127, 1, 127, 0, 102, 137, 149, 106, 96, 96, 96, 96, 96, 148, 162, 255, 162, 148, 8, 4, 126, 4, 8, 16, 32, 126, 32, 16, 8, 8, 42, 28, 8, 8, 28, 42, 8, 8, 30, 16, 16, 16, 16, 12, 30, 12, 30, 12, 48, 56, 62, 56, 48, 6, 14, 62, 14, 6, 0, 0, 0, 0, 0, 0, 0, 95, 0, 0, 0, 7, 0, 7, 0, 20, 127, 20, 127, 20, 36, 42, 127, 42, 18, 35, 19, 8, 100, 98, 54, 73, 86, 32, 80, 0, 8, 7, 3, 0, 0, 28, 34, 65, 0, 0, 65, 34, 28, 0, 42, 28, 127, 28, 42, 8, 8, 62, 8, 8, 0, 128, 112, 48, 0, 8, 8, 8, 8, 8, 0, 0, 96, 96, 0, 32, 16, 8, 4, 2, 62, 81, 73, 69, 62, 0, 66, 127, 64, 0, 114, 73, 73, 73, 70, 33, 65, 73, 77, 51, 24, 20, 18, 127, 16, 39, 69, 69, 69, 57, 60, 74, 73, 73, 49, 65, 33, 17, 9, 7, 54, 73, 73, 73, 54, 70, 73, 73, 41, 30, 0, 0, 20, 0, 0, 0, 64, 52, 0, 0, 0, 8, 20, 34, 65, 20, 20, 20, 20, 20, 0, 65, 34, 20, 8, 2, 1, 89, 9, 6, 62, 65, 93, 89, 78, 124, 18, 17, 18, 124, 127, 73, 73, 73, 54, 62, 65, 65, 65, 34, 127, 65, 65, 65, 62, 127, 73, 73, 73, 65, 127, 9, 9, 9, 1, 62, 65, 65, 81, 115, 127, 8, 8, 8, 127, 0, 65, 127, 65, 0, 32, 64, 65, 63, 1, 127, 8, 20, 34, 65, 127, 64, 64, 64, 64, 127, 2, 28, 2, 127, 127, 4, 8, 16, 127, 62, 65, 65, 65, 62, 127, 9, 9, 9, 6, 62, 65, 81, 33, 94, 127, 9, 25, 41, 70, 38, 73, 73, 73, 50, 3, 1, 127, 1, 3, 63, 64, 64, 64, 63, 31, 32, 64, 32, 31, 63, 64, 56, 64, 63, 99, 20, 8, 20, 99, 3, 4, 120, 4, 3, 97, 89, 73, 77, 67, 0, 127, 65, 65, 65, 2, 4, 8, 16, 32, 0, 65, 65, 65, 127, 4, 2, 1, 2, 4, 64, 64, 64, 64, 64, 0, 3, 7, 8, 0, 32, 84, 84, 120, 64, 127, 40, 68, 68, 56, 56, 68, 68, 68, 40, 56, 68, 68, 40, 127, 56, 84, 84, 84, 24, 0, 8, 126, 9, 2, 24, 164, 164, 156, 120, 127, 8, 4, 4, 120, 0, 68, 125, 64, 0, 32, 64, 64, 61, 0, 127, 16, 40, 68, 0, 0, 65, 127, 64, 0, 124, 4, 120, 4, 120, 124, 8, 4, 4, 120, 56, 68, 68, 68, 56, 252, 24, 36, 36, 24, 24, 36, 36, 24, 252, 124, 8, 4, 4, 8, 72, 84, 84, 84, 36, 4, 4, 63, 68, 36, 60, 64, 64, 32, 124, 28, 32, 64, 32, 28, 60, 64, 48, 64, 60, 68, 40, 16, 40, 68, 76, 144, 144, 144, 124, 68, 100, 84, 76, 68, 0, 8, 54, 65, 0, 0, 0, 119, 0, 0, 0, 65, 54, 8, 0, 2, 1, 2, 4, 2, 60, 38, 35, 38, 60, 30, 161, 161, 97, 18, 58, 64, 64, 32, 122, 56, 84, 84, 85, 89, 33, 85, 85, 121, 65, 33, 84, 84, 120, 65, 33, 85, 84, 120, 64, 32, 84, 85, 121, 64, 12, 30, 82, 114, 18, 57, 85, 85, 85, 89, 57, 84, 84, 84, 89, 57, 85, 84, 84, 88, 0, 0, 69, 124, 65, 0, 2, 69, 125, 66, 0, 1, 69, 124, 64, 240, 41, 36, 41, 240, 240, 40, 37, 40, 240, 124, 84, 85, 69, 0, 32, 84, 84, 124, 84, 124, 10, 9, 127, 73, 50, 73, 73, 73, 50, 50, 72, 72, 72, 50, 50, 74, 72, 72, 48, 58, 65, 65, 33, 122, 58, 66, 64, 32, 120, 0, 157, 160, 160, 125, 57, 68, 68, 68, 57, 61, 64, 64, 64, 61, 60, 36, 255, 36, 36, 72, 126, 73, 67, 102, 43, 47, 252, 47, 43, 255, 9, 41, 246, 32, 192, 136, 126, 9, 3, 32, 84, 84, 121, 65, 0, 0, 68, 125, 65, 48, 72, 72, 74, 50, 56, 64, 64, 34, 122, 0, 122, 10, 10, 114, 125, 13, 25, 49, 125, 38, 41, 41, 47, 40, 38, 41, 41, 41, 38, 48, 72, 77, 64, 32, 56, 8, 8, 8, 8, 8, 8, 8, 8, 56, 47, 16, 200, 172, 186, 47, 16, 40, 52, 250, 0, 0, 123, 0, 0, 8, 20, 42, 20, 34, 34, 20, 42, 20, 8, 170, 0, 85, 0, 170, 170, 85, 170, 85, 170, 0, 0, 0, 255, 0, 16, 16, 16, 255, 0, 20, 20, 20, 255, 0, 16, 16, 255, 0, 255, 16, 16, 240, 16, 240, 20, 20, 20, 252, 0, 20, 20, 247, 0, 255, 0, 0, 255, 0, 255, 20, 20, 244, 4, 252, 20, 20, 23, 16, 31, 16, 16, 31, 16, 31, 20, 20, 20, 31, 0, 16, 16, 16, 240, 0, 0, 0, 0, 31, 16, 16, 16, 16, 31, 16, 16, 16, 16, 240, 16, 0, 0, 0, 255, 16, 16, 16, 16, 16, 16, 16, 16, 16, 255, 16, 0, 0, 0, 255, 20, 0, 0, 255, 0, 255, 0, 0, 31, 16, 23, 0, 0, 252, 4, 244, 20, 20, 23, 16, 23, 20, 20, 244, 4, 244, 0, 0, 255, 0, 247, 20, 20, 20, 20, 20, 20, 20, 247, 0, 247, 20, 20, 20, 23, 20, 16, 16, 31, 16, 31, 20, 20, 20, 244, 20, 16, 16, 240, 16, 240, 0, 0, 31, 16, 31, 0, 0, 0, 31, 20, 0, 0, 0, 252, 20, 0, 0, 240, 16, 240, 16, 16, 255, 16, 255, 20, 20, 20, 255, 20, 16, 16, 16, 31, 0, 0, 0, 0, 240, 16, 255, 255, 255, 255, 255, 240, 240, 240, 240, 240, 255, 255, 255, 0, 0, 0, 0, 0, 255, 255, 15, 15, 15, 15, 15, 56, 68, 68, 56, 68, 124, 42, 42, 62, 20, 126, 2, 2, 6, 6, 2, 126, 2, 126, 2, 99, 85, 73, 65, 99, 56, 68, 68, 60, 4, 64, 126, 32, 30, 32, 6, 2, 126, 2, 2, 153, 165, 231, 165, 153, 28, 42, 73, 42, 28, 76, 114, 1, 114, 76, 48, 74, 77, 77, 48, 48, 72, 120, 72, 48, 188, 98, 90, 70, 61, 62, 73, 73, 73, 0, 126, 1, 1, 1, 126, 42, 42, 42, 42, 42, 68, 68, 95, 68, 68, 64, 81, 74, 68, 64, 64, 68, 74, 81, 64, 0, 0, 255, 1, 3, 224, 128, 255, 0, 0, 8, 8, 107, 107, 8, 54, 18, 54, 36, 54, 6, 15, 9, 15, 6, 0, 0, 24, 24, 0, 0, 0, 16, 16, 0, 48, 64, 255, 1, 1, 0, 31, 1, 1, 30, 0, 25, 29, 23, 18, 0, 60, 60, 60, 60, 0, 0, 0, 0, 0]} \ No newline at end of file diff --git a/PSL/SENSORS/supported.py b/pslab/external/supported.py similarity index 100% rename from PSL/SENSORS/supported.py rename to pslab/external/supported.py diff --git a/pslab/external/tcd1304.py b/pslab/external/tcd1304.py new file mode 100644 index 00000000..aaec8346 --- /dev/null +++ b/pslab/external/tcd1304.py @@ -0,0 +1,129 @@ +"""Proof of concept TCD1304 driver. + +This driver can only drive the TCD1304 in normal mode, not electronic shutter +mode. This is because the PSLab can (currently) only output two different PWM +frequencies simultaneously, and electronic shutter mode requires three. + +Furthermode, the clock frequencies are locked to 2 MHz (master) and 125 Hz (SH +and ICG). The reason is the following: + +The ICG period must be greater than the readout period, which is: + master clock period * 4 * number of pixels = 7.4 ms +7.4 ms -> 135 Hz, which is therefore the fastest the ICG clock can run. + +The lowest possible frequency the PSLab can generate with sufficient +clock precision is 123 Hz. Below that the 16-bit timer that drives the +PWM must be prescaled so much that we can no longer satisfy the TCD1304's +timing requirements. + +Thus, the range of possible ICG frequencies is [123, 135], which is so small +that it makes more sense to just lock it to 125 Hz, which has the added +advantage of being an even divisor of the PSLab's MCU frequency (64 MHz). + +It should be possible to increase the master clock to 4 MHz, which would also +make ICG frequencies up to 250 Hz possible. However, the readout period would +be 3.7 ms, which the PSLab's oscilloscope might struggle to capture with good +quality. +""" + +from typing import List + +from numpy import ndarray + +from pslab import Oscilloscope +from pslab import PowerSupply +from pslab import PWMGenerator +from pslab.instrument.waveform_generator import _get_wavelength +from pslab.protocol import MAX_SAMPLES +from pslab.serial_handler import SerialHandler + + +class TCD1304: + def __init__(self, device: SerialHandler): + self._pwm = PWMGenerator(device) + self._oscilloscope = Oscilloscope(device) + self._sh_frequency = 125 + + def start_clocks(self, inverted: bool = True): + """Start the Master, SH, and ICG clocks. + + Parameters + ---------- + inverted : bool, optional + The TCD1304 datasheet recommends placing a hex inverter between the + sensor and the MCU. By default, the clocks are therefore inverted + relative to what they should be to drive the sensor. If you do not + use a hex inverter, set this to False. + + Returns + ------- + None. + + """ + self._pwm.map_reference_clock("SQ1", 6) # 2 MHz + + resolution = _get_wavelength(self._sh_frequency)[0] ** -1 + # Timing requirements: + # (1) The SH clock must go high between 100 ns to 1000 ns after the ICG + # clock goes low. + # (2) The SH clock must stay high for at least 1 µs. + # (3) The ICG clock must stay low at least 1 µs after the SH clock goes + # low. + # I got the following numbers through trial and error. They meet the + # above requirements. + # TODO: Calculate appropriate duty cycles and phases. + magic_numbers = [ + 12 * resolution, + 48 * resolution, + 16 * resolution, + 1 - 42 * resolution, + ] + + if inverted: + self._pwm.generate( + ["SQ2", "SQ3"], + frequency=self._sh_frequency, + duty_cycles=[1 - magic_numbers[0], magic_numbers[1]], + phases=[magic_numbers[2], 0], + ) + else: + self._pwm.generate( + ["SQ2", "SQ3"], + frequency=self._sh_frequency, + duty_cycles=[magic_numbers[0], 1 - magic_numbers[1]], + phases=[magic_numbers[3], 0], + ) + + def stop_clocks(self): + """Stop the Master, SH, and ICG clocks. + + Returns + ------- + None. + + """ + self._pwm.set_state(sq1=0, sq2=0, sq3=0) + + def read(self, analogin: str = "CH1", trigger: str = "CH2") -> List[ndarray]: + """Read the sensor's analog output. + + Connect one of the PSLab's analog inputs to the sensor's analog output. + + Parameters + ---------- + channel : str, optional + The analog input connected to the sensor's OS pin. + Defaults to "CH1". + trigger : str, optional + The analog input connected to the sensor's ICG pin. + Defaults to "CH2". + + Returns + ------- + List[ndarray] + Timestamps and corresponding voltages. + + """ + return self._oscilloscope.capture( + analogin, 8000, 1, trigger=3, trigger_channel=trigger + ) diff --git a/pslab/instrument/__init__.py b/pslab/instrument/__init__.py new file mode 100644 index 00000000..bdf88e7a --- /dev/null +++ b/pslab/instrument/__init__.py @@ -0,0 +1,5 @@ +"""Contains modules for controlling the PSLab's built-in instruments. + +The built-in instruments are ``LogicAnalyzer``, ``Multimeter``, +``Oscilloscope``, ``PowerSupply``, ``PWMGenerator``, and ``WaveformGenerator``. +""" diff --git a/pslab/instrument/analog.py b/pslab/instrument/analog.py new file mode 100644 index 00000000..bc3ccca0 --- /dev/null +++ b/pslab/instrument/analog.py @@ -0,0 +1,246 @@ +"""Objects related to the PSLab's analog input channels. + +This module contains several module level variables with details on the analog +inputs' capabilities, including possible gain values, voltage ranges, and +firmware-interal enumeration. + +This module also contains the AnalogInput class, an instance of which functions +as a model of a particular analog input. +""" + +import logging +from typing import List, Union + +import numpy as np + +logger = logging.getLogger(__name__) + +GAIN_VALUES = (1, 2, 4, 5, 8, 10, 16, 32) + +ANALOG_CHANNELS = ( + "CH1", + "CH2", + "CH3", + "MIC", + "CAP", + "RES", + "VOL", + "AN4", +) + +INPUT_RANGES = { + "CH1": (16.5, -16.5), # Specify inverted channels explicitly by reversing range! + "CH2": (16.5, -16.5), + "CH3": (-3.3, 3.3), # external gain control analog input + "MIC": (-3.3, 3.3), # connected to MIC amplifier + "CAP": (0, 3.3), + "RES": (0, 3.3), + "VOL": (0, 3.3), + "AN4": (0, 3.3), +} + +PIC_ADC_MULTIPLEX = { + "CH1": 3, + "CH2": 0, + "CH3": 1, + "MIC": 2, + "AN4": 4, + "RES": 7, + "CAP": 5, + "VOL": 8, +} + + +class AnalogInput: + """Model of the PSLab's analog inputs, used to scale raw values to voltages. + + Parameters + ---------- + name : {'CH1', 'CH2', 'CH3', 'MIC', 'CAP', 'RES', 'VOL'} + Name of the analog channel to model. + + Attributes + ---------- + samples_in_buffer : int + Number of samples collected on this channel currently being held in the + device's ADC buffer. + buffer_idx : int or None + Location in the device's ADC buffer where the samples are stored. None + if no samples captured by this channel are currently held in the + buffer. + chosa : int + Number used to refer to this channel in the firmware. + """ + + def __init__(self, name: str): + self._name = name + self._resolution = 2**10 - 1 + + if self._name == "CH1": + self.programmable_gain_amplifier = 1 + self._gain = 1 + elif self._name == "CH2": + self.programmable_gain_amplifier = 2 + self._gain = 1 + else: + self.programmable_gain_amplifier = None + self._gain = 1 + + self.samples_in_buffer = 0 + self.buffer_idx = None + self._scale = np.poly1d(0) + self._unscale = np.poly1d(0) + self.chosa = PIC_ADC_MULTIPLEX[self._name] + self._calibrate() + + @property + def gain(self) -> Union[int, None]: + """int: Analog gain. + + Setting a new gain level will automatically recalibrate the channel. + On channels other than CH1 and CH2 gain is None. + + Raises + ------ + TypeError + If gain is set on a channel which does not support it. + ValueError + If a gain value other than 1, 2, 4, 5, 8, 10, 16, 32 is set. + """ + if self._name in ("CH1", "CH2"): + return self._gain + else: + return None + + @gain.setter + def gain(self, value: Union[int, float]): + if self._name not in ("CH1", "CH2"): + raise TypeError(f"Analog gain is not available on {self._name}.") + + if value not in GAIN_VALUES: + raise ValueError(f"Invalid gain. Valid values are {GAIN_VALUES}.") + + self._gain = value + self._calibrate() + + @property + def resolution(self) -> int: + """int: Resolution in bits. + + Setting a new resolution will automatically recalibrate the channel. + + Raises + ------ + ValueError + If a resolution other than 10 or 12 is set. + """ + return int(np.log2((self._resolution + 1))) + + @resolution.setter + def resolution(self, value: int): + if value not in (10, 12): + raise ValueError("Resolution must be 10 or 12 bits.") + self._resolution = 2**value - 1 + self._calibrate() + + def _calibrate(self): + A = INPUT_RANGES[self._name][0] / self._gain + B = INPUT_RANGES[self._name][1] / self._gain + slope = B - A + intercept = A + self._scale = np.poly1d([slope / self._resolution, intercept]) + self._unscale = np.poly1d( + [self._resolution / slope, -self._resolution * intercept / slope] + ) + + def scale(self, raw: Union[int, List[int]]) -> float: + """Translate raw integer value from device to corresponding voltage. + + Inverse of :meth:`unscale`. + + Parameters + ---------- + raw : int, List[int] + An integer, or a list of integers, received from the device. + + Returns + ------- + float + Voltage, translated from raw based on channel range, gain, and resolution. + """ + return self._scale(raw) + + def unscale(self, voltage: float) -> int: + """Translate a voltage to a raw integer value interpretable by the device. + + Inverse of :meth:`scale`. + + Parameters + ---------- + voltage : float + Voltage in volts. + + Returns + ------- + int + Corresponding integer value, adjusted for resolution and gain and clipped + to the channel's range. + """ + level = self._unscale(voltage) + level = np.clip(level, 0, self._resolution) + level = np.round(level) + return int(level) + + +class AnalogOutput: + """Model of the PSLab's analog outputs. + + Parameters + ---------- + name : str + Name of the analog output pin represented by this instance. + + Attributes + ---------- + frequency : float + Frequency of the waveform on this pin in Hz. + wavetype : {'sine', 'tria', 'custom'} + Type of waveform on this pin. 'sine' is a sine wave with amplitude + 3.3 V, 'tria' is a triangle wave with amplitude 3.3 V, 'custom' is any + other waveform set with :meth:`load_function` or :meth:`load_table`. + """ + + RANGE = (-3.3, 3.3) + + def __init__(self, name): + self.name = name + self.frequency = 0 + self.wavetype = "sine" + self._waveform_table = self.RANGE[1] * np.sin( + np.arange( + self.RANGE[0], self.RANGE[1], (self.RANGE[1] - self.RANGE[0]) / 512 + ) + ) + + @property + def waveform_table(self) -> np.ndarray: + """numpy.ndarray: 512-value waveform table loaded on this output.""" + # A form of amplitude control. Max PWM duty cycle out of 512 clock cycles. + return self._range_normalize(self._waveform_table, 511) + + @waveform_table.setter + def waveform_table(self, points: np.ndarray): + if max(points) - min(points) > self.RANGE[1] - self.RANGE[0]: + logger.warning(f"Analog output {self.name} saturated.") + self._waveform_table = np.clip(points, self.RANGE[0], self.RANGE[1]) + + @property + def lowres_waveform_table(self) -> np.ndarray: + """numpy.ndarray: 32-value waveform table loaded on this output.""" + # Max PWM duty cycle out of 64 clock cycles. + return self._range_normalize(self._waveform_table[::16], 63) + + def _range_normalize(self, x: np.ndarray, norm: int = 1) -> np.ndarray: + """Normalize waveform table to the digital output range.""" + x = (x - self.RANGE[0]) / (self.RANGE[1] - self.RANGE[0]) * norm + return np.int16(np.round(x)).tolist() diff --git a/pslab/instrument/buffer.py b/pslab/instrument/buffer.py new file mode 100644 index 00000000..a2a5cdbb --- /dev/null +++ b/pslab/instrument/buffer.py @@ -0,0 +1,87 @@ +"""The PSLab has a sample buffer where collected data is stored temporarily.""" + +import pslab.protocol as CP + + +class ADCBufferMixin: + """Mixin for classes that need to read or write to the ADC buffer.""" + + def fetch_buffer(self, samples: int, starting_position: int = 0): + """Fetch a section of the ADC buffer. + + Parameters + ---------- + samples : int + Number of samples to fetch. + starting_position : int, optional + Location in the ADC buffer to start from. By default samples will + be fetched from the beginning of the buffer. + + Returns + ------- + received : list of int + List of received samples. + """ + received = [] + buf_size = 128 + remaining = samples + idx = starting_position + + while remaining > 0: + self._device.send_byte(CP.COMMON) + self._device.send_byte(CP.RETRIEVE_BUFFER) + self._device.send_int(idx) + samps = min(remaining, buf_size) + self._device.send_int(samps) + received += [self._device.get_int() for _ in range(samps)] + self._device.get_ack() + remaining -= samps + idx += samps + + return received + + def clear_buffer(self, samples: int, starting_position: int = 0): + """Clear a section of the ADC buffer. + + Parameters + ---------- + samples : int + Number of samples to clear from the buffer. + starting_position : int, optional + Location in the ADC buffer to start from. By default samples will + be cleared from the beginning of the buffer. + """ + self._device.send_byte(CP.COMMON) + self._device.send_byte(CP.CLEAR_BUFFER) + self._device.send_int(starting_position) + self._device.send_int(samples) + self._device.get_ack() + + def fill_buffer(self, data: list[int], starting_position: int = 0): + """Fill a section of the ADC buffer with data. + + Parameters + ---------- + data : list of int + Values to write to the ADC buffer. + starting_position : int, optional + Location in the ADC buffer to start from. By default writing will + start at the beginning of the buffer. + """ + buf_size = 128 + idx = starting_position + remaining = len(data) + + while remaining > 0: + self._device.send_byte(CP.COMMON) + self._device.send_byte(CP.FILL_BUFFER) + self._device.send_int(idx) + samps = min(remaining, buf_size) + self._device.send_int(samps) + + for value in data[idx : idx + samps]: + self._device.send_int(value) + + self._device.get_ack() + idx += samps + remaining -= samps diff --git a/pslab/instrument/digital.py b/pslab/instrument/digital.py new file mode 100644 index 00000000..6040202d --- /dev/null +++ b/pslab/instrument/digital.py @@ -0,0 +1,183 @@ +"""Objects related to the PSLab's digital input channels.""" + +import numpy as np + +DIGITAL_INPUTS = ("LA1", "LA2", "LA3", "LA4", "RES", "EXT", "FRQ") + +DIGITAL_OUTPUTS = ("SQ1", "SQ2", "SQ3", "SQ4") + +MODES = { + "sixteen rising": 5, + "four rising": 4, + "rising": 3, + "falling": 2, + "any": 1, + "disabled": 0, +} + + +class DigitalInput: + """Model of the PSLab's digital inputs. + + Parameters + ---------- + name : {"LA1", "LA2", "LA3", "LA4", "RES", "FRQ"} + Name of the digital channel to model. + + Attributes + ---------- + name : str + One of {"LA1", "LA2", "LA3", "LA4", "RES", "FRQ"}. + number : int + Number used to refer to this channel in the firmware. + datatype : str + Either "int" or "long", depending on if a 16 or 32-bit counter is used + to capture timestamps for this channel. + events_in_buffer : int + Number of logic events detected on this channel, the timestamps of + which are currently being held in the device's ADC buffer. + buffer_idx : Union[int, None] + Location in the device's ADC buffer where the events are stored. None + if no events captured by this channel are currently held in the buffer. + """ + + def __init__(self, name: str): + self.name = name + self.number = DIGITAL_INPUTS.index(self.name) + self.datatype = "long" + self.events_in_buffer = 0 + self.buffer_idx = None + self._logic_mode = MODES["any"] + + @property + def logic_mode(self) -> str: + """str: Type of logic event which should be captured on this channel. + + The options are: + any: Capture every edge. + rising: Capture every rising edge. + falling: Capture every falling edge. + four rising: Capture every fourth rising edge. + sixteen rising: Capture every fourth rising edge. + """ + return {v: k for k, v in MODES.items()}[self._logic_mode] + + def _get_xy(self, initial_state: bool, timestamps: np.ndarray): + x = np.repeat(timestamps, 3) + x = np.insert(x, 0, 0) + x[0] = 0 + y = np.array(len(x) * [False]) + + if self.logic_mode == "any": + y[0] = initial_state + for i in range(1, len(x), 3): + y[i] = y[i - 1] # Value before this timestamp. + y[i + 1] = not y[i] # Value at this timestamp. + y[i + 2] = y[i + 1] # Value leaving this timetamp. + elif self.logic_mode == "falling": + y[0] = True + for i in range(1, len(x), 3): + y[i] = True # Value before this timestamp. + y[i + 1] = False # Value at this timestamp. + y[i + 2] = True # Value leaving this timetamp. + else: + y[0] = False + for i in range(1, len(x), 3): + y[i] = False # Value before this timestamp. + y[i + 1] = True # Value at this timestamp. + y[i + 2] = False # Value leaving this timetamp. + + return x, y + + +class DigitalOutput: + """Model of the PSLab's digital outputs. + + Parameters + ---------- + name : {'SQ1', 'SQ2', 'SQ3', 'SQ4'} + Name of the digital output pin represented by the instance. + """ + + def __init__(self, name: str): + self._name = name + self._state = "LOW" + self._duty_cycle = 0 + self.phase = 0 + self.remapped = False + + @property + def name(self) -> str: + """str: Name of this pin.""" + return self._name + + @name.setter + def name(self, value): + if value in DIGITAL_OUTPUTS: + self._name = value + else: + e = f"Invalid digital output {value}. Choose one of {DIGITAL_OUTPUTS}." + raise ValueError(e) + + @property + def state(self) -> str: + """str: State of the digital output. Can be 'HIGH', 'LOW', or 'PWM'.""" + return self._state + + @property + def duty_cycle(self) -> float: + """float: Duty cycle of the PWM signal on this pin.""" + return self._duty_cycle + + @duty_cycle.setter + def duty_cycle(self, value: float): + if value == 0: + self._state = "LOW" + elif value < 1: + self._state = "PWM" + elif value == 1: + self._state = "HIGH" + else: + raise ValueError("Duty cycle must be in range [0, 1].") + + self._duty_cycle = value + + @property + def state_mask(self) -> int: + """int: State mask for this pin. + + The state mask is used in the DOUT->SET_STATE command to set the + digital output pins HIGH or LOW. For example: + + 0x10 | 1 << 0 | 0x40 | 0 << 2 | 0x80 | 1 << 3 + + would set SQ1 and SQ4 HIGH, SQ3 LOW, and leave SQ2 unchanged. + """ + if self.name == "SQ1": + return 0x10 + elif self.name == "SQ2": + return 0x20 + elif self.name == "SQ3": + return 0x40 + elif self.name == "SQ4": + return 0x80 + + @property + def reference_clock_map(self) -> int: + """int: Reference clock map value for this pin. + + The reference clock map is used in the WAVEGEN->MAP_REFERENCE command + to map a digital output pin directly to the interal oscillator. This + can be used to achieve very high frequencies, with the caveat that + the only frequencies available are quotients of 128 MHz and powers of + 2 up to 15. For example, sending (2 | 4) followed by 3 outputs + 128 / (1 << 3) = 16 MHz on SQ2 and SQ3. + """ + if self.name == "SQ1": + return 1 + elif self.name == "SQ2": + return 2 + elif self.name == "SQ3": + return 4 + elif self.name == "SQ4": + return 8 diff --git a/pslab/instrument/logic_analyzer.py b/pslab/instrument/logic_analyzer.py new file mode 100644 index 00000000..8888034f --- /dev/null +++ b/pslab/instrument/logic_analyzer.py @@ -0,0 +1,785 @@ +"""Classes and functions related to the PSLab's logic analyzer instrument. + +Example +------- +>>> from pslab import LogicAnalyzer +>>> la = LogicAnalyzer() +>>> t = la.capture(channels=2, events=1600, modes=["falling", "any"]) +""" + +import time +from collections import OrderedDict +from typing import Dict, List, Tuple, Union + +import numpy as np + +import pslab.protocol as CP +from pslab.connection import ConnectionHandler, autoconnect +from pslab.instrument.buffer import ADCBufferMixin +from pslab.instrument.digital import DigitalInput, DIGITAL_INPUTS, MODES + + +class LogicAnalyzer(ADCBufferMixin): + """Investigate digital signals on up to four channels simultaneously. + + Parameters + ---------- + device : :class:`SerialHandler`, optional + Serial connection to PSLab device. If not provided, a new one will be + created. + + Attributes + ---------- + trigger_channel : str + See :meth:`configure_trigger`. + trigger_mode : str + See :meth:`configure_trigger`. + """ + + _PRESCALERS = { + 0: 1, + 1: 8, + 2: 64, + 3: 256, + } + + # When capturing multiple channels, there is a two clock cycle + # delay between channels. + _CAPTURE_DELAY = 2 + + def __init__(self, device: ConnectionHandler | None = None): + self._device = device if device is not None else autoconnect() + self._channels = {d: DigitalInput(d) for d in DIGITAL_INPUTS} + self.trigger_channel = "LA1" + self._trigger_channel = self._channels["LA1"] + self.trigger_mode = "disabled" + self._trigger_mode = 0 + self._prescaler = 0 + self._channel_one_map = "LA1" + self._channel_two_map = "LA2" + self._trimmed = 0 + + def measure_frequency( + self, channel: str, simultaneous_oscilloscope: bool = False, timeout: float = 1 + ) -> float: + """Measure the frequency of a signal. + + Parameters + ---------- + channel : {"LA1", "LA2", "LA3", "LA4"} + Name of the digital input channel in which to measure the + frequency. + simultaneous_oscilloscope: bool, optional + Set this to True if you need to use the oscilloscope at the same + time. Uses firmware instead of software to measure the frequency, + which may fail and return 0. Will not give accurate results above + 10 MHz. The default value is False. + timeout : float, optional + Timeout in seconds before cancelling measurement. The default value + is 1 second. + + Returns + ------- + frequency : float + The signal's frequency in Hz. + """ + if simultaneous_oscilloscope: + return self._measure_frequency_firmware(channel, timeout) + else: + tmp = self._channel_one_map + self._channel_one_map = channel + t = self.capture(1, 2, modes=["sixteen rising"], timeout=timeout)[0] + self._channel_one_map = tmp + + try: + period = (t[1] - t[0]) * 1e-6 / 16 + frequency = period**-1 + except IndexError: + frequency = 0 + + if frequency >= 1e7: + frequency = self._get_high_frequency(channel) + + return frequency + + def _measure_frequency_firmware( + self, channel: str, timeout: float, retry: bool = True + ) -> float: + self._device.send_byte(CP.COMMON) + self._device.send_byte(CP.GET_FREQUENCY) + self._device.send_int(int(timeout * 64e6) >> 16) + self._device.send_byte(self._channels[channel].number) + time.sleep(timeout) + + error = self._device.get_byte() + t = [self._device.get_long() for a in range(2)] + self._device.get_ack() + edges = 16 + period = (t[1] - t[0]) / edges / CP.CLOCK_RATE + + if error or period == 0: + # Retry once. + if retry: + return self._measure_frequency_firmware(channel, timeout, False) + else: + return 0 + else: + return period**-1 + + def _get_high_frequency(self, channel: str) -> float: + """Measure high frequency signals using firmware. + + The input frequency is fed to a 32 bit counter for a period of 100 ms. + The value of the counter at the end of 100 ms is used to calculate the + frequency. + """ + self._device.send_byte(CP.COMMON) + self._device.send_byte(CP.GET_ALTERNATE_HIGH_FREQUENCY) + self._device.send_byte(self._channels[channel].number) + scale = self._device.get_byte() + counter_value = self._device.get_long() + self._device.get_ack() + + return scale * counter_value / 1e-1 # 100 ms sampling + + def measure_interval( + self, channels: List[str], modes: List[str], timeout: float = 1 + ) -> float: + """Measure the time between two events. + + This method cannot be used at the same time as the oscilloscope. + + Parameters + ---------- + channels : List[str] + A pair of digital inputs, LA1, LA2, LA3, or LA4. Both can be the + same. + modes : List[str] + Type of logic event to listen for on each channel. See + :class:`DigitalInput` for available modes. + timeout : float, optional + Timeout in seconds before cancelling measurement. The default value + is 1 second. + + Returns + ------- + interval : float + Time between events in microseconds. A negative value means that + the event on the second channel happend first. + """ + tmp_trigger = self._trigger_channel.name + self.configure_trigger(channels[0], self.trigger_mode) + tmp_map = self._channel_one_map, self._channel_two_map + self._channel_one_map = channels[0] + self._channel_two_map = channels[1] + + if channels[0] == channels[1]: + # 34 edges contains 17 rising edges, i.e two + # 'every sixteenth rising edge' events. + t = self.capture(1, 34, modes=["any"], timeout=timeout)[0] + initial = self.get_initial_states()[self._channel_one_map] + t1 = self._get_first_event(t, modes[0], initial) + + if modes[0] == modes[1]: + idx = 1 if modes[1] == "any" else 2 + initial = initial if idx == 2 else not initial + t2 = self._get_first_event(t[idx:], modes[1], initial) + else: + t2 = self._get_first_event(t, modes[1], initial) + else: + t1, t2 = self.capture(2, 1, modes=modes, timeout=timeout) + + t1, t2 = t1[0], t2[0] + + self.configure_trigger(tmp_trigger, self.trigger_mode) + self._channel_one_map = tmp_map[0] + self._channel_two_map = tmp_map[1] + + return t2 - t1 + + @staticmethod + def _get_first_event(events: np.ndarray, mode: str, initial: bool) -> np.ndarray: + if mode == "any": + return events[0] + elif mode == "rising": + return events[int(initial)] + elif mode == "falling": + return events[int(not initial)] + elif mode == "four rising": + return events[initial::2][3] + elif mode == "sixteen rising": + return events[initial::2][15] + + def measure_duty_cycle(self, channel: str, timeout: float = 1) -> Tuple[float]: + """Measure duty cycle and wavelength. + + This method cannot be used at the same time as the oscilloscope. + + Parameters + ---------- + channel : {"LA1", "LA2", "LA3", "LA4"} + Digital input on which to measure. + timeout : float, optional + Timeout in seconds before cancelling measurement. The default value + is 1 second. + + Returns + ------- + wavelength : float + Wavelength in microseconds. + duty_cycle : float + Duty cycle as a value between 0 - 1. + """ + tmp_trigger_mode = self.trigger_mode + tmp_trigger_channel = self._trigger_channel.name + self.configure_trigger(trigger_channel=channel, trigger_mode="rising") + tmp_map = self._channel_one_map + self._channel_one_map = channel + t = self.capture(1, 3, modes=["any"], timeout=timeout)[0] + self._channel_one_map = tmp_map + self.configure_trigger(tmp_trigger_channel, tmp_trigger_mode) + + period = t[2] - t[0] + # First change is HIGH -> LOW since we trigger on rising. + duty_cycle = 1 - (t[1] - t[0]) / period + + return period, duty_cycle + + def capture( + self, + channels: Union[int, str, List[str]], + events: int = CP.MAX_SAMPLES // 4, + timeout: float = 1, + modes: List[str] = 4 * ("any",), + e2e_time: float = None, + block: bool = True, + ) -> Union[List[np.ndarray], None]: + """Capture logic events. + + This method cannot be used at the same time as the oscilloscope. + + Parameters + ---------- + channels : {1, 2, 3, 4} or str or list of str + Number of channels to capture events on. Events will be captured on + LA1, LA2, LA3, and LA4, in that order. Alternatively, the name of + of a single digital input, or a list of two names of digital inputs + can be provided. In that case, events will be captured only on that + or those specific channels. + events : int, optional + Number of logic events to capture on each channel. The default and + maximum value is 2500. + timeout : float, optional + Timeout in seconds before cancelling measurement in blocking mode. + If the timeout is reached, the events captured up to that point + will be returned. The default value is 1 second. + modes : List[str], optional + List of strings specifying the type of logic level change to + capture on each channel. See :class:`DigitalInput` for available + modes. The default value is ("any", "any", "any", "any"). + e2e_time : float, optional + The maximum time between events in seconds. This is only required + in three and four channel mode, which uses 16-bit counters as + opposed to 32-bit counters which are used in one and two channel + mode. The 16-bit counter normally rolls over after 1024 µs, so if + the time between events is greater than that the timestamp + calculations will be incorrect. By setting this to a value greater + than 1024 µs, the counter will be slowed down by a prescaler, which + can extend the maximum allowed event-to-event time gap to up to + 262 ms. If the time gap is greater than that, three and four + channel mode cannot be used. One and two channel mode supports + timegaps up to 67 seconds. + block : bool, optional + Whether to block while waiting for events to be captured. If this + is False, this method will return None immediately and the captured + events must be manually fetched by calling :meth:`fetch_data`. The + default value is True. + + Returns + ------- + events : list of numpy.ndarray or None + List of numpy.ndarrays containing timestamps in microseconds when + logic events were detected, or None if block is False. The length + of the list is equal to the number of channels that were used to + capture events, and each list element corresponds to a channel. + + Raises + ------ + ValueError if too many events are requested, or + ValueError if too many channels are selected. + """ + channels = self._check_arguments(channels, events) + self.stop() + self._prescaler = 0 + self.clear_buffer(CP.MAX_SAMPLES) + self._invalidate_buffer() + self._configure_trigger(channels) + modes = [MODES[m] for m in modes] + start_time = time.time() + + for e, c in enumerate( + [self._channel_one_map, self._channel_two_map, "LA3", "LA4"][:channels] + ): + c = self._channels[c] + c.events_in_buffer = events + c.datatype = "long" if channels < 3 else "int" + c.buffer_idx = 2500 * e * (1 if c.datatype == "int" else 2) + c._logic_mode = modes[e] + + if channels == 1: + self._capture_one() + elif channels == 2: + self._capture_two() + else: + self._capture_four(e2e_time) + + if block: + # Discard 4:th channel if user asked for 3. + timestamps = self.fetch_data()[:channels] + progress = min([len(t) for t in timestamps]) + while progress < events: + timestamps = self.fetch_data()[:channels] + progress = min([len(t) for t in timestamps]) + if time.time() - start_time >= timeout: + break + if progress >= CP.MAX_SAMPLES // 4 - self._trimmed: + break + else: + return + + for e, t in enumerate(timestamps): + timestamps[e] = t[:events] # Don't surprise the user with extra events. + + return timestamps + + def _check_arguments(self, channels: Union[int, str, List[str]], events: int): + if isinstance(channels, str): + self._channel_one_map = channels + channels = 1 + + if isinstance(channels, list): + self._channel_one_map = channels[0] + self._channel_two_map = channels[1] + channels = 2 + + max_events = CP.MAX_SAMPLES // 4 + + if events > max_events: + raise ValueError(f"Events must be fewer than {max_events}.") + elif channels < 0 or channels > 4: + raise ValueError("Channels must be between 1-4.") + + return channels + + def _capture_one(self): + self._channels[self._channel_one_map]._prescaler = 0 + self._device.send_byte(CP.TIMING) + self._device.send_byte(CP.START_ALTERNATE_ONE_CHAN_LA) + self._device.send_int(CP.MAX_SAMPLES // 4) + self._device.send_byte( + (self._channels[self._channel_one_map].number << 4) + | self._channels[self._channel_one_map]._logic_mode + ) + self._device.send_byte( + (self._channels[self._channel_one_map].number << 4) | self._trigger_mode + ) + self._device.get_ack() + + def _capture_two(self): + for c in list(self._channels.values())[:2]: + c._prescaler = 0 + + self._device.send_byte(CP.TIMING) + self._device.send_byte(CP.START_TWO_CHAN_LA) + self._device.send_int(CP.MAX_SAMPLES // 4) + self._device.send_byte((self._trigger_channel.number << 4) | self._trigger_mode) + self._device.send_byte( + self._channels[self._channel_one_map]._logic_mode + | (self._channels[self._channel_two_map]._logic_mode << 4) + ) + self._device.send_byte( + self._channels[self._channel_one_map].number + | (self._channels[self._channel_two_map].number << 4) + ) + self._device.get_ack() + + def _capture_four(self, e2e_time: float): + rollover_time = (2**16 - 1) / CP.CLOCK_RATE + e2e_time = 0 if e2e_time is None else e2e_time + + if e2e_time > rollover_time * self._PRESCALERS[3]: + raise ValueError("Timegap too big for four channel mode.") + elif e2e_time > rollover_time * self._PRESCALERS[2]: + self._prescaler = 3 + elif e2e_time > rollover_time * self._PRESCALERS[1]: + self._prescaler = 2 + elif e2e_time > rollover_time: + self._prescaler = 1 + else: + self._prescaler = 0 + + self._device.send_byte(CP.TIMING) + self._device.send_byte(CP.START_FOUR_CHAN_LA) + self._device.send_int(CP.MAX_SAMPLES // 4) + self._device.send_int( + self._channels["LA1"]._logic_mode + | (self._channels["LA2"]._logic_mode << 4) + | (self._channels["LA3"]._logic_mode << 8) + | (self._channels["LA4"]._logic_mode << 12) + ) + self._device.send_byte(self._prescaler) + + try: + trigger = { + 0: 4, + 1: 8, + 2: 16, + }[self._trigger_channel.number] | self._trigger_mode + except KeyError: + e = "Triggering is only possible on LA1, LA2, or LA3." + raise NotImplementedError(e) + + self._device.send_byte(trigger) + self._device.get_ack() + + def fetch_data(self) -> List[np.ndarray]: + """Collect captured logic events. + + It is possible to call fetch_data while the capture routine is still running. + Doing so will not interrupt the capture process. In multi-channel mode, the + number of timestamps may differ between channels when fetch_data is called + before the capture is complete. + + Returns + ------- + data : list of numpy.ndarray + List of numpy.ndarrays holding timestamps in microseconds when logic events + were detected. The length of the list is equal to the number of channels + that were used to capture events, and each list element corresponds to a + channel. + """ + counter_values = [] + channels = list( + OrderedDict.fromkeys( + [self._channel_one_map, self._channel_two_map, "LA3", "LA4"] + ) + ) + for c in channels: + c = self._channels[c] + + if c.events_in_buffer: + if c.datatype == "long": + raw_timestamps = self._fetch_long(c) + else: + raw_timestamps = self._fetch_int(c) + counter_values.append(raw_timestamps) + + prescaler = [1 / 64, 1 / 8, 1.0, 4.0][self._prescaler] + + timestamps = [] + capture_delay = self._CAPTURE_DELAY if self._prescaler == 0 else 0 + for e, cv in enumerate(counter_values): + adjusted_counter = cv + e * capture_delay + timestamps.append(adjusted_counter * prescaler) + + return timestamps + + def _fetch_long(self, channel: DigitalInput) -> np.ndarray: + self._device.send_byte(CP.TIMING) + self._device.send_byte(CP.FETCH_LONG_DMA_DATA) + self._device.send_int(CP.MAX_SAMPLES // 4) + self._device.send_byte(channel.buffer_idx // 5000) + raw = self._device.read(CP.MAX_SAMPLES) + self._device.get_ack() + + raw_timestamps = [ + CP.Integer.unpack(raw[a * 4 : a * 4 + 4])[0] + for a in range(CP.MAX_SAMPLES // 4) + ] + raw_timestamps = np.array(raw_timestamps) + raw_timestamps = np.trim_zeros(raw_timestamps, "b") + pretrim = len(raw_timestamps) + raw_timestamps = np.trim_zeros(raw_timestamps, "f") + self._trimmed = pretrim - len(raw_timestamps) + + return raw_timestamps + + def _fetch_int(self, channel: DigitalInput) -> np.ndarray: + raw_timestamps = self.fetch_buffer(CP.MAX_SAMPLES // 4, channel.buffer_idx) + raw_timestamps = np.array(raw_timestamps) + raw_timestamps = np.trim_zeros(raw_timestamps, "b") + pretrim = len(raw_timestamps) + raw_timestamps = np.trim_zeros(raw_timestamps, "f") + self._trimmed = pretrim - len(raw_timestamps) + + for i, diff in enumerate(np.diff(raw_timestamps)): + if diff <= 0: # Counter has rolled over. + raw_timestamps[i + 1 :] += 2**16 - 1 + + return raw_timestamps + + def get_progress(self) -> int: + """Return the number of captured events per channel held in the buffer. + + Returns + ------- + progress : int + Number of events held in buffer. If multiple channels have events + in buffer, the lowest value will be returned. + """ + active_channels = [] + a = 0 + for c in self._channels.values(): + if c.events_in_buffer: + active_channels.append(a * (1 if c.datatype == "int" else 2)) + a += 1 + + p = CP.MAX_SAMPLES // 4 + progress = self._get_initial_states_and_progress()[0] + for a in active_channels: + p = min(progress[a], p) + + return p + + def get_initial_states(self) -> Dict[str, bool]: + """Return the initial state of each digital input at the beginning of capture. + + Returns + ------- + dict of four str: bool pairs + Dictionary containing pairs of channel names and the corresponding initial + state, e.g. {'LA1': True, 'LA2': True, 'LA3': True, 'LA4': False}. + True means HIGH, False means LOW. + """ + before, after = self._get_initial_states_and_progress()[1:] + initial_states = { + "LA1": (before & 1 != 0), + "LA2": (before & 2 != 0), + "LA3": (before & 4 != 0), + "LA4": (before & 8 != 0), + } + + if before != after: + disagreements = before ^ after + uncertain_states = [ + disagreements & 1 != 0, + disagreements & 2 != 0, + disagreements & 4 != 0, + disagreements & 8 != 0, + ] + timestamps = self.fetch_data() + + if len(timestamps) == 1: + # One channel mode does not record states after start. + return initial_states + + channels = ["LA1", "LA2", "LA3", "LA4"] + for i, state in enumerate(uncertain_states[: len(timestamps)]): + if state: + if timestamps[i][0] > 0.1: # µs + # States captured immediately after start are usually + # better than immediately before, except when an event + # was captures within ~100 ns of start. + initial_states[channels[i]] = not initial_states[channels[i]] + + return initial_states + + def get_xy( + self, timestamps: List[np.ndarray], initial_states: Dict[str, bool] = None + ) -> List[np.ndarray]: + """Turn timestamps into plottable data. + + Parameters + ---------- + timestamps : list of numpy.ndarray + List of timestamps as returned by :meth:`capture` or + :meth:`fetch_data`. + initial_states : dict of str, bool + Initial states of digital inputs at beginning of capture, as + returned by :meth:`get_initial_states`. If no additional capture + calls have been issued before calling :meth:`get_xy`, this can be + omitted. + + Returns + ------- + list of numpy.ndarray + List of x, y pairs suitable for plotting using, for example, + matplotlib.pyplot.plot. One pair of x and y values is returned for + each list of timestamps given as input. + """ + xy = [] + initial_states = ( + initial_states if initial_states is not None else self.get_initial_states() + ) + + for e, c in enumerate( + [self._channel_one_map, self._channel_two_map, "LA3", "LA4"][ + : len(timestamps) + ] + ): + c = self._channels[c] + if c.events_in_buffer: + x, y = c._get_xy(initial_states[c.name], timestamps[e]) + xy.append(x) + xy.append(y) + + return xy + + def _get_initial_states_and_progress(self) -> Tuple[int, int, int]: + self._device.send_byte(CP.TIMING) + self._device.send_byte(CP.GET_INITIAL_DIGITAL_STATES) + initial = self._device.get_int() + progress = [0, 0, 0, 0] + progress[0] = (self._device.get_int() - initial) // 2 + progress[1] = (self._device.get_int() - initial) // 2 - CP.MAX_SAMPLES // 4 + progress[2] = (self._device.get_int() - initial) // 2 - 2 * CP.MAX_SAMPLES // 4 + progress[3] = (self._device.get_int() - initial) // 2 - 3 * CP.MAX_SAMPLES // 4 + states_immediately_before_start = self._device.get_byte() + states_immediately_after_start = self._device.get_byte() + self._device.get_ack() + + for e, i in enumerate(progress): + if i == 0: + progress[e] = CP.MAX_SAMPLES // 4 + elif i < 0: + progress[e] = 0 + + return progress, states_immediately_before_start, states_immediately_after_start + + def configure_trigger(self, trigger_channel: str, trigger_mode: str): + """Set up trigger channel and trigger condition. + + Parameters + ---------- + trigger_channel : {"LA1", "LA2", "LA3", "LA4"} + The digital input on which to trigger. + trigger_mode : {"disabled", "falling", "rising"} + The type of logic level change on which to trigger. + """ + self.trigger_channel = trigger_channel + self._trigger_channel = self._channels[trigger_channel] + self.trigger_mode = trigger_mode + + def _configure_trigger(self, channels: int): + # For some reason firmware uses different values for trigger_mode + # depending on number of channels. + if channels == 1: + self._trigger_mode = { + "disabled": 0, + "any": 1, + "falling": 2, + "rising": 3, + "four rising": 4, + "sixteen rising": 5, + }[self.trigger_mode] + elif channels == 2: + self._trigger_mode = { + "disabled": 0, + "falling": 3, + "rising": 1, + }[self.trigger_mode] + elif channels == 4: + self._trigger_mode = { + "disabled": 0, + "falling": 1, + "rising": 3, + }[self.trigger_mode] + + def stop(self): + """Stop a running :meth:`capture` function.""" + self._device.send_byte(CP.TIMING) + self._device.send_byte(CP.STOP_LA) + self._device.get_ack() + + def get_states(self) -> Dict[str, bool]: + """Return the current state of the digital inputs. + + Returns + ------- + dict of four str: bool pairs + Dictionary containing pairs of channel names and the corresponding + current state, e.g. {'LA1': True, 'LA2': True, 'LA3': True, + 'LA4': False}. True means HIGH, False means LOW. + """ + self._device.send_byte(CP.DIN) + self._device.send_byte(CP.GET_STATES) + s = self._device.get_byte() + self._device.get_ack() + return { + "LA1": (s & 1 != 0), + "LA2": (s & 2 != 0), + "LA3": (s & 4 != 0), + "LA4": (s & 8 != 0), + } + + def count_pulses( + self, channel: str = "FRQ", interval: float = 1, block: bool = True + ) -> Union[int, None]: + """Count pulses on a digital input. + + The counter is 16 bits, so it will roll over after 65535 pulses. This + method can be used at the same time as the oscilloscope. + + Parameters + ---------- + channel : {"LA1", "LA2", "LA3", "LA4", "FRQ"}, optional + Digital input on which to count pulses. The default value is "FRQ". + interval : float, optional + Time in seconds during which to count pulses. The default value is + 1 second. + block : bool, optional + Whether to block while counting pulses or not. If False, this + method will return None, and the pulses must be manually fetched + using :meth:`fetch_pulse_count`. Additionally, the interval + argument has no meaning if block is False; the counter will keep + counting even after the interval time has expired. The default + value is True. + + Returns + ------- + Union[int, None] + Number of pulses counted during the interval, or None if block is + False. + """ + self._reset_prescaler() + self._device.send_byte(CP.COMMON) + self._device.send_byte(CP.START_COUNTING) + self._device.send_byte(self._channels[channel].number) + self._device.get_ack() + + if block: + time.sleep(interval) + else: + return + + return self.fetch_pulse_count() + + def fetch_pulse_count(self) -> int: + """Return the number of pulses counted since calling :meth:`count_pulses`. + + Returns + ------- + int + Number of pulses counted since calling :meth:`count_pulses`. + """ + self._device.send_byte(CP.COMMON) + self._device.send_byte(CP.FETCH_COUNT) + count = self._device.get_int() + self._device.get_ack() + return count + + def _reset_prescaler(self): + self._device.send_byte(CP.TIMING) + self._device.send_byte(CP.START_FOUR_CHAN_LA) + self._device.send_int(0) + self._device.send_int(0) + self._device.send_byte(0) + self._device.send_byte(0) + self._device.get_ack() + self.stop() + self._prescaler = 0 + + def _invalidate_buffer(self): + for c in self._channels.values(): + c.events_in_buffer = 0 + c.buffer_idx = None diff --git a/pslab/instrument/multimeter.py b/pslab/instrument/multimeter.py new file mode 100644 index 00000000..db7f6d53 --- /dev/null +++ b/pslab/instrument/multimeter.py @@ -0,0 +1,250 @@ +"""The PSLab's multimeter can measure voltage, resistance, and capacitance.""" + +import time +from typing import Tuple + +import numpy as np +from scipy.optimize import curve_fit + +import pslab.protocol as CP +from pslab.connection import ConnectionHandler +from pslab.instrument.analog import GAIN_VALUES, INPUT_RANGES +from pslab.instrument.oscilloscope import Oscilloscope + +_MICROSECONDS = 1e-6 + + +class Multimeter(Oscilloscope): + """Measure voltage, resistance and capacitance. + + Parameters + ---------- + device : Handler + Serial interface for communicating with the PSLab device. If not + provided, a new one will be created. + """ + + _CURRENTS = [5.5e-4, 5.5e-7, 5.5e-6, 5.5e-5] + _CURRENTS_RANGES = [1, 2, 3, 0] # Smallest first, + _RC_RESISTANCE = 1e4 + _CAPACITOR_CHARGED_VOLTAGE = 0.9 * max(INPUT_RANGES["CAP"]) + _CAPACITOR_DISCHARGED_VOLTAGE = 0.01 * max(INPUT_RANGES["CAP"]) + + def __init__(self, device: ConnectionHandler | None = None): + self._stray_capacitance = 46e-12 + super().__init__(device) + + def measure_resistance(self) -> float: + """Measure the resistance of a resistor connected between RES and GND. + + Returns + ------- + resistance : float + Resistance in ohm. + """ + voltage = self.measure_voltage("RES") + resolution = max(INPUT_RANGES["RES"]) / ( + 2 ** self._channels["RES"].resolution - 1 + ) + + if voltage >= max(INPUT_RANGES["RES"]) - resolution: + return np.inf + + pull_up_resistance = 5.1e3 + current = (INPUT_RANGES["RES"][1] - voltage) / pull_up_resistance + return voltage / current + + def measure_voltage(self, channel: str = "VOL") -> float: + """Measure the voltage on the selected channel. + + Parameters + ---------- + channel : {"CH1", "CH2", "CH3", "MIC", "CAP", "RES", "VOL"}, optional + The name of the analog input on which to measure the voltage. The + default channel is VOL. + + Returns + ------- + voltage : float + Voltage in volts. + """ + self._voltmeter_autorange(channel) + return self._measure_voltage(channel) + + def _measure_voltage(self, channel: str) -> float: + self._channels[channel].resolution = 12 + scale = self._channels[channel].scale + chosa = self._channels[channel].chosa + self._device.send_byte(CP.ADC) + self._device.send_byte(CP.GET_VOLTAGE_SUMMED) + self._device.send_byte(chosa) + raw_voltage_sum = self._device.get_int() # Sum of 16 samples. + self._device.get_ack() + raw_voltage_mean = round(raw_voltage_sum / 16) + voltage = scale(raw_voltage_mean) + return voltage + + def _voltmeter_autorange(self, channel: str) -> float: + if channel in ("CH1", "CH2"): + self._set_gain(channel, 1) # Reset gain. + voltage = self._measure_voltage(channel) + + for gain in GAIN_VALUES[::-1]: + rng = max(INPUT_RANGES[channel]) / gain + if abs(voltage) < rng: + break + + self._set_gain(channel, gain) + + return rng + else: + return max(INPUT_RANGES[channel]) + + def calibrate_capacitance(self): + """Calibrate stray capacitance. + + Correctly calibrated stray capacitance is important when measuring + small capacitors (picofarad range). + + Stray capacitace should be recalibrated if external wiring is connected + to the CAP pin. + """ + for charge_time in np.unique(np.int16(np.logspace(2, 3))): + self._discharge_capacitor() + voltage, capacitance = self._measure_capacitance(1, 0, charge_time) + if voltage >= self._CAPACITOR_CHARGED_VOLTAGE: + break + self._stray_capacitance += capacitance + + def measure_capacitance(self) -> float: + """Measure the capacitance of a capacitor connected between CAP and GND. + + Returns + ------- + capacitance : float + Capacitance in Farad. + """ + for current_range in self._CURRENTS_RANGES: + charge_time = 10 + for _ in range(10): + if charge_time > 50000: + break # Increase current. + voltage, capacitance = self._measure_capacitance( + current_range, 0, charge_time + ) + if 0.98 < voltage / self._CAPACITOR_CHARGED_VOLTAGE < 1.02: + return capacitance + charge_time = int( + charge_time * self._CAPACITOR_CHARGED_VOLTAGE / voltage + ) + + # Capacitor too big, use alternative method. + return self._measure_rc_capacitance() + + def _set_cap(self, state, charge_time): + """Set CAP HIGH or LOW.""" + self._device.send_byte(CP.ADC) + self._device.send_byte(CP.SET_CAP) + self._device.send_byte(state) + self._device.send_int(charge_time) + self._device.get_ack() + + def _discharge_capacitor( + self, discharge_time: int = 50000, timeout: float = 1 + ) -> float: + start_time = time.time() + voltage = previous_voltage = self.measure_voltage("CAP") + + while voltage > self._CAPACITOR_DISCHARGED_VOLTAGE: + self._set_cap(0, discharge_time) + voltage = self.measure_voltage("CAP") + + if abs(previous_voltage - voltage) < self._CAPACITOR_DISCHARGED_VOLTAGE: + break + + previous_voltage = voltage + + if time.time() - start_time > timeout: + break + + return voltage + + def _measure_capacitance( + self, current_range: int, trim: int, charge_time: int + ) -> Tuple[float, float]: + self._discharge_capacitor() + self._channels["CAP"].resolution = 12 + self._device.send_byte(CP.COMMON) + self._device.send_byte(CP.GET_CAPACITANCE) + self._device.send_byte(current_range) + + if trim < 0: + self._device.send_byte(int(31 - abs(trim) / 2) | 32) + else: + self._device.send_byte(int(trim / 2)) + + self._device.send_int(charge_time) + time.sleep(charge_time * _MICROSECONDS) + raw_voltage = self._device.get_int() + voltage = self._channels["CAP"].scale(raw_voltage) + self._device.get_ack() + charge_current = self._CURRENTS[current_range] * (100 + trim) / 100 + + if voltage: + capacitance = ( + charge_current * charge_time * _MICROSECONDS / voltage + - self._stray_capacitance + ) + else: + capacitance = 0 + + return voltage, capacitance + + def _measure_rc_capacitance(self) -> float: + """Measure the capacitance by discharge through a 10K resistor.""" + (x,) = self.capture("CAP", CP.MAX_SAMPLES, 10, block=False) + x *= _MICROSECONDS + self._set_cap(1, 50000) # charge + self._set_cap(0, 50000) # discharge + (y,) = self.fetch_data() + + if y.max() >= self._CAPACITOR_CHARGED_VOLTAGE: + discharge_start = np.where(y >= self._CAPACITOR_CHARGED_VOLTAGE)[0][-1] + else: + discharge_start = np.where(y == y.max())[0][-1] + + x = x[discharge_start:] + y = y[discharge_start:] + + # CAP floats for a brief period of time (~500 µs) between being set + # HIGH until it is set LOW. This data is not useful and should be + # discarded. When CAP is set LOW the voltage declines sharply, which + # manifests as a negative peak in the time derivative. + dydx = np.diff(y) / np.diff(x) + cap_low = np.where(dydx == dydx.min())[0][0] + x = x[cap_low:] + y = y[cap_low:] + + # Discard data after the voltage reaches zero (improves fit). + try: + v_zero = np.where(y == 0)[0][0] + x = x[:v_zero] + y = y[:v_zero] + except IndexError: + pass + + # Remove time offset. + x -= x[0] + + def discharging_capacitor_voltage( + x: np.ndarray, v_init: float, rc_time_constant: float + ) -> np.ndarray: + return v_init * np.exp(-x / rc_time_constant) + + # Solve discharging_capacitor_voltage for rc_time_constant. + rc_time_constant_guess = (-x[1:] / np.log(y[1:] / y[0])).mean() + guess = [y[0], rc_time_constant_guess] + popt, _ = curve_fit(discharging_capacitor_voltage, x, y, guess) + rc_time_constant = popt[1] + rc_capacitance = rc_time_constant / self._RC_RESISTANCE + return rc_capacitance diff --git a/pslab/instrument/oscilloscope.py b/pslab/instrument/oscilloscope.py new file mode 100644 index 00000000..b8d3e30c --- /dev/null +++ b/pslab/instrument/oscilloscope.py @@ -0,0 +1,411 @@ +"""Classes and functions related to the PSLab's oscilloscope instrument. + +Example +------- +>>> from pslab import Oscilloscope +>>> scope = Oscilloscope() +>>> x, y1, y2, y3 = scope.capture(channels=3, samples=1600, timegap=2) +""" + +import time +from typing import List, Tuple, Union + +import numpy as np + +import pslab.protocol as CP +from pslab.bus.spi import SPIMaster +from pslab.connection import ConnectionHandler, autoconnect +from pslab.instrument.analog import ANALOG_CHANNELS, AnalogInput, GAIN_VALUES +from pslab.instrument.buffer import ADCBufferMixin + + +class Oscilloscope(ADCBufferMixin): + """Capture varying voltage signals on up to four channels simultaneously. + + Parameters + ---------- + device : :class:`SerialHandler`, optional + Serial interface for communicating with the PSLab device. If not + provided, a new one will be created. + """ + + _CH234 = ["CH2", "CH3", "MIC"] + + def __init__(self, device: ConnectionHandler | None = None): + self._device = device if device is not None else autoconnect() + self._channels = {a: AnalogInput(a) for a in ANALOG_CHANNELS} + self._channel_one_map = "CH1" + self._trigger_voltage = None + self._trigger_enabled = False + self._trigger_channel = "CH1" + self._set_gain("CH1", 1) + self._set_gain("CH2", 1) + + def capture( + self, + channels: int, + samples: int, + timegap: float, + trigger: Union[float, bool] = None, + trigger_channel: str = None, + block: bool = True, + ) -> List[np.ndarray]: + """Capture an oscilloscope trace from the specified input channels. + + Parameters + ---------- + channels : str or {1, 2, 3, 4} + Number of channels to sample from simultaneously, or the name + (CH1, CH2, CH3, MIC, CAP, RES, VOL) of a single channel to sample + from. If channel is an integer, the oscilloscope will sample the + first one, two, three, or four channels in the aforementioned list. + samples : int + Number of samples to fetch. Maximum 10000 divided by number of + channels. + timegap : float + Time gap between samples in microseconds. Will be rounded to the + closest 1 / 8 µs. The minimum time gap depends on the type of + measurement: + + +--------------+------------+----------+------------+ + | Simultaneous | No trigger | Trigger | No trigger | + | channels | (10-bit) | (10-bit) | (12-bit) | + +==============+============+==========+============+ + | 1 | 0.5 µs | 0.75 µs | 1 µs | + +--------------+------------+----------+------------+ + | 2 | 0.875 µs | 0.875 µs | N/A | + +--------------+------------+----------+------------+ + | 4 | 1.75 µs | 1.75 µs | N/A | + +--------------+------------+----------+------------+ + + Sample resolution is set automatically based on the above + limitations; i.e. to get 12-bit samples only one channel may be + sampled, there must be no active trigger, and the time gap must be + 1 µs or greater. + trigger : float or bool, optional + Voltage at which to trigger sampling. Triggering is disabled by + default. Trigger settings persist between calls; disable by setting + trigger=False. + trigger_channel : str, optional + Wait for the voltage level on this channel to cross the trigger + value before sampling. Same as the first sampled channel by + default. + block : bool, optional + Whether or not to block while sampling. If False, return timestamps + immediately without waiting for corresponding voltages. User is + responsible for waiting an appropriate amount of time before + collecting samples with :meth:`fetch_data`. True by default. + + Example + ------- + >>> from pslab import Oscilloscope + >>> scope = Oscilloscope() + >>> x, y = scope.capture(1, 3200, 1) + + Returns + ------- + list of numpy.ndarray + List of numpy arrays holding timestamps and corresponding voltages. + In non-blocking mode, only timestamps are returned; voltages must + be fetched using :meth:`fetch_data`. + + Raises + ------ + ValueError + If :channels: is not 1, 2, 3, 4, or one of CH1, CH2, CH3, MIC, CAP, + RES, VOL, or + :samples: > 10000 / :channels:, or + :timegap: is too low. + """ + if isinstance(channels, str): + self._channel_one_map = channels + channels = 1 + + if trigger_channel is None: + self._trigger_channel = self._channel_one_map + else: + self._trigger_channel = trigger_channel + + if trigger is False: + self._trigger_enabled = False + elif trigger is not None: + if trigger != self._trigger_voltage: + self.configure_trigger(voltage=trigger) + + self._check_args(channels, samples, timegap) + timegap = int(timegap * 8) / 8 + + for channel in ("CH1", "CH2"): + # Reset gain (another instance could have changed it). + self._set_gain(channel, self._channels[channel].gain) + + self._capture(channels, samples, timegap) + x = [timegap * np.arange(samples)] + + if block: + time.sleep(1e-6 * samples * timegap) + + while not self.progress()[0]: + pass + + # Discard MIC if user requested three channels. + y = self.fetch_data()[:channels] + + return x + y + else: + return x + + def _check_args(self, channels: int, samples: int, timegap: float): + if channels not in (1, 2, 3, 4): + raise ValueError("Number of channels to sample must be 1, 2, 3, or 4.") + + max_samples = CP.MAX_SAMPLES // channels + if not 0 < samples <= max_samples: + e1 = f"Cannot collect more than {max_samples} when sampling from " + e2 = f"{channels} channels." + raise ValueError(e1 + e2) + + min_timegap = self._lookup_mininum_timegap(channels) + if timegap < min_timegap: + raise ValueError(f"timegap must be at least {min_timegap}.") + + if self._channel_one_map not in self._channels: + e1 = f"{self._channel_one_map} is not a valid channel. " + e2 = f"Valid channels are {list(self._channels.keys())}." + raise ValueError(e1 + e2) + + def _lookup_mininum_timegap(self, channels: int) -> float: + channels_idx = { + 1: 0, + 2: 1, + 3: 2, + 4: 2, + } + min_timegaps = [[0.5, 0.75], [0.875, 0.875], [1.75, 1.75]] + + return min_timegaps[channels_idx[channels]][self.trigger_enabled] + + def _capture(self, channels: int, samples: int, timegap: float): + self._invalidate_buffer() + chosa = self._channels[self._channel_one_map].chosa + self._channels[self._channel_one_map].resolution = 10 + self._device.send_byte(CP.ADC) + + CH123SA = 0 # TODO what is this? + chosa = self._channels[self._channel_one_map].chosa + self._channels[self._channel_one_map].samples_in_buffer = samples + self._channels[self._channel_one_map].buffer_idx = 0 + if channels == 1: + if self.trigger_enabled: + self._device.send_byte(CP.CAPTURE_ONE) + self._device.send_byte(chosa | 0x80) # Trigger + elif timegap >= 1: + self._channels[self._channel_one_map].resolution = 12 + self._device.send_byte(CP.CAPTURE_DMASPEED) + self._device.send_byte(chosa | 0x80) # 12-bit mode + else: + self._device.send_byte(CP.CAPTURE_DMASPEED) + self._device.send_byte(chosa) # 10-bit mode + elif channels == 2: + self._channels["CH2"].resolution = 10 + self._channels["CH2"].samples_in_buffer = samples + self._channels["CH2"].buffer_idx = 1 * samples + self._device.send_byte(CP.CAPTURE_TWO) + self._device.send_byte(chosa | (0x80 * self.trigger_enabled)) + else: + for e, c in enumerate(self._CH234): + self._channels[c].resolution = 10 + self._channels[c].samples_in_buffer = samples + self._channels[c].buffer_idx = (e + 1) * samples + self._device.send_byte(CP.CAPTURE_FOUR) + self._device.send_byte( + chosa | (CH123SA << 4) | (0x80 * self.trigger_enabled) + ) + + self._device.send_int(samples) + self._device.send_int(int(timegap * 8)) # 8 MHz clock + self._device.get_ack() + + def _invalidate_buffer(self): + for c in self._channels.values(): + c.samples_in_buffer = 0 + c.buffer_idx = None + + def fetch_data(self) -> List[np.ndarray]: + """Fetch captured samples. + + Example + ------- + >>> from pslab import Oscilloscope + >>> scope = Oscilloscope() + >>> scope.capture_nonblocking(channels=2, samples=1600, timegap=1) + >>> y1, y2 = scope.fetch_data() + + Returns + ------- + list of numpy.ndarray + List of numpy arrays holding sampled voltages. + """ + channels = [c for c in self._channels.values() if c.samples_in_buffer] + data = [None] * len(channels) + + for i, channel in enumerate(channels): + samples = channel.samples_in_buffer + data[i] = self.fetch_buffer(samples, channel.buffer_idx) + data[i] = channel.scale(np.array(data[i])) + + return data + + def progress(self) -> Tuple[bool, int]: + """Return the status of a capture call. + + Returns + ------- + bool, int + A boolean indicating whether the capture is complete, followed by + the number of samples currently held in the buffer. + """ + self._device.send_byte(CP.ADC) + self._device.send_byte(CP.GET_CAPTURE_STATUS) + conversion_done = self._device.get_byte() + samples = self._device.get_int() + self._device.get_ack() + + return bool(conversion_done), samples + + def configure_trigger( + self, + channel: str = None, + voltage: float = 0, + prescaler: int = 0, + enable: bool = True, + ): + """Configure trigger parameters for 10-bit capture routines. + + The capture routines will wait until a rising edge of the input signal + crosses the specified level. The trigger will timeout within 8 ms, and + capture will start regardless. + + To disable the trigger after configuration, set the trigger_enabled + attribute of the Oscilloscope instance to False. + + Parameters + ---------- + channel : {'CH1', 'CH2', 'CH3', 'MIC', 'CAP', 'RES', 'VOL'}, optional + The name of the trigger channel. First sampled channel by default. + voltage : float, optional + The trigger voltage in volts. The default value is 0. + prescaler : int, optional + The default value is 0. + enable_trigger : bool, optional + Set this to False to disable the trigger. True by default. + + Examples + -------- + >>> from pslab import Oscilloscope + >>> scope = Oscilloscope() + >>> scope.configure_trigger(channel='CH1', voltage=1.1) + >>> x, y = scope.capture(channels=1, samples=800, timegap=2) + >>> diff = abs(y[0] - 1.1) # Should be small unless a timeout occurred. + + Raises + ------ + TypeError + If the trigger channel is set to a channel which cannot be sampled. + """ + if enable is False: + self._trigger_enabled = False + return + + if channel is not None: + self._trigger_channel = channel + + if self.trigger_channel == self._channel_one_map: + channel = 0 + elif self.trigger_channel in self._CH234: + channel = self._CH234.index(self.trigger_channel) + 1 + else: + raise TypeError(f"Cannot trigger on {self.trigger_channel}.") + + self._device.send_byte(CP.ADC) + self._device.send_byte(CP.CONFIGURE_TRIGGER) + # Trigger channel (4lsb) , trigger timeout prescaler (4msb) + self._device.send_byte((prescaler << 4) | (1 << channel)) # TODO prescaler? + level = self._channels[self.trigger_channel].unscale(voltage) + self._device.send_int(level) + self._device.get_ack() + self._trigger_enabled = True + + @property + def trigger_enabled(self) -> bool: + """bool: Wait for trigger condition before capture start.""" + return self._trigger_enabled + + @property + def trigger_channel(self) -> str: + """str: Name of channel to trigger on.""" + return self._trigger_channel + + @property + def trigger_voltage(self) -> float: + """float: Trigger when voltage crosses this value.""" + return self._trigger_voltage + + def select_range(self, channel: str, voltage_range: Union[int, float]): + """Set appropriate gain automatically. + + Setting the right voltage range will result in better resolution. + + Parameters + ---------- + channel : {'CH1', 'CH2'} + Channel on which to apply gain. + voltage_range : {16, 8, 4, 3, 2, 1.5, 1, .5} + + Examples + -------- + Set 2x gain on CH1. Voltage range ±8 V: + + >>> from pslab import Oscilloscope + >>> scope = Oscilloscope() + >>> scope.select_range('CH1', 8) + """ + ranges = [16, 8, 4, 3, 2, 1.5, 1, 0.5] + gain = GAIN_VALUES[ranges.index(voltage_range)] + self._set_gain(channel, gain) + + def _set_gain(self, channel: str, gain: int): + spi_config_supported = self._check_spi_config() + + if not spi_config_supported: + spi_parameters = SPIMaster.get_parameters() + spi = SPIMaster(self._device) # Initializing SPIMaster will reset config. + + self._channels[channel].gain = gain + pga = self._channels[channel].programmable_gain_amplifier + gain_idx = GAIN_VALUES.index(gain) + self._device.send_byte(CP.ADC) + self._device.send_byte(CP.SET_PGA_GAIN) + self._device.send_byte(pga) + self._device.send_byte(gain_idx) + self._device.get_ack() + + if not spi_config_supported: + spi.set_parameters(*spi_parameters) + + @staticmethod + def _check_spi_config() -> bool: + """Check whether current SPI config is supported by PGA. + + Returns + ------- + bool + Returns True if SPI config is supported by PGA, otherwise False. + """ + # Check the SPI mode. PGA only supports mode 0 and mode 3. + if (SPIMaster._clock_polarity, SPIMaster._clock_phase) not in [(0, 0), (1, 1)]: + return False + if SPIMaster._frequency > 10e6: # PGA only supports max of 10MHz. + return False + + return True diff --git a/pslab/instrument/power_supply.py b/pslab/instrument/power_supply.py new file mode 100644 index 00000000..7d31885e --- /dev/null +++ b/pslab/instrument/power_supply.py @@ -0,0 +1,128 @@ +"""Control voltage and current with the PSLab's PV1, PV2, PV3, and PCS pins. + +Examples +-------- +>>> from pslab import PowerSupply +>>> ps = PowerSupply() +>>> ps.pv1 = 4.5 +>>> ps.pv1 +4.5 + +>>> ps.pcs = 2e-3 +>>> ps.pcs +0.002 +""" + +import pslab.protocol as CP +from pslab.connection import ConnectionHandler, autoconnect + + +class PowerSupply: + """Control the PSLab's programmable voltage and current sources. + + An instance of PowerSupply controls three programmable voltage sources on + pins PV1, PV2, and PV3, as well as a programmable current source on pin + PCS. + + Parameters + ---------- + device : :class:`SerialHandler` + Serial connection with which to communicate with the device. A new + instance will be created automatically if not specified. + """ + + _REFERENCE = 3300 + _PV1_CH = 3 + _PV1_RANGE = (-5, 5) + _PV2_CH = 2 + _PV2_RANGE = (-3.3, 3.3) + _PV3_CH = 1 + _PV3_RANGE = (0, 3.3) + _PCS_CH = 0 + _PCS_RANGE = (3.3e-3, 0) + + def __init__(self, device: ConnectionHandler | None = None): + self._device = device if device is not None else autoconnect() + self._pv1 = None + self._pv2 = None + self._pv3 = None + self._pcs = None + + def _set_power(self, channel, output): + self._device.send_byte(CP.DAC) + self._device.send_byte(CP.SET_POWER) + self._device.send_byte(channel) + self._device.send_int(output) + return self._device.get_ack() + + @staticmethod + def _bound(value, output_range): + return max(min(value, max(output_range)), min(output_range)) + + def _scale(self, value, output_range): + scaled = (value - output_range[0]) / (output_range[1] - output_range[0]) + return int(scaled * self._REFERENCE) + + @property + def pv1(self): + """float: Voltage on PV1; range [-5, 5] V.""" + return self._pv1 + + @pv1.setter + def pv1(self, value: float): + value = self._bound(value, self._PV1_RANGE) + ret = self._set_power(self._PV1_CH, self._scale(value, self._PV1_RANGE)) + self._pv1 = value + return ret + + @property + def pv2(self): + """float: Voltage on PV2; range [-3.3, 3.3] V.""" + return self._pv2 + + @pv2.setter + def pv2(self, value: float): + value = self._bound(value, self._PV2_RANGE) + self._set_power(self._PV2_CH, self._scale(value, self._PV2_RANGE)) + self._pv2 = value + + @property + def pv3(self): + """float: Voltage on PV3; range [0, 3.3] V.""" + return self._pv3 + + @pv3.setter + def pv3(self, value: float): + value = self._bound(value, self._PV3_RANGE) + self._set_power(self._PV3_CH, self._scale(value, self._PV3_RANGE)) + self._pv3 = value + + @property + def pcs(self): + """float: Current on PCS; range [0, 3.3e-3] A. + + Notes + ----- + The maximum available current that can be output by the current source + is dependent on load resistance: + + I_max = 3.3 V / (1 kΩ + R_load) + + For example, the maximum current that can be driven across a 100 Ω load + is 3.3 V / 1.1 kΩ = 3 mA. If the load is 10 kΩ, the maximum current is + only 3.3 V / 11 kΩ = 300 µA. + + Be careful to not set a current higher than available for a given load. + If a current greater than the maximum for a certain load is requested, + the actual current will instead be much smaller. For example, if a + current of 3 mA is requested when connected to a 1 kΩ load, the actual + current will be only a few hundred µA instead of the maximum available + 1.65 mA. + """ + return self._pcs + + @pcs.setter + def pcs(self, value: float): + value = self._bound(value, self._PCS_RANGE) + self._set_power(self._PCS_CH, self._scale(value, self._PCS_RANGE)) + self._pcs = value diff --git a/pslab/instrument/waveform_generator.py b/pslab/instrument/waveform_generator.py new file mode 100644 index 00000000..07c35238 --- /dev/null +++ b/pslab/instrument/waveform_generator.py @@ -0,0 +1,540 @@ +"""Control the PSLab's waveform generators. + +Two types of waveform generator are available: WaveformGenerator and +PWMGenerator. WaveformGenerator can output arbitrary waveforms on pins SI1 and +SI2. PWMGenerator can output square waveforms on pins SQ1, SQ2, SQ3, and SQ4. +""" + +import logging +from typing import Callable, List, Tuple, Union + +import numpy as np + +import pslab.protocol as CP +from pslab.connection import ConnectionHandler, autoconnect +from pslab.instrument.analog import AnalogOutput +from pslab.instrument.digital import DigitalOutput, DIGITAL_OUTPUTS + +logger = logging.getLogger(__name__) + +_PRESCALERS = [1, 8, 64, 256] + + +def _listify(channel, maxlen, *args): + if not isinstance(channel, list): + channel = [channel] + elif len(channel) > maxlen: + raise ValueError("Too many channels.") + + ret = [channel] + + for arg in args: + if isinstance(arg, list): + if len(arg) == len(channel): + ret.append(arg) + else: + raise ValueError("Dimension mismatch.") + else: + ret.append(len(channel) * [arg]) + + return ret + + +def _get_wavelength(frequency: float, table_size: int = 1) -> Tuple[int, int]: + """Get the wavelength of a PWM signal in clock cycles, and the clock prescaler. + + For an analog signal, the wavelength of the underlying PWM signal is equal + to the time gap between two points in the analog waveform. + + Parameters + ---------- + frequency : float + Frequency of the signal in Hz. + table_size : int, optional + Number of points in the analog signal. The default value is 1, which + implies a digital signal. + + Returns + ------- + wavelength : int + Signal wavelength in clock cycles. + prescaler : int + Factor by which the clock rate is reduced, e.g. a prescaler of 64 means + that the clock rate is 1 Mhz instead of the original 64 MHz. + """ + for prescaler in _PRESCALERS: + timegap = int(round(CP.CLOCK_RATE / frequency / prescaler / table_size)) + if 0 < timegap < 2**16: + return timegap, prescaler + + e = ( + "Prescaler out of range. This should not happen." + + " " + + "Please report this bug, including steps to trigger it, to" + + " " + + "https://github.com/fossasia/pslab-python/issues." + ) + raise ValueError(e) + + +class WaveformGenerator: + """Generate analog waveforms on SI1 or SI2. + + Parameters + ---------- + device : :class:`SerialHandler`, optional + Serial connection with which to communicate with the device. A new + instance is created automatically if not specified. + + Examples + -------- + Output the default function (3.3*sin(t)) on SI2 with frequency 2.5 kHz: + + >>> from pslab import WaveformGenerator + >>> wavegen = WaveformGenerator() + >>> wavegen.generate("SI2", 2500) + [2500] + + Output phase shifted sine waves on SI1 and SI2: + + >>> wavegen.generate(["SI1", "SI2"], 1000, 90) + [1000, 1000] + + Reduce the amplitude on SI1: + + >>> import numpy as np + >>> wavegen.load_function("SI1", lambda x: 1.5*np.sin(x), [0, 2*np.pi]) + + Output two superimposed sine waves on SI2: + + >>> wavegen.load_function("SI2", lambda x: 2*np.sin(x) + np.sin(5*x), [0, 2*np.pi]) + """ + + _HIGHRES_TABLE_SIZE = 512 + _LOWRES_TABLE_SIZE = 32 + _LOW_FREQUENCY_WARNING = 20 + _LOW_FREQUENCY_LIMIT = 0.1 + _HIGH_FREQUENCY_WARNING = 5e3 + _HIGHRES_FREQUENCY_LIMIT = 1100 + + def __init__(self, device: ConnectionHandler | None = None): + self._channels = {n: AnalogOutput(n) for n in ("SI1", "SI2")} + self._device = device if device is not None else autoconnect() + + def generate( + self, + channels: Union[str, List[str]], + frequency: Union[float, List[float]], + phase: float = 0, + ) -> List[float]: + """Generate analog waveforms on SI1 or SI2. + + The default waveform is a sine wave with amplitude 3.3 V. Other + waveforms can be set using :meth:`load_function` or :meth:`load_table`. + + Parameters + ---------- + channels : {1, 2} or {'SI1', 'SI2', ['SI1', 'SI2']} + Pin(s) on which to generate a waveform. + frequency : float or list of floats + Frequency in Hz. Can be a list containing two different values when + 'channel' is ['SI1', 'SI2']. Must be greater than 0.1 Hz. For + frequencies below 1 Hz the signal is noticably attenuated by AC + coupling. + phase : float, optional + Phase between waveforms when generating waveforms on both SI1 and + SI2 in degrees. The default is 0. + + Returns + ------- + frequency : List[float] + The actual frequency may differ from the requested value due to + the device-interal integer representation. The actual frequency is + therefore returned as a list. The length of the list is equal to + the number of channels used to generate waveforms. + """ + if isinstance(channels, int): + channels = ["SI1", "SI2"][:channels] + + channels, frequency = _listify(channels, 2, frequency) + table_size = len(channels) * [None] + timegap = len(channels) * [None] + prescaler = len(channels) * [None] + + for i, (chan, freq) in enumerate(zip(channels, frequency)): + if freq < self._LOW_FREQUENCY_WARNING: + w = ( + f"Frequencies under {self._LOW_FREQUENCY_WARNING} Hz have" + + " " + + "reduced amplitude due to AC coupling restrictions." + ) + logger.warning(w) + elif freq > self._HIGH_FREQUENCY_WARNING: + w = ( + f"Frequencies above {self._HIGH_FREQUENCY_WARNING} Hz have" + + " " + + "reduced amplitude." + ) + logger.warning(w) + + table_size[i] = self._get_table_size(freq) + timegap[i], prescaler[i] = _get_wavelength(freq, table_size[i]) + frequency[i] = CP.CLOCK_RATE / timegap[i] / prescaler[i] / table_size[i] + self._channels[chan].frequency = frequency[i] + + if len(channels) == 1: + self._output_one(channels, table_size, prescaler, timegap) + else: + self._output_two(table_size, phase, prescaler, timegap) + + return frequency + + def _output_one(self, channel: str, table_size: int, prescaler: int, timegap: int): + self._device.send_byte(CP.WAVEGEN) + secondary_cmd = CP.SET_SINE1 if channel[0] == "SI1" else CP.SET_SINE2 + self._device.send_byte(secondary_cmd) + # Use larger table for low frequencies. + highres = table_size[0] == self._HIGHRES_TABLE_SIZE + self._device.send_byte(highres | (_PRESCALERS.index(prescaler[0]) << 1)) + self._device.send_int(timegap[0] - 1) + self._device.get_ack() + + def _output_two(self, table_size: int, phase: float, prescaler: int, timegap: int): + self._device.send_byte(CP.WAVEGEN) + phase_coarse = int(table_size[1] * phase / 360) + phase_fine = int( + timegap[1] + * (phase - phase_coarse * 360 / table_size[1]) + / (360 / table_size[1]) + ) + self._device.send_byte(CP.SET_BOTH_WG) + self._device.send_int(timegap[0] - 1) + self._device.send_int(timegap[1] - 1) + self._device.send_int(phase_coarse) # Table position for phase adjust. + self._device.send_int(phase_fine) # Timer delay / fine phase adjust. + highres = [t == self._HIGHRES_TABLE_SIZE for t in table_size] + self._device.send_byte( + (_PRESCALERS.index(prescaler[1]) << 4) + | (_PRESCALERS.index(prescaler[0]) << 2) + | (highres[1] << 1) + | (highres[0]) + ) + self._device.get_ack() + + def _get_table_size(self, frequency: float) -> int: + if frequency < self._LOW_FREQUENCY_LIMIT: + e = f"Frequency must be greater than {self._LOW_FREQUENCY_LIMIT} Hz." + raise ValueError(e) + elif frequency < self._HIGHRES_FREQUENCY_LIMIT: + table_size = self._HIGHRES_TABLE_SIZE + else: + table_size = self._LOWRES_TABLE_SIZE + + return table_size + + def load_function( + self, channel: str, function: Union[str, Callable], span: List[float] = None + ): + """Load a custom waveform function. + + Parameters + ---------- + channel : {'SI1', 'SI2'} + The output pin on which to generate the waveform. + function : Union[str, Callable] + A callable function which takes a numpy.ndarray of x values as + input and returns a corresponding numpy.ndarray of y values. The + y-values should be voltages in V, and should lie between -3.3 V + and 3.3 V. Values outside this range will be clipped. + + Alternatively, 'function' can be a string literal 'sine' or 'tria', + for a sine wave or triangle wave with amplitude 3.3 V. + span : List[float], optional + The minimum and maximum x values between which to evaluate + 'function'. Should typically correspond to one period. Omit if + 'function' is 'sine' or 'tria'. + """ + if function == "sine": + + def sine(x): + return AnalogOutput.RANGE[1] * np.sin(x) + + function = sine + span = [0, 2 * np.pi] + self._channels[channel].wavetype = "sine" + elif function == "tria": + + def tria(x): + return AnalogOutput.RANGE[1] * (abs(x % 4 - 2) - 1) + + function = tria + span = [0, 4] + self._channels[channel].wavetype = "tria" + else: + self._channels[channel].wavetype = "custom" + + x = np.arange(span[0], span[1], (span[1] - span[0]) / 512) + y = function(x) + self._load_table( + channel=channel, points=y, mode=self._channels[channel].wavetype + ) + + def load_table(self, channel: str, points: np.ndarray): + """Load a custom waveform as a table. + + Parameters + ---------- + channel : {'SI1', 'SI2'} + The output pin on which to generate the waveform. + points : np.ndarray + Array of voltage values which make up the waveform. Array length + must be 512. Values outside the range -3.3 V to 3.3 V will be + clipped. + """ + self._load_table(channel, points, "custom") + + def _load_table(self, channel, points, mode="custom"): + self._channels[channel].wavetype = mode + self._channels[channel].waveform_table = points + logger.info(f"Reloaded waveform table for {channel}: {mode}.") + self._device.send_byte(CP.WAVEGEN) + + if self._channels[channel].name == "SI1": + self._device.send_byte(CP.LOAD_WAVEFORM1) + else: + self._device.send_byte(CP.LOAD_WAVEFORM2) + + for val in self._channels[channel].waveform_table: + self._device.send_int(val) + for val in self._channels[channel].lowres_waveform_table: + self._device.send_byte(val) + + self._device.get_ack() + + +class PWMGenerator: + """Generate PWM signals on SQ1, SQ2, SQ3, and SQ4. + + Parameters + ---------- + device : :class:`SerialHandler` + Serial connection with which to communicate with the device. A new + instance will be created automatically if not specified. + + Examples + -------- + Output 40 kHz PWM signals on SQ1 and SQ3 phase shifted by 50%. Set the duty + cycles to 75% and 33%, respectivelly: + + >>> from pslab import PWMGenerator + >>> pwmgen = PWMGenerator() + >>> pwmgen.generate(["SQ1", "SQ2"], 4e4, [0.75, 0.33], 0.5) + + Output a 32 MHz PWM signal on SQ4 with a duty cycle of 50%: + + >>> pwmgen.map_reference_clock(["SQ4"], 2) + + Set SQ2 high: + + >>> pwmgen.set_states(sq2=True) + """ + + _LOW_FREQUENCY_LIMIT = 4 + _HIGH_FREQUENCY_LIMIT = 1e7 + + def __init__(self, device: ConnectionHandler | None = None): + self._device = device if device is not None else autoconnect() + self._channels = {n: DigitalOutput(n) for n in DIGITAL_OUTPUTS} + self._frequency = 0 + self._reference_prescaler = 0 + + @property + def frequency(self) -> float: + """Get the common frequency for all digital outputs in Hz.""" + return self._frequency + + def generate( + self, + channels: Union[str, List[str]], + frequency: float, + duty_cycles: Union[float, List[float]], + phases: Union[float, List[float]] = 0, + ): + """Generate PWM signals on SQ1, SQ2, SQ3, and SQ4. + + Parameters + ---------- + channels : {1, 2, 3, 4} or {'SQ1', 'SQ2', 'SQ3', 'SQ4'} or list of the same + Pin name or list of pin names on which to generate PWM signals. + Pins which are not included in the argument will not be affected. + frequency : float + Frequency in Hz. Shared by all outputs. + duty_cycles : float or list of floats + Duty cycle between 0 and 1 as either a single value or a list of + values. + + If 'duty_cycles' is a single value, it is applied to all channels + given in 'channels'. + + If 'duty_cycles' is a list, the values in the list will be applied + to the corresponding channel in the 'channels' list. The lists must + have the same length. + phases : float or list of floats + Phase between 0 and 1 as either a single value or a list of values. + + If 'phases' is a single value, it will be the phase between each + subsequent channel in 'channels'. For example, + + >>> generate(['SQ1', 'SQ2', 'SQ3', 'SQ4'], 1000, 0.5, 0.1) + + SQ2 will be shifted relative to SQ1 by 10%, SQ3 will be shifted + relative to SQ2 by 10% (i.e. 20% relative to SQ1), and SQ4 will be + shifted relative to SQ3 by 10% (i.e. 30% relative to SQ1). + + If 'phases' is a list, the values in the list will be applied to + the corresponding channel in the 'channels' list. The lists must + have the same length. + """ + if isinstance(channels, int): + channels = ["SQ1", "SQ2", "SQ3", "SQ4"][:channels] + + if frequency > self._HIGH_FREQUENCY_LIMIT: + e = ( + "Frequency is greater than 10 MHz." + + " " + + "Please use map_reference_clock for 16 & 32 MHz outputs." + ) + raise ValueError(e) + elif frequency < self._LOW_FREQUENCY_LIMIT: + raise ValueError( + f"Frequency must be at least {self._LOW_FREQUENCY_LIMIT} Hz." + ) + else: + self._frequency = frequency + channels, duty_cycles = _listify(channels, 4, duty_cycles) + + if not isinstance(phases, list): + phases = [i * phases for i in range(len(channels))] + + for channel, duty_cycle, phase in zip(channels, duty_cycles, phases): + self._channels[channel].duty_cycle = duty_cycle + self._channels[channel].phase = phase + self._channels[channel].remapped = False + + # Turn on all channels, minimum duty cycle of 1 wavelength. + self._generate( + [self._channels[c].duty_cycle for c in DIGITAL_OUTPUTS], + [self._channels[c].phase for c in DIGITAL_OUTPUTS], + ) + # Reset channels which should be LOW or HIGH to correct duty cycle. + self.set_state(**{k.lower(): v.state for k, v in self._channels.items()}) + + # Remap channels which should be mapped to the referene clock. + remapped = [c.name for c in self._channels.values() if c.remapped] + self.map_reference_clock(remapped, self._reference_prescaler) + + def _generate( + self, + duty_cycles: List[float], + phases: List[float], + ): + """Generate PWM signals on all four digital output pins. + + Paramaters + ---------- + duty_cycles : list of floats + List of length four containing duty cycles for each output as a + float in the open interval (0, 1). Note that it is not possible + to set the duty cycle to exactly 0 or 1; use :meth:`set_state` + instead. + phases : list of floats + List of length four containing phases for each output as a float in + the half open interval [0, 1). A phase of 1 is identical to a phase + of 0. + """ + wavelength, prescaler = _get_wavelength(self._frequency) + self._frequency = CP.CLOCK_RATE / wavelength / prescaler + continuous = 1 << 5 + + for i, (duty_cycle, phase) in enumerate(zip(duty_cycles, phases)): + duty_cycles[i] = int((duty_cycle + phase) % 1 * wavelength) + duty_cycles[i] = max(1, duty_cycles[i] - 1) # Zero index. + phases[i] = int(phase % 1 * wavelength) + phases[i] = max(0, phases[i] - 1) # Zero index. + + self._device.send_byte(CP.WAVEGEN) + self._device.send_byte(CP.SQR4) + self._device.send_int(wavelength - 1) # Zero index. + self._device.send_int(duty_cycles[0]) + self._device.send_int(phases[1]) + self._device.send_int(duty_cycles[1]) + self._device.send_int(phases[2]) + self._device.send_int(duty_cycles[2]) + self._device.send_int(phases[3]) + self._device.send_int(duty_cycles[3]) + self._device.send_byte(_PRESCALERS.index(prescaler) | continuous) + self._device.get_ack() + + def set_state( + self, + sq1: Union[bool, str, None] = None, + sq2: Union[bool, str, None] = None, + sq3: Union[bool, str, None] = None, + sq4: Union[bool, str, None] = None, + ): + """Set the digital outputs HIGH or LOW. + + Parameters + ---------- + sq1 : {True, False, None, 'HIGH', 'LOW', 'PWM'}, optional + Set the state of SQ1. True or "HIGH" sets it HIGH, False or "LOW" + sets it low, and None or "PWM" leaves it in its current state. The + default value is None. + sq2 : {True, False, None, 'HIGH', 'LOW', 'PWM'}, optional + See 'sq1'. + sq3 : {True, False, None, 'HIGH', 'LOW', 'PWM'}, optional + See 'sq1'. + sq4 : {True, False, None, 'HIGH', 'LOW', 'PWM'}, optional + See 'sq1'. + """ + states = 0 + + for i, sq in enumerate([sq1, sq2, sq3, sq4]): + if sq in (True, False, "HIGH", "LOW"): + sq = 1 if sq in (True, "HIGH") else 0 + self._channels[DIGITAL_OUTPUTS[i]].duty_cycle = sq + states |= self._channels[DIGITAL_OUTPUTS[i]].state_mask | (sq << i) + + self._device.send_byte(CP.DOUT) + self._device.send_byte(CP.SET_STATE) + self._device.send_byte(states) + self._device.get_ack() + + def map_reference_clock(self, channels: List[str], prescaler: int): + """Map the internal oscillator output to a digital output. + + The duty cycle of the output is locked to 50%. + + Parameters + ---------- + channels : {'SQ1', 'SQ2', 'SQ3', 'SQ4'} or list of the same + Digital output pin(s) to which to map the internal oscillator. + prescaler : int + Prescaler value in interval [0, 15]. The output frequency is + 128 / (1 << prescaler) MHz. + """ + (channels,) = _listify(channels, 4) + self._device.send_byte(CP.WAVEGEN) + self._device.send_byte(CP.MAP_REFERENCE) + self._reference_prescaler = prescaler + maps = 0 + + for channel in channels: + self._channels[channel].duty_cycle = 0.5 + self._channels[channel].phase = 0 + self._channels[channel].remapped = True + maps |= self._channels[channel].reference_clock_map + + self._device.send_byte(maps) + self._device.send_byte(prescaler) + self._device.get_ack() diff --git a/pslab/peripherals.py b/pslab/peripherals.py new file mode 100644 index 00000000..f8e9ad7e --- /dev/null +++ b/pslab/peripherals.py @@ -0,0 +1,657 @@ +import logging +import time + +import pslab.protocol as CP + +logger = logging.getLogger(__name__) + + +class NRF24L01(): + # Commands + R_REG = 0x00 + W_REG = 0x20 + RX_PAYLOAD = 0x61 + TX_PAYLOAD = 0xA0 + ACK_PAYLOAD = 0xA8 + FLUSH_TX = 0xE1 + FLUSH_RX = 0xE2 + ACTIVATE = 0x50 + R_STATUS = 0xFF + + # Registers + NRF_CONFIG = 0x00 + EN_AA = 0x01 + EN_RXADDR = 0x02 + SETUP_AW = 0x03 + SETUP_RETR = 0x04 + RF_CH = 0x05 + RF_SETUP = 0x06 + NRF_STATUS = 0x07 + OBSERVE_TX = 0x08 + CD = 0x09 + RX_ADDR_P0 = 0x0A + RX_ADDR_P1 = 0x0B + RX_ADDR_P2 = 0x0C + RX_ADDR_P3 = 0x0D + RX_ADDR_P4 = 0x0E + RX_ADDR_P5 = 0x0F + TX_ADDR = 0x10 + RX_PW_P0 = 0x11 + RX_PW_P1 = 0x12 + RX_PW_P2 = 0x13 + RX_PW_P3 = 0x14 + RX_PW_P4 = 0x15 + RX_PW_P5 = 0x16 + R_RX_PL_WID = 0x60 + FIFO_STATUS = 0x17 + DYNPD = 0x1C + FEATURE = 0x1D + PAYLOAD_SIZE = 0 + ACK_PAYLOAD_SIZE = 0 + READ_PAYLOAD_SIZE = 0 + + ADC_COMMANDS = 1 + READ_ADC = 0 << 4 + + I2C_COMMANDS = 2 + I2C_TRANSACTION = 0 << 4 + I2C_WRITE = 1 << 4 + I2C_SCAN = 2 << 4 + PULL_SCL_LOW = 3 << 4 + I2C_CONFIG = 4 << 4 + I2C_READ = 5 << 4 + + NRF_COMMANDS = 3 + NRF_READ_REGISTER = 0 + NRF_WRITE_REGISTER = 1 << 4 + + CURRENT_ADDRESS = 0xAAAA01 + nodelist = {} + nodepos = 0 + NODELIST_MAXLENGTH = 15 + connected = False + + def __init__(self, device): + self.H = device + self.ready = False + self.sigs = {self.CURRENT_ADDRESS: 1} + if self.H.connected: + self.connected = self.init() + + def init(self): + self.H.send_byte(CP.NRFL01) + self.H.send_byte(CP.NRF_SETUP) + self.H.get_ack() + time.sleep(0.015) # 15 mS settling time + stat = self.get_status() + if stat & 0x80: + logger.info("Radio transceiver not installed/not found") + return False + else: + self.ready = True + self.selectAddress(self.CURRENT_ADDRESS) + # self.write_register(self.RF_SETUP,0x06) + self.rxmode() + time.sleep(0.1) + self.flush() + return True + + def rxmode(self): + ''' + Puts the radio into listening mode. + ''' + self.H.send_byte(CP.NRFL01) + self.H.send_byte(CP.NRF_RXMODE) + self.H.get_ack() + + def txmode(self): + ''' + Puts the radio into transmit mode. + ''' + self.H.send_byte(CP.NRFL01) + self.H.send_byte(CP.NRF_TXMODE) + self.H.get_ack() + + def triggerAll(self, val): + self.txmode() + self.selectAddress(0x111111) + self.write_register(self.EN_AA, 0x00) + self.write_payload([val], True) + self.write_register(self.EN_AA, 0x01) + + def power_down(self): + self.H.send_byte(CP.NRFL01) + self.H.send_byte(CP.NRF_POWER_DOWN) + self.H.get_ack() + + def rxchar(self): + ''' + Receives a 1 Byte payload + ''' + self.H.send_byte(CP.NRFL01) + self.H.send_byte(CP.NRF_RXCHAR) + value = self.H.get_byte() + self.H.get_ack() + return value + + def txchar(self, char): + ''' + Transmits a single character + ''' + self.H.send_byte(CP.NRFL01) + self.H.send_byte(CP.NRF_TXCHAR) + self.H.send_byte(char) + return self.H.get_ack() >> 4 + + def hasData(self): + ''' + Check if the RX FIFO contains data + ''' + self.H.send_byte(CP.NRFL01) + self.H.send_byte(CP.NRF_HASDATA) + value = self.H.get_byte() + self.H.get_ack() + return value + + def flush(self): + ''' + Flushes the TX and RX FIFOs + ''' + self.H.send_byte(CP.NRFL01) + self.H.send_byte(CP.NRF_FLUSH) + self.H.get_ack() + + def write_register(self, address, value): + ''' + write a byte to any of the configuration registers on the Radio. + address byte can either be located in the NRF24L01+ manual, or chosen + from some of the constants defined in this module. + ''' + # print ('writing',address,value) + self.H.send_byte(CP.NRFL01) + self.H.send_byte(CP.NRF_WRITEREG) + self.H.send_byte(address) + self.H.send_byte(value) + self.H.get_ack() + + def read_register(self, address): + ''' + Read the value of any of the configuration registers on the radio module. + + ''' + self.H.send_byte(CP.NRFL01) + self.H.send_byte(CP.NRF_READREG) + self.H.send_byte(address) + val = self.H.get_byte() + self.H.get_ack() + return val + + def get_status(self): + ''' + Returns a byte representing the STATUS register on the radio. + Refer to NRF24L01+ documentation for further details + ''' + self.H.send_byte(CP.NRFL01) + self.H.send_byte(CP.NRF_GETSTATUS) + val = self.H.get_byte() + self.H.get_ack() + return val + + def write_command(self, cmd): + self.H.send_byte(CP.NRFL01) + self.H.send_byte(CP.NRF_WRITECOMMAND) + self.H.send_byte(cmd) + self.H.get_ack() + + def write_address(self, register, address): + ''' + register can be TX_ADDR, RX_ADDR_P0 -> RX_ADDR_P5 + 3 byte address. eg 0xFFABXX . XX cannot be FF + if RX_ADDR_P1 needs to be used along with any of the pipes + from P2 to P5, then RX_ADDR_P1 must be updated last. + Addresses from P1-P5 must share the first two bytes. + ''' + self.H.send_byte(CP.NRFL01) + self.H.send_byte(CP.NRF_WRITEADDRESS) + self.H.send_byte(register) + self.H.send_byte(address & 0xFF) + self.H.send_byte((address >> 8) & 0xFF) + self.H.send_byte((address >> 16) & 0xFF) + self.H.get_ack() + + def selectAddress(self, address): + ''' + Sets RX_ADDR_P0 and TX_ADDR to the specified address. + + ''' + self.H.send_byte(CP.NRFL01) + self.H.send_byte(CP.NRF_WRITEADDRESSES) + self.H.send_byte(address & 0xFF) + self.H.send_byte((address >> 8) & 0xFF) + self.H.send_byte((address >> 16) & 0xFF) + self.H.get_ack() + self.CURRENT_ADDRESS = address + if address not in self.sigs: + self.sigs[address] = 1 + + def read_payload(self, numbytes): + self.H.send_byte(CP.NRFL01) + self.H.send_byte(CP.NRF_READPAYLOAD) + self.H.send_byte(numbytes) + data = self.H.fd.read(numbytes) + self.H.get_ack() + return [ord(a) for a in data] + + def write_payload(self, data, verbose=False, **args): + self.H.send_byte(CP.NRFL01) + self.H.send_byte(CP.NRF_WRITEPAYLOAD) + numbytes = len( + data) | 0x80 # 0x80 implies transmit immediately. Otherwise it will simply load the TX FIFO ( used by ACK_payload) + if (args.get('rxmode', False)): numbytes |= 0x40 + self.H.send_byte(numbytes) + self.H.send_byte(self.TX_PAYLOAD) + for a in data: + self.H.send_byte(a) + val = self.H.get_ack() >> 4 + if (verbose): + if val & 0x2: + print(' NRF radio not found. Connect one to the add-on port') + elif val & 0x1: + print(' Node probably dead/out of range. It failed to acknowledge') + return + return val + + def I2C_scan(self): + ''' + Scans the I2C bus and returns a list of live addresses + ''' + x = self.transaction([self.I2C_COMMANDS | self.I2C_SCAN | 0x80], timeout=500) + if not x: return [] + if not sum(x): return [] + addrs = [] + for a in range(16): + if (x[a] ^ 255): + for b in range(8): + if x[a] & (0x80 >> b) == 0: + addr = 8 * a + b + addrs.append(addr) + return addrs + + def GuessingScan(self): + ''' + Scans the I2C bus and also prints the possible devices associated with each found address + ''' + from PSL import sensorlist + print('Scanning addresses 0-127...') + x = self.transaction([self.I2C_COMMANDS | self.I2C_SCAN | 0x80], timeout=500) + if not x: return [] + if not sum(x): return [] + addrs = [] + print('Address', '\t', 'Possible Devices') + + for a in range(16): + if (x[a] ^ 255): + for b in range(8): + if x[a] & (0x80 >> b) == 0: + addr = 8 * a + b + addrs.append(addr) + print(hex(addr), '\t\t', sensorlist.sensors.get(addr, 'None')) + + return addrs + + def transaction(self, data, **args): + self.H.send_byte(CP.NRFL01) + self.H.send_byte(CP.NRF_TRANSACTION) + self.H.send_byte(len(data)) # total Data bytes coming through + if 'listen' not in args: args['listen'] = True + if args.get('listen', False): data[0] |= 0x80 # You need this if hardware must wait for a reply + timeout = args.get('timeout', 200) + verbose = args.get('verbose', False) + self.H.send_int(timeout) # timeout. + for a in data: + self.H.send_byte(a) + + # print ('dt send',time.time()-st,timeout,data[0]&0x80,data) + numbytes = self.H.get_byte() + # print ('byte 1 in',time.time()-st) + if numbytes: + data = self.H.fd.read(numbytes) + else: + data = [] + val = self.H.get_ack() >> 4 + if (verbose): + if val & 0x1: print(time.time(), '%s Err. Node not found' % (hex(self.CURRENT_ADDRESS))) + if val & 0x2: print(time.time(), + '%s Err. NRF on-board transmitter not found' % (hex(self.CURRENT_ADDRESS))) + if val & 0x4 and args['listen']: print(time.time(), + '%s Err. Node received command but did not reply' % ( + hex(self.CURRENT_ADDRESS))) + if val & 0x7: # Something didn't go right. + self.flush() + self.sigs[self.CURRENT_ADDRESS] = self.sigs[self.CURRENT_ADDRESS] * 50 / 51. + return False + + self.sigs[self.CURRENT_ADDRESS] = (self.sigs[self.CURRENT_ADDRESS] * 50 + 1) / 51. + return [ord(a) for a in data] + + def transactionWithRetries(self, data, **args): + retries = args.get('retries', 5) + reply = False + while retries > 0: + reply = self.transaction(data, verbose=(retries == 1), **args) + if reply: + break + retries -= 1 + return reply + + def write_ack_payload(self, data, pipe): + if (len(data) != self.ACK_PAYLOAD_SIZE): + self.ACK_PAYLOAD_SIZE = len(data) + if self.ACK_PAYLOAD_SIZE > 15: + print('too large. truncating.') + self.ACK_PAYLOAD_SIZE = 15 + data = data[:15] + else: + print('ack payload size:', self.ACK_PAYLOAD_SIZE) + + self.H.send_byte(CP.NRFL01) + self.H.send_byte(CP.NRF_WRITEPAYLOAD) + self.H.send_byte(len(data)) + self.H.send_byte(self.ACK_PAYLOAD | pipe) + for a in data: + self.H.send_byte(a) + return self.H.get_ack() >> 4 + + def start_token_manager(self): + ''' + ''' + self.H.send_byte(CP.NRFL01) + self.H.send_byte(CP.NRF_START_TOKEN_MANAGER) + self.H.get_ack() + + def stop_token_manager(self): + ''' + ''' + self.H.send_byte(CP.NRFL01) + self.H.send_byte(CP.NRF_STOP_TOKEN_MANAGER) + self.H.get_ack() + + def total_tokens(self): + ''' + ''' + self.H.send_byte(CP.NRFL01) + self.H.send_byte(CP.NRF_TOTAL_TOKENS) + x = self.H.get_byte() + self.H.get_ack() + return x + + def fetch_report(self, num): + ''' + ''' + self.H.send_byte(CP.NRFL01) + self.H.send_byte(CP.NRF_REPORTS) + self.H.send_byte(num) + data = [self.H.get_byte() for a in range(20)] + self.H.get_ack() + return data + + @staticmethod + def __decode_I2C_list__(data): + lst = [] + if sum(data) == 0: + return lst + for i, d in enumerate(data): + if (d ^ 255): + for b in range(8): + if d & (0x80 >> b) == 0: + addr = 8 * i + b + lst.append(addr) + return lst + + def get_nodelist(self): + ''' + Refer to the variable 'nodelist' if you simply want a list of nodes that either registered while your code was + running , or were loaded from the firmware buffer(max 15 entries) + + If you plan to use more than 15 nodes, and wish to register their addresses without having to feed them manually, + then this function must be called each time before the buffer resets. + + The dictionary object returned by this function [addresses paired with arrays containing their registered sensors] + is filtered by checking with each node if they are alive. + + ''' + + total = self.total_tokens() + if self.nodepos != total: + for nm in range(self.NODELIST_MAXLENGTH): + dat = self.fetch_report(nm) + txrx = (dat[0]) | (dat[1] << 8) | (dat[2] << 16) + if not txrx: continue + self.nodelist[txrx] = self.__decode_I2C_list__(dat[3:19]) + self.nodepos = total + # else: + # self.__delete_registered_node__(nm) + + filtered_lst = {} + for a in self.nodelist: + if self.isAlive(a): filtered_lst[a] = self.nodelist[a] + + return filtered_lst + + def __delete_registered_node__(self, num): + self.H.send_byte(CP.NRFL01) + self.H.send_byte(CP.NRF_DELETE_REPORT_ROW) + self.H.send_byte(num) + self.H.get_ack() + + def __delete_all_registered_nodes__(self): + while self.total_tokens(): + print('-') + self.__delete_registered_node__(0) + + def isAlive(self, addr): + self.selectAddress(addr) + return self.transaction([self.NRF_COMMANDS | self.NRF_READ_REGISTER] + [self.R_STATUS], timeout=100, + verbose=False) + + def init_shockburst_transmitter(self, **args): + ''' + Puts the radio into transmit mode. + Dynamic Payload with auto acknowledge is enabled. + upto 5 retransmits with 1ms delay between each in case a node doesn't respond in time + Receivers must acknowledge payloads + ''' + self.PAYLOAD_SIZE = args.get('PAYLOAD_SIZE', self.PAYLOAD_SIZE) + myaddr = args.get('myaddr', 0xAAAA01) + sendaddr = args.get('sendaddr', 0xAAAA01) + + self.init() + # shockburst + self.write_address(self.RX_ADDR_P0, myaddr) # transmitter's address + self.write_address(self.TX_ADDR, sendaddr) # send to node with this address + self.write_register(self.RX_PW_P0, self.PAYLOAD_SIZE) + self.rxmode() + time.sleep(0.1) + self.flush() + + def init_shockburst_receiver(self, **args): + ''' + Puts the radio into receive mode. + Dynamic Payload with auto acknowledge is enabled. + ''' + self.PAYLOAD_SIZE = args.get('PAYLOAD_SIZE', self.PAYLOAD_SIZE) + if 'myaddr0' not in args: + args['myaddr0'] = 0xA523B5 + # if 'sendaddr' non in args: + # args['sendaddr']=0xA523B5 + print(args) + self.init() + self.write_register(self.RF_SETUP, 0x26) # 2MBPS speed + + # self.write_address(self.TX_ADDR,sendaddr) #send to node with this address + # self.write_address(self.RX_ADDR_P0,myaddr) #will receive the ACK Payload from that node + enabled_pipes = 0 # pipes to be enabled + for a in range(0, 6): + x = args.get('myaddr' + str(a), None) + if x: + print(hex(x), hex(self.RX_ADDR_P0 + a)) + enabled_pipes |= (1 << a) + self.write_address(self.RX_ADDR_P0 + a, x) + P15_base_address = args.get('myaddr1', None) + if P15_base_address: self.write_address(self.RX_ADDR_P1, P15_base_address) + + self.write_register(self.EN_RXADDR, enabled_pipes) # enable pipes + self.write_register(self.EN_AA, enabled_pipes) # enable auto Acknowledge on all pipes + self.write_register(self.DYNPD, enabled_pipes) # enable dynamic payload on Data pipes + self.write_register(self.FEATURE, 0x06) # enable dynamic payload length + # self.write_register(self.RX_PW_P0,self.PAYLOAD_SIZE) + + self.rxmode() + time.sleep(0.1) + self.flush() + + +class RadioLink(): + ADC_COMMANDS = 1 + READ_ADC = 0 << 4 + + I2C_COMMANDS = 2 + I2C_TRANSACTION = 0 << 4 + I2C_WRITE = 1 << 4 + SCAN_I2C = 2 << 4 + PULL_SCL_LOW = 3 << 4 + I2C_CONFIG = 4 << 4 + I2C_READ = 5 << 4 + + NRF_COMMANDS = 3 + NRF_READ_REGISTER = 0 << 4 + NRF_WRITE_REGISTER = 1 << 4 + + MISC_COMMANDS = 4 + WS2812B_CMD = 0 << 4 + + def __init__(self, NRF, **args): + self.NRF = NRF + if 'address' in args: + self.ADDRESS = args.get('address', False) + else: + print('Address not specified. Add "address=0x....." argument while instantiating') + self.ADDRESS = 0x010101 + + def __selectMe__(self): + if self.NRF.CURRENT_ADDRESS != self.ADDRESS: + self.NRF.selectAddress(self.ADDRESS) + + def I2C_scan(self): + self.__selectMe__() + from PSL import sensorlist + print('Scanning addresses 0-127...') + x = self.NRF.transaction([self.I2C_COMMANDS | self.SCAN_I2C | 0x80], timeout=500) + if not x: return [] + if not sum(x): return [] + addrs = [] + print('Address', '\t', 'Possible Devices') + + for a in range(16): + if (x[a] ^ 255): + for b in range(8): + if x[a] & (0x80 >> b) == 0: + addr = 8 * a + b + addrs.append(addr) + print(hex(addr), '\t\t', sensorlist.sensors.get(addr, 'None')) + + return addrs + + @staticmethod + def __decode_I2C_list__(data): + lst = [] + if sum(data) == 0: + return lst + for i, d in enumerate(data): + if (d ^ 255): + for b in range(8): + if d & (0x80 >> b) == 0: + addr = 8 * i + b + lst.append(addr) + return lst + + def writeI2C(self, I2C_addr, regaddress, data_bytes): + self.__selectMe__() + return self.NRF.transaction([self.I2C_COMMANDS | self.I2C_WRITE] + [I2C_addr] + [regaddress] + data_bytes) + + def readI2C(self, I2C_addr, regaddress, numbytes): + self.__selectMe__() + return self.NRF.transaction([self.I2C_COMMANDS | self.I2C_TRANSACTION] + [I2C_addr] + [regaddress] + [numbytes]) + + def writeBulk(self, I2C_addr, data_bytes): + self.__selectMe__() + return self.NRF.transaction([self.I2C_COMMANDS | self.I2C_WRITE] + [I2C_addr] + data_bytes) + + def readBulk(self, I2C_addr, regaddress, numbytes): + self.__selectMe__() + return self.NRF.transactionWithRetries( + [self.I2C_COMMANDS | self.I2C_TRANSACTION] + [I2C_addr] + [regaddress] + [numbytes]) + + def simpleRead(self, I2C_addr, numbytes): + self.__selectMe__() + return self.NRF.transactionWithRetries([self.I2C_COMMANDS | self.I2C_READ] + [I2C_addr] + [numbytes]) + + def readADC(self, channel): + self.__selectMe__() + return self.NRF.transaction([self.ADC_COMMANDS | self.READ_ADC] + [channel]) + + def pullSCLLow(self, t_ms): + self.__selectMe__() + dat = self.NRF.transaction([self.I2C_COMMANDS | self.PULL_SCL_LOW] + [t_ms]) + if dat: + return self.__decode_I2C_list__(dat) + else: + return [] + + def configI2C(self, freq): + self.__selectMe__() + brgval = int(32e6 / freq / 4 - 1) + print(brgval) + return self.NRF.transaction([self.I2C_COMMANDS | self.I2C_CONFIG] + [brgval], listen=False) + + def write_register(self, reg, val): + self.__selectMe__() + # print ('writing to ',reg,val) + return self.NRF.transaction([self.NRF_COMMANDS | self.NRF_WRITE_REGISTER] + [reg, val], listen=False) + + def WS2812B(self, cols): + """ + set shade of WS2182 LED on CS1/RC0 + + .. tabularcolumns:: |p{3cm}|p{11cm}| + + ============== ============================================================================================ + **Arguments** + ============== ============================================================================================ + cols 2Darray [[R,G,B],[R2,G2,B2],[R3,G3,B3]...] + brightness of R,G,B ( 0-255 ) + ============== ============================================================================================ + + example:: + + >>> WS2812B([[10,0,0],[0,10,10],[10,0,10]]) + #sets red, cyan, magenta to three daisy chained LEDs + + """ + self.__selectMe__() + colarray = [] + for a in cols: + colarray.append(int('{:08b}'.format(int(a[1]))[::-1], 2)) + colarray.append(int('{:08b}'.format(int(a[0]))[::-1], 2)) + colarray.append(int('{:08b}'.format(int(a[2]))[::-1], 2)) + + res = self.NRF.transaction([self.MISC_COMMANDS | self.WS2812B_CMD] + colarray, listen=False) + return res + + def read_register(self, reg): + self.__selectMe__() + x = self.NRF.transaction([self.NRF_COMMANDS | self.NRF_READ_REGISTER] + [reg]) + if x: + return x[0] + else: + return False diff --git a/PSL/commands_proto.py b/pslab/protocol.py similarity index 72% rename from PSL/commands_proto.py rename to pslab/protocol.py index f912e839..de7f258f 100644 --- a/PSL/commands_proto.py +++ b/pslab/protocol.py @@ -1,4 +1,8 @@ -import math, sys, time, struct +"""TODO""" + +import enum +import struct + # allows to pack numeric values into byte strings Byte = struct.Struct("B") # size 1 @@ -8,13 +12,14 @@ ACKNOWLEDGE = Byte.pack(254) MAX_SAMPLES = 10000 DATA_SPLITTING = 200 +CLOCK_RATE = 64e6 # /*----flash memory----*/ -FLASH = Byte.pack(1) -READ_FLASH = Byte.pack(1) -WRITE_FLASH = Byte.pack(2) -WRITE_BULK_FLASH = Byte.pack(3) -READ_BULK_FLASH = Byte.pack(4) +# FLASH = Byte.pack(1) +# READ_FLASH = Byte.pack(1) +# WRITE_FLASH = Byte.pack(2) +# WRITE_BULK_FLASH = Byte.pack(3) +# READ_BULK_FLASH = Byte.pack(4) # /*-----ADC------*/ ADC = Byte.pack(2) @@ -28,10 +33,10 @@ SET_PGA_GAIN = Byte.pack(8) GET_VOLTAGE = Byte.pack(9) GET_VOLTAGE_SUMMED = Byte.pack(10) -START_ADC_STREAMING = Byte.pack(11) +# START_ADC_STREAMING = Byte.pack(11) SELECT_PGA_CHANNEL = Byte.pack(12) CAPTURE_12BIT = Byte.pack(13) -CAPTURE_MULTIPLE = Byte.pack(14) +# CAPTURE_MULTIPLE = Byte.pack(14) SET_HI_CAPTURE = Byte.pack(15) SET_LO_CAPTURE = Byte.pack(16) @@ -66,7 +71,6 @@ I2C_INIT = Byte.pack(14) I2C_PULLDOWN_SCL = Byte.pack(15) I2C_DISABLE_SMBUS = Byte.pack(16) -I2C_START_SCOPE = Byte.pack(17) # /*------UART2--------*/ UART_2 = Byte.pack(5) @@ -83,6 +87,7 @@ DAC = Byte.pack(6) SET_DAC = Byte.pack(1) SET_CALIBRATED_DAC = Byte.pack(2) +SET_POWER = Byte.pack(3) # /*--------WAVEGEN-----*/ WAVEGEN = Byte.pack(7) @@ -112,10 +117,10 @@ GET_STATE = Byte.pack(1) GET_STATES = Byte.pack(2) -ID1 = Byte.pack(0) -ID2 = Byte.pack(1) -ID3 = Byte.pack(2) -ID4 = Byte.pack(3) +LA1 = Byte.pack(0) +LA2 = Byte.pack(1) +LA3 = Byte.pack(2) +LA4 = Byte.pack(3) LMETER = Byte.pack(4) # /*------TIMING FUNCTIONS-----*/ @@ -148,6 +153,7 @@ GET_INDUCTANCE = Byte.pack(4) GET_VERSION = Byte.pack(5) +GET_FW_VERSION = Byte.pack(6) RETRIEVE_BUFFER = Byte.pack(8) GET_HIGH_FREQUENCY = Byte.pack(9) @@ -163,6 +169,7 @@ READ_LOG = Byte.pack(18) RESTORE_STANDALONE = Byte.pack(19) GET_ALTERNATE_HIGH_FREQUENCY = Byte.pack(20) +SET_RGB_COMMON = Byte.pack(21) SET_RGB3 = Byte.pack(22) START_CTMU = Byte.pack(23) @@ -173,16 +180,16 @@ FILL_BUFFER = Byte.pack(27) # /*---------- BAUDRATE for main comm channel----*/ -SETBAUD = Byte.pack(12) -BAUD9600 = Byte.pack(1) -BAUD14400 = Byte.pack(2) -BAUD19200 = Byte.pack(3) -BAUD28800 = Byte.pack(4) -BAUD38400 = Byte.pack(5) -BAUD57600 = Byte.pack(6) -BAUD115200 = Byte.pack(7) -BAUD230400 = Byte.pack(8) -BAUD1000000 = Byte.pack(9) +SETBAUD_LEGACY = Byte.pack(12) +BAUD9600_LEGACY = Byte.pack(1) +BAUD14400_LEGACY = Byte.pack(2) +BAUD19200_LEGACY = Byte.pack(3) +BAUD28800_LEGACY = Byte.pack(4) +BAUD38400_LEGACY = Byte.pack(5) +BAUD57600_LEGACY = Byte.pack(6) +BAUD115200_LEGACY = Byte.pack(7) +BAUD230400_LEGACY = Byte.pack(8) +BAUD1000000_LEGACY = Byte.pack(9) # /*-----------NRFL01 radio module----------*/ NRFL01 = Byte.pack(13) @@ -214,20 +221,21 @@ # ---------Non standard IO protocols-------- NONSTANDARD_IO = Byte.pack(14) -HX711_HEADER = Byte.pack(1) +# HX711_HEADER = Byte.pack(1) HCSR04_HEADER = Byte.pack(2) -AM2302_HEADER = Byte.pack(3) -TCD1304_HEADER = Byte.pack(4) -STEPPER_MOTOR = Byte.pack(5) +# AM2302_HEADER = Byte.pack(3) +# TCD1304_HEADER = Byte.pack(4) +# STEPPER_MOTOR = Byte.pack(5) # --------COMMUNICATION PASSTHROUGHS-------- # Data sent to the device is directly routed to output ports such as (SCL, SDA for UART) -PASSTHROUGHS = Byte.pack(15) +PASSTHROUGHS = Byte.pack(12) +PASSTHROUGHS_LEGACY = Byte.pack(15) PASS_UART = Byte.pack(1) # /*--------STOP STREAMING------*/ -STOP_STREAMING = Byte.pack(253) +# STOP_STREAMING = Byte.pack(253) # /*------INPUT CAPTURE---------*/ # capture modes @@ -250,66 +258,3 @@ # resolutions TEN_BIT = Byte.pack(10) TWELVE_BIT = Byte.pack(12) - - -def applySIPrefix(value, unit='', precision=2): - neg = False - if value < 0.: - value *= -1 - neg = True - elif value == 0.: - return '0 ' # mantissa & exponnt both 0 - exponent = int(math.log10(value)) - if exponent > 0: - exponent = (exponent // 3) * 3 - else: - exponent = (-1 * exponent + 3) // 3 * (-3) - - value *= (10 ** (-exponent)) - if value >= 1000.: - value /= 1000.0 - exponent += 3 - if neg: - value *= -1 - exponent = int(exponent) - PREFIXES = "yzafpnum kMGTPEZY" - prefix_levels = (len(PREFIXES) - 1) // 2 - si_level = exponent // 3 - if abs(si_level) > prefix_levels: - raise ValueError("Exponent out range of available prefixes.") - return '%.*f %s%s' % (precision, value, PREFIXES[si_level + prefix_levels], unit) - - -''' -def reverse_bits(x): - return int('{:08b}'.format(x)[::-1], 2) - -def InttoString(val): - return ShortInt.pack(int(val)) - -def StringtoInt(string): - return ShortInt.unpack(string)[0] - -def StringtoLong(string): - return Integer.unpack(string)[0] - -def getval12(val): - return val*3.3/4095 - -def getval10(val): - return val*3.3/1023 - - -def getL(F,C): - return 1.0/(C*4*math.pi*math.pi*F*F) - -def getF(L,C): - return 1.0/(2*math.pi*math.sqrt(L*C)) - -def getLx(f1,f2,f3,Ccal): - a=(f1/f3)**2 - b=(f1/f2)**2 - c=(2*math.pi*f1)**2 - return (a-1)*(b-1)/(Ccal*c) - -''' diff --git a/pslab/sciencelab.py b/pslab/sciencelab.py new file mode 100644 index 00000000..1cbc81e8 --- /dev/null +++ b/pslab/sciencelab.py @@ -0,0 +1,322 @@ +"""Convenience module that creates instances of every instrument for you. + +Every PSLab instrument can be imported and instantiated individually. However, +if you need to use several at once the ScienceLab class provides a convenient +collection. +""" + +from __future__ import annotations + +import time +from typing import Iterable, List + +import pslab.protocol as CP +from pslab.connection import ConnectionHandler, SerialHandler, autoconnect +from pslab.instrument.logic_analyzer import LogicAnalyzer +from pslab.instrument.multimeter import Multimeter +from pslab.instrument.oscilloscope import Oscilloscope +from pslab.instrument.power_supply import PowerSupply +from pslab.instrument.waveform_generator import PWMGenerator, WaveformGenerator + + +class ScienceLab: + """Aggregate interface for the PSLab's instruments. + + Attributes + ---------- + logic_analyzer : pslab.LogicAnalyzer + oscilloscope : pslab.Oscilloscope + waveform_generator : pslab.WaveformGenerator + pwm_generator : pslab.PWMGenerator + multimeter : pslab.Multimeter + power_supply : pslab.PowerSupply + i2c : pslab.I2CMaster + nrf : pslab.peripherals.NRF24L01 + """ + + def __init__(self, device: ConnectionHandler | None = None): + self.device = device if device is not None else autoconnect() + self.firmware = self.device.get_firmware_version() + self.logic_analyzer = LogicAnalyzer(device=self.device) + self.oscilloscope = Oscilloscope(device=self.device) + self.waveform_generator = WaveformGenerator(device=self.device) + self.pwm_generator = PWMGenerator(device=self.device) + self.multimeter = Multimeter(device=self.device) + self.power_supply = PowerSupply(device=self.device) + + @property + def temperature(self): + """float: Temperature of the MCU in degrees Celsius.""" + # TODO: Get rid of magic numbers. + cs = 3 + V = self._get_ctmu_voltage(0b11110, cs, 0) + + if cs == 1: + return (646 - V * 1000) / 1.92 # current source = 1 + elif cs == 2: + return (701.5 - V * 1000) / 1.74 # current source = 2 + elif cs == 3: + return (760 - V * 1000) / 1.56 # current source = 3 + + def _get_ctmu_voltage(self, channel: int, current_range: int, tgen: bool = True): + """Control the Charge Time Measurement Unit (CTMU). + + ctmu_voltage(5, 2) will activate a constant current source of 5.5 µA on + CAP and then measure the voltage at the output. + + If a diode is used to connect CAP to ground, the forward voltage drop + of the diode will be returned, e.g. 0.6 V for a 4148 diode. + + If a resistor is connected, Ohm's law will be followed within + reasonable limits. + + Parameters + ---------- + channel : int + Pin number on which to generate a current and measure output + voltage. Refer to the PIC24EP64GP204 datasheet for channel + numbering. + current_range : {0, 1, 2, 3} + 0 -> 550 µA + 1 -> 550 nA + 2 -> 5.5 µA + 3 -> 55 µA + tgen : bool, optional + Use Time Delay mode instead of Measurement mode. The default value + is True. + + Returns + ------- + voltage : float + """ + self.device.send_byte(CP.COMMON) + self.device.send_byte(CP.GET_CTMU_VOLTAGE) + self.device.send_byte((channel) | (current_range << 5) | (tgen << 7)) + raw_voltage = self.get_int() / 16 # 16*voltage across the current source + self.device.get_ack() + vmax = 3.3 + resolution = 12 + voltage = vmax * raw_voltage / (2**resolution - 1) + return voltage + + def _start_ctmu(self, current_range: int, trim: int, tgen: int = 1): + self.device.send_byte(CP.COMMON) + self.device.send_byte(CP.START_CTMU) + self.device.send_byte((current_range) | (tgen << 7)) + self.device.send_byte(trim) + self.device.get_ack() + + def _stop_ctmu(self): + self.device.send_byte(CP.COMMON) + self.device.send_byte(CP.STOP_CTMU) + self.device.get_ack() + + def reset(self): + """Reset the device.""" + self.device.send_byte(CP.COMMON) + self.device.send_byte(CP.RESTORE_STANDALONE) + + def enter_bootloader(self): + """Reboot and stay in bootloader mode.""" + if not isinstance(self.device, SerialHandler): + msg = "cannot enter bootloader over wireless" + raise RuntimeError(msg) + + self.reset() + self.device.interface.baudrate = 460800 + # The PSLab's RGB LED flashes some colors on boot. + boot_lightshow_time = 0.6 + # Wait before sending magic number to make sure UART is initialized. + time.sleep(boot_lightshow_time / 2) + # PIC24 UART RX buffer is four bytes deep; no need to time it perfectly. + self.device.write(CP.Integer.pack(0xDECAFBAD)) + # Wait until lightshow is done to prevent accidentally overwriting magic number. + time.sleep(boot_lightshow_time) + + def rgb_led(self, colors: List, output: str = "RGB", order: str = "GRB"): + """Set shade of a WS2812B RGB LED. + + Parameters + ---------- + colors : list + List of three values between 0-255, where each value is the + intensity of red, green, and blue, respectively. When daisy + chaining several LEDs, colors should be a list of three-value + lists. + output : {"RGB", "PGC", "SQ1"}, optional + Pin on which to output the pulse train setting the LED color. The + default value, "RGB", sets the color of the built-in WS2812B + (PSLab v6 only). + order : str, optional + Color order of the connected LED as a three-letter string. The + built-in LED has order "GRB", which is the default. + + Examples + -------- + Set the built-in WS2812B to yellow. + + >>> import pslab + >>> psl = pslab.ScienceLab() + >>> psl.rgb_led([10, 10, 0]) + + Set a chain of three RGB WS2812B connected to SQ1 to red, cyan, and + magenta. + + >>> psl.rgb_led([[10,0,0],[0,10,10],[10,0,10]], output="SQ1", order="RGB") + """ + if "6" in self.device.version: + pins = {"ONBOARD": 0, "SQ1": 1, "SQ2": 2, "SQ3": 3, "SQ4": 4} + else: + pins = {"RGB": CP.SET_RGB1, "PGC": CP.SET_RGB2, "SQ1": CP.SET_RGB3} + + try: + pin = pins[output] + except KeyError: + pinnames = ", ".join(pins.keys()) + raise ValueError( + f"Invalid output: {output}. output must be one of {pinnames}." + ) + + if not isinstance(colors[0], Iterable): + colors = [colors] + + if not all([len(color) == 3 for color in colors]): + raise ValueError("Invalid color; each color list must have three values.") + + order = order.upper() + + if not sorted(order) == ["B", "G", "R"]: + raise ValueError( + f"Invalid order: {order}. order must contain 'R', 'G', and 'B'." + ) + + self.device.send_byte(CP.COMMON) + + if "6" in self.device.version: + self.device.send_byte(CP.SET_RGB_COMMON) + else: + self.device.send_byte(pin) + + self.device.send_byte(len(colors) * 3) + + for color in colors: + self.device.send_byte(color[order.index("R")]) + self.device.send_byte(color[order.index("G")]) + self.device.send_byte(color[order.index("B")]) + + if "6" in self.device.version: + self.device.send_byte(pin) + + self.device.get_ack() + + def _read_program_address(self, address: int): + """Return the value stored at the specified address in program memory. + + Parameters + ---------- + address : int + Address to read from. Refer to PIC24EP64GP204 programming manual. + + Returns + ------- + data : int + 16-bit wide value read from program memory. + """ + self.device.send_byte(CP.COMMON) + self.device.send_byte(CP.READ_PROGRAM_ADDRESS) + self.device.send_int(address & 0xFFFF) + self.device.send_int((address >> 16) & 0xFFFF) + data = self.device.get_int() + self.device.get_ack() + return data + + def _device_id(self): + a = self._read_program_address(0x800FF8) + b = self._read_program_address(0x800FFA) + c = self._read_program_address(0x800FFC) + d = self._read_program_address(0x800FFE) + val = d | (c << 16) | (b << 32) | (a << 48) + return val + + def _read_data_address(self, address: int): + """Return the value stored at the specified address in RAM. + + Parameters + ---------- + address : int + Address to read from. Refer to PIC24EP64GP204 programming manual. + + Returns + ------- + data : int + 16-bit wide value read from RAM. + """ + self.device.send_byte(CP.COMMON) + self.device.send_byte(CP.READ_DATA_ADDRESS) + self.device.send_int(address & 0xFFFF) + data = self.device.get_int() + self.device.get_ack() + return data + + def _write_data_address(self, address: int, value: int): + """Write a value to the specified address in RAM. + + Parameters + ---------- + address : int + Address to write to. Refer to PIC24EP64GP204 programming manual. + value : int + Value to write to RAM. + """ + self.device.send_byte(CP.COMMON) + self.device.send_byte(CP.WRITE_DATA_ADDRESS) + self.device.send_int(address & 0xFFFF) + self.device.send_int(value) + self.device.get_ack() + + def enable_uart_passthrough(self, baudrate: int): + """Relay all data received by the device to TXD/RXD. + + Can be used to load programs into secondary microcontrollers with + bootloaders such ATMEGA or ESP8266 + + Parameters + ---------- + baudrate : int + Baudrate of the UART2 bus. + """ + if self.firmware.major < 3: + self._uart_passthrough_legacy(baudrate) + else: + self._uart_passthrough(baudrate) + + def _uart_passthrough(self, baudrate: int) -> None: + self.device.send_byte(CP.PASSTHROUGHS) + self.device.send_byte(CP.PASS_UART) + self.device.send_int(self._get_brgval(baudrate)) + self.device.baudrate = baudrate + + def _uart_passthrough_legacy(self, baudrate: int) -> None: + self.device.send_byte(CP.PASSTHROUGHS_LEGACY) + self.device.send_byte(CP.PASS_UART) + disable_watchdog = 1 + self.device.send_byte(disable_watchdog) + self.device.send_int(self._get_brgval(baudrate)) + + @staticmethod + def _get_brgval(baudrate: int) -> int: + return int((CP.CLOCK_RATE / (4 * baudrate)) - 1) + + def read_log(self): + """Read hardware debug log. + + Returns + ------- + log : bytes + Bytes read from the hardware debug log. + """ + self.device.send_byte(CP.COMMON) + self.device.send_byte(CP.READ_LOG) + log = self.device.interface.readline().strip() + self.get_ack() + return log diff --git a/pslab/serial_handler.py b/pslab/serial_handler.py new file mode 100644 index 00000000..4b05c1d1 --- /dev/null +++ b/pslab/serial_handler.py @@ -0,0 +1,29 @@ +"""Deprecated provider of SerialHandler.""" + +import warnings + +from pslab.connection import SerialHandler, autoconnect + +warnings.warn( + "pslab.serial_handler is deprecated and will be removed in a future release. " + "Use pslab.connection instead." +) + + +class _SerialHandler(SerialHandler): + def __init__( + self, + port: str | None = None, + baudrate: int = 1000000, + timeout: float = 1.0, + ) -> None: + if port is None: + tmp_handler = autoconnect() + port = tmp_handler.port + tmp_handler.disconnect() + + super().__init__(port=port, baudrate=baudrate, timeout=timeout) + self.connect() + + +SerialHandler = _SerialHandler diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 00000000..c78ef11f --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,29 @@ +[build-system] +requires = ["flit_core >=3.4,<4"] +build-backend = "flit_core.buildapi" + +[project] +name = "pslab" +authors = [{name = "FOSSASIA PSLab Developers", email = "pslab-fossasia@googlegroups.com"}] +dynamic = ["version", "description"] +readme = "README.md" +requires-python = ">=3.8" +license = {file = "LICENSE"} +dependencies = [ + "pyserial", + "numpy", + "scipy", + "mcbootflash >= 8.0.0", +] +classifiers = [ + "Programming Language :: Python :: 3", + "License :: OSI Approved :: GNU General Public License v3 (GPLv3)", + "Operating System :: OS Independent", + "Development Status :: 5 - Production/Stable", +] + +[project.urls] +Home = "https://pslab.io/" + +[project.scripts] +pslab = "pslab.cli:cmdline" diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 00000000..d7fb242b --- /dev/null +++ b/requirements.txt @@ -0,0 +1,5 @@ +setuptools >= 35.0.2 +numpy >= 1.16.3 +pyserial >= 3.4 +scipy >= 1.3.0 +mcbootflash >= 4.1.0 diff --git a/setup.py b/setup.py deleted file mode 100644 index ed80adb0..00000000 --- a/setup.py +++ /dev/null @@ -1,55 +0,0 @@ -#!/usr/bin/env python - - -from __future__ import print_function -#from distutils.core import setup -from setuptools import setup, find_packages -from setuptools.command.install import install -import os,shutil -from distutils.util import execute -from distutils.cmd import Command -from subprocess import call - -def udev_reload_rules(): - call(["udevadm", "control", "--reload-rules"]) - -def udev_trigger(): - call(["udevadm", "trigger", "--subsystem-match=usb","--attr-match=idVendor=04d8", "--action=add"]) - -def install_udev_rules(raise_exception): - if check_root(): - shutil.copy('99-pslab.rules', '/etc/udev/rules.d') - execute(udev_reload_rules, [], "Reloading udev rules") - execute(udev_trigger, [], "Triggering udev rules") - else: - msg = "You must have root privileges to install udev rules. Run 'sudo python setup.py install'" - if raise_exception: - raise OSError(msg) - else: - print(msg) - -def check_root(): - return os.geteuid() == 0 - -class CustomInstall(install): - def run(self): - if not hasattr(self,"root"): - install_udev_rules(True) - elif self.root is not None: - if 'debian' not in self.root: - install_udev_rules(True) - install.run(self) - -setup(name='PSL', - version='1.0.0', - description='Pocket Science Lab from FOSSASIA - inspired by ExpEYES http://expeyes.in', - author='Praveen Patil and Jithin B.P.', - author_email='praveenkumar103@gmail.com', - url='http://fossasia.github.io/pslab.fossasia.org/', - install_requires = ['numpy>=1.8.1','pyqtgraph>=0.9.10'], - packages=find_packages(), - #scripts=["PSL/bin/"+a for a in os.listdir("PSL/bin/")], - package_data={'': ['*.css','*.png','*.gif','*.html','*.css','*.js','*.png','*.jpg','*.jpeg','*.htm','99-pslab.rules']}, - cmdclass={'install': CustomInstall}, -) - diff --git a/setup.cfg b/tests/__init__.py similarity index 100% rename from setup.cfg rename to tests/__init__.py diff --git a/tests/conftest.py b/tests/conftest.py new file mode 100644 index 00000000..db604d15 --- /dev/null +++ b/tests/conftest.py @@ -0,0 +1,13 @@ +"""Common fixtures for pslab tests.""" + +import pytest + +from pslab.connection import SerialHandler + + +@pytest.fixture +def handler(): + """Return a SerialHandler instance.""" + sh = SerialHandler() + sh.connect() + return sh diff --git a/tests/test_cli.py b/tests/test_cli.py new file mode 100644 index 00000000..5c64144d --- /dev/null +++ b/tests/test_cli.py @@ -0,0 +1,236 @@ +"""Tests for PSL.cli. + +Before running the tests, connect: + SQ1 -> LA1 + SQ2 -> LA2 + SQ3 -> LA3 + SQ4 -> LA4 + SI1 -> CH1 + SI2 -> CH2 + SI1 -> CH3 +""" + +import csv +import json + +import numpy as np +import pytest + +import pslab.protocol as CP +from pslab import cli +from pslab.instrument.analog import AnalogOutput +from pslab.instrument.waveform_generator import WaveformGenerator + +LA_CHANNELS = 4 +EVENTS = 2450 +LA_DURATION = 1.5 + +SCOPE_CHANNELS = 4 +SAMPLES = CP.MAX_SAMPLES // SCOPE_CHANNELS +SCOPE_DURATION = 0.5 + + +@pytest.fixture +def la(mocker): + mock = mocker.patch("pslab.cli.LogicAnalyzer") + mock().fetch_data.return_value = [np.arange(2500)] * LA_CHANNELS + return mock + + +@pytest.fixture +def scope(mocker): + mock = mocker.patch("pslab.cli.Oscilloscope") + mock()._lookup_mininum_timegap.return_value = 0.5 + mock().capture.return_value = [np.zeros(SAMPLES)] * (SCOPE_CHANNELS + 1) + mock()._channel_one_map = "CH1" + mock()._CH234 = ["CH2", "CH3", "MIC"] + return mock + + +def logic_analyzer(device, channels, duration): + headers = ["LA1", "LA2", "LA3", "LA4"][:channels] + timestamps = [np.arange(0, duration * 1e6, (duration * 1e6) / EVENTS)] * channels + return headers, timestamps + + +def oscilloscope(device, channels, duration): + headers = ["Timestamp", "CH1", "CH2", "CH3", "MIC"][: 1 + channels] + timestamp = np.arange(0, duration * 1e6, (duration * 1e6) / SAMPLES) + data = [np.random.random_sample(SAMPLES)] * channels + return headers, [timestamp] + data + + +@pytest.fixture(name="collect") +def setup_collect(mocker, monkeypatch): + """Return a ArgumentParser instance with all arguments added.""" + mocker.patch("pslab.cli.SerialHandler") + INSTRUMENTS = { + "logic_analyzer": logic_analyzer, + "oscilloscope": oscilloscope, + } + monkeypatch.setattr(cli, "INSTRUMENTS", INSTRUMENTS) + + +@pytest.fixture(name="wave") +def setup_wave(mocker): + mocker.patch("pslab.cli.SerialHandler") + + +def test_logic_analyzer(la): + channels, timestamps = cli.logic_analyzer(la, LA_CHANNELS, LA_DURATION) + assert len(channels) == LA_CHANNELS + for timestamp in timestamps: + assert len(timestamp) > EVENTS + + +def test_oscilloscope(scope): + headers, values = cli.oscilloscope(scope, SCOPE_CHANNELS, SCOPE_DURATION) + assert len(headers) == 1 + SCOPE_CHANNELS + for value in values: + assert len(value) > SAMPLES + + +def test_collect_csv_stdout(collect, capsys): + cli.cmdline(["collect", "logic_analyzer", "--channels", str(LA_CHANNELS)]) + output = list(csv.reader(capsys.readouterr().out.splitlines())) + assert len(output[0]) == LA_CHANNELS + assert len(output) == 1 + EVENTS + + cli.cmdline(["collect", "oscilloscope", "--channels", str(SCOPE_CHANNELS)]) + output = list(csv.reader(capsys.readouterr().out.splitlines())) + assert len(output[0]) == 1 + SCOPE_CHANNELS + assert len(output) == 1 + SAMPLES + + +def test_collect_csv_file(collect, tmp_path): + la_temp_csv = str(tmp_path / "logic_analyzer.csv") + cli.cmdline( + [ + "collect", + "logic_analyzer", + "--channels", + str(LA_CHANNELS), + "--output", + la_temp_csv, + ] + ) + with open(la_temp_csv) as csv_file: + output = list(csv.reader(csv_file.read().splitlines())) + assert len(output[0]) == LA_CHANNELS + assert len(output) == 1 + EVENTS + + scope_temp_csv = str(tmp_path / "oscilloscope.csv") + cli.cmdline( + [ + "collect", + "oscilloscope", + "--channels", + str(SCOPE_CHANNELS), + "--output", + scope_temp_csv, + ] + ) + with open(scope_temp_csv) as csv_file: + output = list(csv.reader(csv_file.read().splitlines())) + assert len(output[0]) == 1 + SCOPE_CHANNELS + assert len(output) == 1 + SAMPLES + + +def test_collect_json_stdout(collect, capsys): + cli.cmdline(["collect", "logic_analyzer", "--channels", str(LA_CHANNELS), "--json"]) + output = json.loads(capsys.readouterr().out) + assert len(output) == LA_CHANNELS + assert len(list(output.values())[0]) == EVENTS + + cli.cmdline( + ["collect", "oscilloscope", "--channels", str(SCOPE_CHANNELS), "--json"] + ) + output = json.loads(capsys.readouterr().out) + assert len(output) == 1 + SCOPE_CHANNELS + assert len(list(output.values())[0]) == SAMPLES + + +def test_collect_json_file(collect, tmp_path): + la_tmp_json = str(tmp_path / "logic_analyzer.json") + cli.cmdline( + [ + "collect", + "logic_analyzer", + "--channels", + str(LA_CHANNELS), + "--output", + la_tmp_json, + "--json", + ] + ) + with open(la_tmp_json) as json_file: + output = json.load(json_file) + assert len(output) == LA_CHANNELS + assert len(list(output.values())[0]) == EVENTS + + scope_tmp_json = str(tmp_path / "oscilloscope.json") + cli.cmdline( + [ + "collect", + "oscilloscope", + "--channels", + str(SCOPE_CHANNELS), + "--output", + scope_tmp_json, + "--json", + ] + ) + with open(scope_tmp_json) as json_file: + output = json.load(json_file) + assert len(output) == 1 + SCOPE_CHANNELS + assert len(list(output.values())[0]) == SAMPLES + + +def test_wave_load_table(wave, mocker): + wavegen = WaveformGenerator(mocker.Mock()) + wavegen.load_function("SI1", "tria") + + def tria(x): + return AnalogOutput.RANGE[1] * (abs(x % 4 - 2) - 1) + + span = [-1, 3] + x = np.arange(span[0], span[1], (span[1] - span[0]) / 512) + table = json.dumps(tria(x).tolist()) + cli.cmdline(["wave", "load", "SI2", "--table", table]) + assert AnalogOutput("SI1").waveform_table == AnalogOutput("SI2").waveform_table + + +def test_wave_load_table_expand(wave): + table1 = json.dumps([0, 1]) + cli.cmdline(["wave", "load", "SI1", "--table", table1]) + table2 = json.dumps(([0] * (512 // 2)) + ([1] * (512 // 2))) + cli.cmdline(["wave", "load", "SI2", "--table", table2]) + assert AnalogOutput("SI1").waveform_table == AnalogOutput("SI2").waveform_table + + +def test_wave_load_tablefile(wave, mocker, tmp_path): + wavegen = WaveformGenerator(mocker.Mock()) + wavegen.load_function("SI1", "tria") + + def tria(x): + return AnalogOutput.RANGE[1] * (abs(x % 4 - 2) - 1) + + span = [-1, 3] + x = np.arange(span[0], span[1], (span[1] - span[0]) / 512) + table_tmp_json = str(tmp_path / "table.json") + with open(table_tmp_json, "w") as json_file: + json.dump(tria(x).tolist(), json_file) + cli.cmdline(["wave", "load", "SI2", "--table-file", table_tmp_json]) + assert AnalogOutput("SI1").waveform_table == AnalogOutput("SI2").waveform_table + + +def test_wave_load_tablefile_expand(wave, tmp_path): + table1_tmp_json = str(tmp_path / "table1.json") + with open(table1_tmp_json, "w") as json_file: + json.dump([0, 1], json_file) + cli.cmdline(["wave", "load", "SI1", "--table-file", table1_tmp_json]) + table2_tmp_json = str(tmp_path / "table2.json") + with open(table2_tmp_json, "w") as json_file: + json.dump(([0] * (512 // 2)) + ([1] * (512 // 2)), json_file) + cli.cmdline(["wave", "load", "SI2", "--table-file", table2_tmp_json]) + assert AnalogOutput("SI1").waveform_table == AnalogOutput("SI2").waveform_table diff --git a/tests/test_i2c.py b/tests/test_i2c.py new file mode 100644 index 00000000..9bb899f2 --- /dev/null +++ b/tests/test_i2c.py @@ -0,0 +1,228 @@ +"""Tests for pslab.bus.i2c. + +The PSLab's logic analyzer is used to verify the function of the I2C bus. Before running +the tests, connect: + SCL->LA1 + SDA->LA2 +""" + +import pytest + +from pslab.bus.i2c import I2CMaster, I2CSlave +from pslab.instrument.logic_analyzer import LogicAnalyzer +from pslab.connection import SerialHandler + +ADDRESS = 0x52 # Not a real device. +REGISTER_ADDRESS = 0x06 +WRITE_DATA = 0x0 +SCL = "LA1" +SDA = "LA2" +MICROSECONDS = 1e-6 +RELTOL = 0.05 +# Number of expected logic level changes. +SCL_START = 1 +SCL_RESTART = 2 +SCL_READ = 18 +SCL_WRITE = 18 +SCL_STOP = 1 +SDA_START = 1 +SDA_DEVICE_ADDRESS = 7 +SDA_REGISTER_ADDRESS = 4 +SDA_RESTART = 1 +SDA_READ = 0 +SDA_NACK = 0 +SDA_ACK = 2 +SDA_WRITE = 2 + + +@pytest.fixture +def master(handler: SerialHandler) -> I2CMaster: + return I2CMaster(device=handler) + + +@pytest.fixture +def slave(handler: SerialHandler) -> I2CSlave: + return I2CSlave(ADDRESS, device=handler) + + +@pytest.fixture +def la(handler: SerialHandler) -> LogicAnalyzer: + return LogicAnalyzer(handler) + + +def test_configure(la: LogicAnalyzer, master: I2CMaster, slave: I2CSlave): + frequency = 1.25e5 + master.configure(frequency) + la.capture(1, block=False) + slave._start(ADDRESS, 1) + slave._stop() + la.stop() + (scl,) = la.fetch_data() + write_start = scl[1] # First event is start bit. + write_stop = scl[-2] # Final event is stop bit. + start_to_stop = 8.5 # 9 periods, but initial and final states are the same. + period = (write_stop - write_start) / start_to_stop + assert (period * MICROSECONDS) ** -1 == pytest.approx(frequency, rel=RELTOL) + + +def test_scan(master: I2CMaster): + mcp4728_address = 0x60 + assert mcp4728_address in master.scan() + + +def test_status(master: I2CMaster): + master.configure(1.25e5) + # No status bits should be set on a newly configured bus. + assert not master._status + + +def test_start_slave(la: LogicAnalyzer, slave: I2CSlave): + la.capture(2, block=False) + slave._start(ADDRESS, 1) + la.stop() + slave._stop() + init = la.get_initial_states() + scl, sda = la.fetch_data() + + assert all([init[c] for c in [SCL, SDA]]) # Both start HIGH. + assert sda[0] < scl[0] # Start bit: SDA 1->0 while SCL is 1. + + +def test_stop_slave(la: LogicAnalyzer, slave: I2CSlave): + slave._start(ADDRESS, 1) + la.capture(2, block=False) + slave._stop() + la.stop() + init = la.get_initial_states() + scl, sda = la.fetch_data() + + assert not init[SCL] and init[SDA] # SDA starts HIGH, SCL starts LOW. + assert sda[0] < scl[0] < sda[1] # Stop bit: SDA 0->1 while SCL is 1. + + +def test_ping_slave(slave: I2CSlave): + assert not slave.ping() + + +def test_read(la: LogicAnalyzer, slave: I2CSlave): + la.capture(2, block=False) + slave.read(1, REGISTER_ADDRESS) + la.stop() + scl, sda = la.fetch_data() + + assert len(scl) == ( + SCL_START + SCL_WRITE * 2 + SCL_RESTART + SCL_WRITE + SCL_READ + SCL_STOP + ) + assert len(sda) == ( + SDA_START + + (SDA_DEVICE_ADDRESS + SDA_NACK) + + (SDA_REGISTER_ADDRESS + SDA_NACK) + + SDA_RESTART + + (SDA_DEVICE_ADDRESS + SDA_NACK) + + (SDA_READ + SDA_ACK) + ) + + +def test_read_byte(la: LogicAnalyzer, slave: I2CSlave): + la.capture(2, block=False) + slave.read_byte(REGISTER_ADDRESS) + la.stop() + scl, sda = la.fetch_data() + + assert len(scl) == ( + SCL_START + SCL_WRITE * 2 + SCL_RESTART + SCL_WRITE + SCL_READ + SCL_STOP + ) + assert len(sda) == ( + SDA_START + + (SDA_DEVICE_ADDRESS + SDA_NACK) + + (SDA_REGISTER_ADDRESS + SDA_NACK) + + SDA_RESTART + + (SDA_DEVICE_ADDRESS + SDA_NACK) + + (SDA_READ + SDA_ACK) + ) + + +def test_read_int(la: LogicAnalyzer, slave: I2CSlave): + la.capture(2, block=False) + slave.read_int(REGISTER_ADDRESS) + la.stop() + scl, sda = la.fetch_data() + + assert len(scl) == ( + SCL_START + SCL_WRITE * 2 + SCL_RESTART + SCL_WRITE + SCL_READ * 2 + SCL_STOP + ) + assert len(sda) == ( + SDA_START + + (SDA_DEVICE_ADDRESS + SDA_NACK) + + (SDA_REGISTER_ADDRESS + SDA_NACK) + + SDA_RESTART + + (SDA_DEVICE_ADDRESS + SDA_NACK) + + (SDA_READ + SDA_ACK) * 2 + ) + + +def test_read_long(la: LogicAnalyzer, slave: I2CSlave): + la.capture(2, block=False) + slave.read_long(REGISTER_ADDRESS) + la.stop() + scl, sda = la.fetch_data() + + assert len(scl) == ( + SCL_START + SCL_WRITE * 2 + SCL_RESTART + SCL_WRITE + SCL_READ * 4 + SCL_STOP + ) + assert len(sda) == ( + SDA_START + + (SDA_DEVICE_ADDRESS + SDA_NACK) + + (SDA_REGISTER_ADDRESS + SDA_NACK) + + SDA_RESTART + + (SDA_DEVICE_ADDRESS + SDA_NACK) + + (SDA_READ + SDA_ACK) * 4 + ) + + +def test_write(la: LogicAnalyzer, slave: I2CSlave): + la.capture(2, block=False) + slave.write(bytearray(b"\x00"), REGISTER_ADDRESS) + la.stop() + scl, sda = la.fetch_data() + + assert len(scl) == (SCL_START + SCL_WRITE * 3 + SCL_STOP) + assert len(sda) == ( + SDA_START + SDA_DEVICE_ADDRESS + SDA_REGISTER_ADDRESS + SDA_WRITE + SDA_ACK + ) + + +def test_write_byte(la: LogicAnalyzer, slave: I2CSlave): + la.capture(2, block=False) + slave.write_byte(WRITE_DATA, REGISTER_ADDRESS) + la.stop() + scl, sda = la.fetch_data() + + assert len(scl) == (SCL_START + SCL_WRITE * 3 + SCL_STOP) + assert len(sda) == ( + SDA_START + SDA_DEVICE_ADDRESS + SDA_REGISTER_ADDRESS + SDA_WRITE + SDA_ACK + ) + + +def test_write_int(la: LogicAnalyzer, slave: I2CSlave): + la.capture(2, block=False) + slave.write_int(WRITE_DATA, REGISTER_ADDRESS) + la.stop() + scl, sda = la.fetch_data() + + assert len(scl) == (SCL_START + SCL_WRITE * 4 + SCL_STOP) + assert len(sda) == ( + SDA_START + SDA_DEVICE_ADDRESS + SDA_REGISTER_ADDRESS + SDA_WRITE * 2 + SDA_ACK + ) + + +def test_write_long(la: LogicAnalyzer, slave: I2CSlave): + la.capture(2, block=False) + slave.write_long(WRITE_DATA, REGISTER_ADDRESS) + la.stop() + scl, sda = la.fetch_data() + + assert len(scl) == (SCL_START + SCL_WRITE * 6 + SCL_STOP) + assert len(sda) == ( + SDA_START + SDA_DEVICE_ADDRESS + SDA_REGISTER_ADDRESS + SDA_WRITE * 4 + SDA_ACK + ) diff --git a/tests/test_logic_analyzer.py b/tests/test_logic_analyzer.py new file mode 100644 index 00000000..550a929b --- /dev/null +++ b/tests/test_logic_analyzer.py @@ -0,0 +1,282 @@ +"""Tests for pslab.instrument.logic_analyzer. + +When integration testing, the PSLab's PWM output is used to generate a signal +which is analyzed by the logic analyzer. Before running the integration tests, +connect: + SQ1 -> LA1 + SQ2 -> LA2 + SQ3 -> LA3 + SQ4 -> LA4 +""" + +import time + +import numpy as np +import pytest + +import pslab.protocol as CP +from pslab.instrument.logic_analyzer import LogicAnalyzer +from pslab.instrument.waveform_generator import PWMGenerator + + +EVENTS = 2495 +FREQUENCY = 1e5 +DUTY_CYCLE = 0.5 +LOW_FREQUENCY = 100 +LOWER_FREQUENCY = 10 +MICROSECONDS = 1e6 +TWO_CLOCK_CYCLES = 2 * CP.CLOCK_RATE**-1 * MICROSECONDS + + +@pytest.fixture +def la(handler, request): + """Turn on PWM output and return a LogicAnalyzer instance.""" + pwm = PWMGenerator(handler) + enable_pwm(pwm, request.node.name) + return LogicAnalyzer(handler) + + +def enable_pwm(pwm: PWMGenerator, test_name: str): + """Enable PWM output for integration testing.""" + low_frequency_tests = ( + "test_capture_four_low_frequency", + "test_capture_four_lower_frequency", + "test_capture_four_lowest_frequency", + "test_capture_timeout", + "test_get_states", + ) + if test_name in low_frequency_tests: + frequency = LOW_FREQUENCY + elif test_name == "test_capture_four_too_low_frequency": + frequency = LOWER_FREQUENCY + else: + frequency = FREQUENCY + + pwm.generate( + ["SQ1", "SQ2", "SQ3", "SQ4"], + frequency, + DUTY_CYCLE, + 0, + ) + + +def test_capture_one_channel(la): + t = la.capture(1, EVENTS) + assert len(t[0]) == EVENTS + + +def test_capture_two_channels(la): + t1, t2 = la.capture(2, EVENTS) + assert len(t1) == len(t2) == EVENTS + + +def test_capture_four_channels(la): + t1, t2, t3, t4 = la.capture(4, EVENTS) + assert len(t1) == len(t2) == len(t3) == len(t4) == EVENTS + + +def test_capture_four_low_frequency(la): + e2e_time = (LOW_FREQUENCY**-1) / 2 + t1 = la.capture(4, 10, e2e_time=e2e_time)[0] + # When capturing every edge, the accuracy seems to depend on + # the PWM prescaler as well as the logic analyzer prescaler. + pwm_abstol = TWO_CLOCK_CYCLES * LogicAnalyzer._PRESCALERS[2] + assert np.array(9 * [e2e_time * MICROSECONDS]) == pytest.approx( + np.diff(t1), abs=TWO_CLOCK_CYCLES * LogicAnalyzer._PRESCALERS[1] + pwm_abstol + ) + + +def test_capture_four_lower_frequency(la): + e2e_time = LOW_FREQUENCY**-1 + t1 = la.capture(4, 10, modes=4 * ["rising"], e2e_time=e2e_time)[0] + assert np.array(9 * [e2e_time * MICROSECONDS]) == pytest.approx( + np.diff(t1), abs=TWO_CLOCK_CYCLES * LogicAnalyzer._PRESCALERS[2] + ) + + +def test_capture_four_lowest_frequency(la): + e2e_time = (LOW_FREQUENCY**-1) * 16 + t1 = la.capture(4, 10, modes=4 * ["sixteen rising"], e2e_time=e2e_time, timeout=2)[ + 0 + ] + assert np.array(9 * [e2e_time * MICROSECONDS]) == pytest.approx( + np.diff(t1), abs=TWO_CLOCK_CYCLES * LogicAnalyzer._PRESCALERS[3] + ) + + +def test_capture_four_too_low_frequency(la): + e2e_time = (LOWER_FREQUENCY**-1) * 4 + with pytest.raises(ValueError): + la.capture(4, 10, modes=4 * ["four rising"], e2e_time=e2e_time, timeout=5) + + +def test_capture_nonblocking(la): + la.capture(1, EVENTS, block=False) + time.sleep(EVENTS * FREQUENCY**-1) + t = la.fetch_data() + assert len(t[0]) >= EVENTS + + +def test_capture_rising_edges(la): + events = 100 + t1, t2 = la.capture(2, events, modes=["any", "rising"]) + expected = FREQUENCY**-1 * MICROSECONDS / 2 + result = t2 - t1 - (t2 - t1)[0] + assert np.arange(0, expected * events, expected) == pytest.approx( + result, abs=TWO_CLOCK_CYCLES + ) + + +def test_capture_four_rising_edges(la): + events = 100 + t1, t2 = la.capture(2, events, modes=["rising", "four rising"]) + expected = FREQUENCY**-1 * MICROSECONDS * 3 + result = t2 - t1 - (t2 - t1)[0] + assert np.arange(0, expected * events, expected) == pytest.approx( + result, abs=TWO_CLOCK_CYCLES + ) + + +def test_capture_sixteen_rising_edges(la): + events = 100 + t1, t2 = la.capture(2, events, modes=["four rising", "sixteen rising"]) + expected = FREQUENCY**-1 * MICROSECONDS * 12 + result = t2 - t1 - (t2 - t1)[0] + assert np.arange(0, expected * events, expected) == pytest.approx( + result, abs=TWO_CLOCK_CYCLES + ) + + +def test_capture_too_many_events(la): + with pytest.raises(ValueError): + la.capture(1, CP.MAX_SAMPLES // 4 + 1) + + +def test_capture_too_many_channels(la): + with pytest.raises(ValueError): + la.capture(5) + + +def test_measure_frequency(la): + frequency = la.measure_frequency("LA1", timeout=0.1) + assert FREQUENCY == pytest.approx(frequency) + + +def test_measure_frequency_firmware(la): + frequency = la.measure_frequency("LA2", timeout=0.1, simultaneous_oscilloscope=True) + assert FREQUENCY == pytest.approx(frequency) + + +def test_measure_interval(la): + la.configure_trigger("LA1", "falling") + interval = la.measure_interval( + channels=["LA1", "LA2"], modes=["rising", "falling"], timeout=0.1 + ) + expected_interval = FREQUENCY**-1 * MICROSECONDS * 0.5 + assert expected_interval == pytest.approx(interval, abs=TWO_CLOCK_CYCLES) + + +def test_measure_interval_same_channel(la): + la.configure_trigger("LA1", "falling") + interval = la.measure_interval( + channels=["LA1", "LA1"], modes=["rising", "falling"], timeout=0.1 + ) + expected_interval = FREQUENCY**-1 * DUTY_CYCLE * MICROSECONDS + assert expected_interval == pytest.approx(interval, abs=TWO_CLOCK_CYCLES) + + +def test_measure_interval_same_channel_any(la): + la.configure_trigger("LA1", "falling") + interval = la.measure_interval( + channels=["LA1", "LA1"], modes=["any", "any"], timeout=0.1 + ) + expected_interval = FREQUENCY**-1 * DUTY_CYCLE * MICROSECONDS + assert expected_interval == pytest.approx(interval, abs=TWO_CLOCK_CYCLES) + + +def test_measure_interval_same_channel_four_rising(la): + la.configure_trigger("LA1", "falling") + interval = la.measure_interval( + channels=["LA1", "LA1"], modes=["rising", "four rising"], timeout=0.1 + ) + expected_interval = FREQUENCY**-1 * 3 * MICROSECONDS + assert expected_interval == pytest.approx(interval, abs=TWO_CLOCK_CYCLES) + + +def test_measure_interval_same_channel_sixteen_rising(la): + la.configure_trigger("LA1", "falling") + interval = la.measure_interval( + channels=["LA1", "LA1"], modes=["rising", "sixteen rising"], timeout=0.1 + ) + expected_interval = FREQUENCY**-1 * 15 * MICROSECONDS + assert expected_interval == pytest.approx(interval, abs=TWO_CLOCK_CYCLES) + + +def test_measure_interval_same_channel_same_event(la): + la.configure_trigger("LA1", "falling") + interval = la.measure_interval( + channels=["LA3", "LA3"], modes=["rising", "rising"], timeout=0.1 + ) + expected_interval = FREQUENCY**-1 * MICROSECONDS + assert expected_interval == pytest.approx(interval, abs=TWO_CLOCK_CYCLES) + + +def test_measure_duty_cycle(la): + period, duty_cycle = la.measure_duty_cycle("LA4", timeout=0.1) + expected_period = FREQUENCY**-1 * MICROSECONDS + assert (expected_period, DUTY_CYCLE) == pytest.approx( + (period, duty_cycle), abs=TWO_CLOCK_CYCLES + ) + + +def test_get_xy_rising_trigger(la): + la.configure_trigger("LA1", "rising") + t = la.capture(1, 100) + _, y = la.get_xy(t) + assert y[0] + + +def test_get_xy_falling_trigger(la): + la.configure_trigger("LA1", "falling") + t = la.capture(1, 100) + _, y = la.get_xy(t) + assert not y[0] + + +def test_get_xy_rising_capture(la): + t = la.capture(1, 100, modes=["rising"]) + _, y = la.get_xy(t) + assert sum(y) == 100 + + +def test_get_xy_falling_capture(la): + t = la.capture(1, 100, modes=["falling"]) + _, y = la.get_xy(t) + assert sum(~y) == 100 + + +def test_stop(la): + la.capture(1, EVENTS, modes=["sixteen rising"], block=False) + time.sleep(EVENTS * FREQUENCY**-1) + progress_time = time.time() + progress = la.get_progress() + la.stop() + stop_time = time.time() + time.sleep(EVENTS * FREQUENCY**-1) + assert progress < CP.MAX_SAMPLES // 4 + abstol = FREQUENCY * (stop_time - progress_time) + assert progress == pytest.approx(la.get_progress(), abs=abstol) + + +def test_get_states(la): + time.sleep(LOW_FREQUENCY**-1) + states = la.get_states() + expected_states = {"LA1": True, "LA2": True, "LA3": True, "LA4": True} + assert states == expected_states + + +def test_count_pulses(la): + interval = 0.2 + pulses = la.count_pulses("LA2", interval) + expected_pulses = FREQUENCY * interval + assert expected_pulses == pytest.approx(pulses, rel=0.1) # Pretty bad accuracy. diff --git a/tests/test_motor.py b/tests/test_motor.py new file mode 100644 index 00000000..4330e87e --- /dev/null +++ b/tests/test_motor.py @@ -0,0 +1,34 @@ +"""Tests for pslab.external.motor. + +Connect SQ1 -> LA1. +""" + +import pytest + +from pslab.external.motor import Servo +from pslab.instrument.logic_analyzer import LogicAnalyzer +from pslab.instrument.waveform_generator import PWMGenerator +from pslab.connection import SerialHandler + +RELTOL = 0.01 + + +@pytest.fixture +def servo(handler: SerialHandler) -> Servo: + return Servo("SQ1", PWMGenerator(handler)) + + +@pytest.fixture +def la(handler: SerialHandler) -> LogicAnalyzer: + return LogicAnalyzer(handler) + + +def test_set_angle(servo: Servo, la: LogicAnalyzer): + servo.angle = 90 + wavelength, duty_cycle = la.measure_duty_cycle("LA1") + assert wavelength * duty_cycle == pytest.approx(1500, rel=RELTOL) + + +def test_get_angle(servo: Servo): + servo.angle = 90 + assert servo.angle == 90 diff --git a/tests/test_mq135.py b/tests/test_mq135.py new file mode 100644 index 00000000..2773fbb7 --- /dev/null +++ b/tests/test_mq135.py @@ -0,0 +1,42 @@ +import pytest + +from pslab.external.gas_sensor import MQ135 + +R_LOAD = 22e3 +R0 = 50e3 +VCC = 5 +VOUT = 2 +A, B, C, D = *MQ135._TEMPERATURE_CORRECTION, MQ135._HUMIDITY_CORRECTION +E, F = MQ135._PARAMS["CO2"] +STANDARD_CORRECTION = A * 20 ** 2 + B * 20 + C + D * (0.65 - 0.65) +EXPECTED_SENSOR_RESISTANCE = (VCC / VOUT - 1) * R_LOAD / STANDARD_CORRECTION +CALIBRATION_CONCENTRATION = E * (EXPECTED_SENSOR_RESISTANCE / R0) ** F + + +@pytest.fixture +def mq135(mocker): + mock = mocker.patch("pslab.external.gas_sensor.Multimeter") + mock().measure_voltage.return_value = VOUT + return MQ135("CO2", R_LOAD) + + +def test_correction(mq135): + assert mq135._correction == STANDARD_CORRECTION + + +def test_sensor_resistance(mq135): + assert mq135._sensor_resistance == EXPECTED_SENSOR_RESISTANCE + + +def test_measure_concentration(mq135): + mq135.r0 = R0 + assert mq135.measure_concentration() == E * (EXPECTED_SENSOR_RESISTANCE / R0) ** F + + +def test_measure_concentration_r0_unset(mq135): + with pytest.raises(TypeError): + mq135.measure_concentration() + + +def test_measure_r0(mq135): + assert mq135.measure_r0(CALIBRATION_CONCENTRATION) == pytest.approx(R0) diff --git a/tests/test_multimeter.py b/tests/test_multimeter.py new file mode 100644 index 00000000..d83581f8 --- /dev/null +++ b/tests/test_multimeter.py @@ -0,0 +1,54 @@ +"""Tests for PSL.multimeter. + +Before running the tests, connect: + PV1 -> 10K resistor -> VOL + RES -> 10K resistor -> GND + CAP -> 1 nF capacitor -> GND +""" + +import pytest + +from pslab.instrument.multimeter import Multimeter +from pslab.instrument.power_supply import PowerSupply +from pslab.connection import SerialHandler + + +RELTOL = 0.05 + + +@pytest.fixture +def multi(handler: SerialHandler) -> Multimeter: + return Multimeter(handler) + + +@pytest.fixture +def source(handler: SerialHandler): + ps = PowerSupply() + ps.pv1.voltage = 2.2 + + +def test_measure_resistance(multi: Multimeter): + assert multi.measure_resistance() == pytest.approx(1e4, rel=RELTOL) + + +def test_measure_voltage(multi: Multimeter, source): + assert multi.measure_voltage("VOL") == pytest.approx(2.2, rel=RELTOL) + + +def test_voltmeter_autorange(multi: Multimeter): + assert multi._voltmeter_autorange("CH1") <= 3.3 + + +def test_calibrate_capacitance(multi: Multimeter): + multi.calibrate_capacitance() + # Need bigger tolerance because measurement will be 1 nF + actual stray + # capacitance (ca 50 pF). + assert multi._stray_capacitance == pytest.approx(1e-9, rel=3 * RELTOL) + + +def test_measure_capacitance(multi: Multimeter): + assert multi.measure_capacitance() == pytest.approx(1e-9, rel=RELTOL) + + +def test_measure_rc_capacitance(multi: Multimeter): + assert multi._measure_rc_capacitance() == pytest.approx(1e-9, rel=RELTOL) diff --git a/tests/test_oscilloscope.py b/tests/test_oscilloscope.py new file mode 100644 index 00000000..c67854ed --- /dev/null +++ b/tests/test_oscilloscope.py @@ -0,0 +1,111 @@ +"""Tests for PSL.oscilloscope. + +The PSLab's waveform generator is used to generate a signal which is sampled by the +oscilloscope. Before running the tests, connect: + SI1 -> CH1 + SI2 -> CH2 + SI1 -> CH3 +""" + +import numpy as np +import pytest + +from pslab.instrument.oscilloscope import Oscilloscope +from pslab.instrument.waveform_generator import WaveformGenerator + + +FREQUENCY = 1000 +MICROSECONDS = 1e-6 +ABSTOL = 4 * (16.5 - (-16.5)) / (2**10 - 1) # Four times lowest CH1/CH2 resolution. + + +@pytest.fixture +def scope(handler): + """Enable waveform generator and return an Oscilloscope instance.""" + wave = WaveformGenerator(handler) + wave.generate(["SI1", "SI2"], FREQUENCY) + return Oscilloscope(handler) + + +def count_zero_crossings(x, y): + sample_rate = (np.diff(x)[0] * MICROSECONDS) ** -1 + samples_per_period = sample_rate / FREQUENCY + zero_crossings = np.where(np.diff(np.sign(y)))[0] + real_crossings = np.where(np.diff(zero_crossings) > samples_per_period * 0.01) + real_crossings = np.append(real_crossings, True) + + if len(real_crossings) % 1: + if y[0] * y[-1] <= 0: + return len(real_crossings) + 1 + + return len(real_crossings) + + +def verify_periods(x, y, channel, periods=1): + zero_crossings = count_zero_crossings(x, y) + assert zero_crossings == 2 * periods + assert y[0] == pytest.approx(y[-1], abs=ABSTOL) + + +def test_capture_one_12bit(scope): + _, y = scope.capture(channels=1, samples=1000, timegap=1) + y.sort() + resolution = min(np.diff(y)[np.diff(y) > 0]) + expected = (16.5 - (-16.5)) / (2**12 - 1) + assert resolution == pytest.approx(expected) + + +def test_capture_one_high_speed(scope): + x, y = scope.capture(channels=1, samples=2000, timegap=0.5) + verify_periods(x, y, scope._channels["CH1"]) + + +def test_capture_one_trigger(scope): + _, y = scope.capture(channels=1, samples=1, timegap=1, trigger=0) + assert y[0] == pytest.approx(0, abs=ABSTOL) + + +def test_capture_two(scope): + x, y1, y2 = scope.capture(channels=2, samples=500, timegap=2) + verify_periods(x, y1, scope._channels["CH1"]) + verify_periods(x, y2, scope._channels["CH2"]) + + +def test_capture_three(scope): + x, y1, y2, y3 = scope.capture(channels=3, samples=500, timegap=2) + verify_periods(x, y1, scope._channels["CH1"]) + verify_periods(x, y2, scope._channels["CH2"]) + verify_periods(x, y3, scope._channels["CH3"]) + + +def test_capture_four(scope): + x, y1, y2, y3, _ = scope.capture(channels=4, samples=500, timegap=2) + verify_periods(x, y1, scope._channels["CH1"]) + verify_periods(x, y2, scope._channels["CH2"]) + verify_periods(x, y3, scope._channels["CH3"]) + + +def test_capture_invalid_channel_one(scope): + with pytest.raises(ValueError): + scope.capture(channels="BAD", samples=200, timegap=2) + + +def test_capture_timegap_too_small(scope): + with pytest.raises(ValueError): + scope.capture(channels=1, samples=200, timegap=0.2) + + +def test_capture_too_many_channels(scope): + with pytest.raises(ValueError): + scope.capture(channels=5, samples=200, timegap=2) + + +def test_capture_too_many_samples(scope): + with pytest.raises(ValueError): + scope.capture(channels=4, samples=3000, timegap=2) + + +def test_select_range(scope): + scope.select_range("CH1", 1.5) + _, y = scope.capture(channels=1, samples=1000, timegap=1) + assert 1.5 <= max(y) <= 1.65 diff --git a/tests/test_power_supply.py b/tests/test_power_supply.py new file mode 100644 index 00000000..ccb22120 --- /dev/null +++ b/tests/test_power_supply.py @@ -0,0 +1,80 @@ +"""Tests for PSL.power_supply. + +When running integration tests, connect: + PV1 -> CH1 + PV2 -> CH2 + PV3 -> VOL + PCS -> 100R -> GND + PCS -> CH3 +""" + +import time + +import pytest +import numpy as np + +from pslab.connection import SerialHandler +from pslab.instrument.multimeter import Multimeter +from pslab.instrument.power_supply import PowerSupply + +RELTOL = 0.1 +ABSTOL = 0.1 + + +@pytest.fixture +def power(handler: SerialHandler) -> PowerSupply: + return PowerSupply(handler) + + +@pytest.fixture +def multi(handler: SerialHandler) -> Multimeter: + return Multimeter(handler) + + +def test_set_voltage_pv1(power: PowerSupply, multi: Multimeter): + voltages = np.arange(-5, 5, 0.1) + measured = np.zeros(len(voltages)) + + for i, v in enumerate(voltages): + power.pv1 = v + time.sleep(0.01) + measured[i] = multi.measure_voltage("CH1") + + assert measured == pytest.approx(voltages, rel=RELTOL * 2, abs=ABSTOL * 2) + + +def test_set_voltage_pv2(power: PowerSupply, multi: Multimeter): + voltages = np.arange(-3.3, 3.3, 0.1) + measured = np.zeros(len(voltages)) + + for i, v in enumerate(voltages): + power.pv2 = v + time.sleep(0.01) + measured[i] = multi.measure_voltage("CH2") + + assert measured == pytest.approx(voltages, rel=RELTOL * 2, abs=ABSTOL * 2) + + +def test_set_voltage_pv3(power: PowerSupply, multi: Multimeter): + voltages = np.arange(0, 3.3, 0.1) + measured = np.zeros(len(voltages)) + + for i, v in enumerate(voltages): + power.pv3 = v + time.sleep(0.01) + measured[i] = multi.measure_voltage("VOL") + + assert measured == pytest.approx(voltages, rel=RELTOL * 2, abs=ABSTOL * 2) + + +def test_set_current(power: PowerSupply, multi: Multimeter): + currents = np.arange(0, 2.5e-3, 1e-4) + measured = np.zeros(len(currents)) + + for i, c in enumerate(currents): + power.pcs = c + time.sleep(0.01) + measured[i] = multi.measure_voltage("CH3") + + resistor = 100 + assert measured == pytest.approx(currents * resistor, rel=RELTOL, abs=ABSTOL) diff --git a/tests/test_serial_handler.py b/tests/test_serial_handler.py new file mode 100644 index 00000000..dcbf1a1a --- /dev/null +++ b/tests/test_serial_handler.py @@ -0,0 +1,127 @@ +import pytest +from serial import SerialException +from serial.tools.list_ports_common import ListPortInfo + +import pslab.protocol as CP +from pslab.connection import detect, SerialHandler + +VERSION = "PSLab vMOCK\n" +PORT = "mock_port" +PORT2 = "mock_port_2" + + +def mock_ListPortInfo(found=True, multiple=False): + if found: + if multiple: + yield from [ListPortInfo(device=PORT), ListPortInfo(device=PORT2)] + else: + yield ListPortInfo(device=PORT) + else: + return + + +@pytest.fixture +def mock_serial(mocker): + serial_patch = mocker.patch("pslab.connection._serial.serial.Serial") + serial_patch().readline.return_value = VERSION.encode() + serial_patch().is_open = False + return serial_patch + + +@pytest.fixture +def mock_handler(mocker, mock_serial, mock_list_ports): + mocker.patch("pslab.connection._serial._check_serial_access_permission") + mock_list_ports.grep.return_value = mock_ListPortInfo() + return SerialHandler() + + +@pytest.fixture +def mock_list_ports(mocker): + return mocker.patch("pslab.connection.list_ports") + + +def test_detect(mocker, mock_serial, mock_list_ports): + mock_list_ports.grep.return_value = mock_ListPortInfo(multiple=True) + assert len(detect()) == 2 + + +def test_connect_scan_port(mocker, mock_serial, mock_list_ports): + mock_list_ports.grep.return_value = mock_ListPortInfo() + mocker.patch("pslab.connection._serial._check_serial_access_permission") + SerialHandler() + mock_serial().open.assert_called() + + +def test_connect_scan_failure(mocker, mock_serial, mock_list_ports): + mock_list_ports.grep.return_value = mock_ListPortInfo(found=False) + mocker.patch("pslab.connection._serial._check_serial_access_permission") + with pytest.raises(SerialException): + SerialHandler() + + +def test_connect_multiple_connected(mocker, mock_serial, mock_list_ports): + mock_list_ports.grep.return_value = mock_ListPortInfo(multiple=True) + mocker.patch("pslab.connection._serial._check_serial_access_permission") + with pytest.raises(RuntimeError): + SerialHandler() + + +def test_disconnect(mock_serial, mock_handler): + mock_handler.disconnect() + mock_serial().close.assert_called() + + +def test_reconnect(mock_serial, mock_handler, mock_list_ports): + mock_list_ports.grep.return_value = mock_ListPortInfo() + mock_handler.reconnect() + mock_serial().close.assert_called() + + +def test_get_version(mock_serial, mock_handler): + mock_handler.get_version() + mock_serial().write.assert_called_with(CP.GET_VERSION) + assert mock_handler.version == VERSION + + +def test_get_ack_success(mock_serial, mock_handler): + success = 1 + mock_serial().read.return_value = CP.Byte.pack(success) + assert mock_handler.get_ack() == success + + +def test_get_ack_failure(mock_serial, mock_handler): + mock_serial().read.return_value = b"" + with pytest.raises(SerialException): + mock_handler.get_ack() + + +def test_send_bytes(mock_serial, mock_handler): + mock_handler.send_byte(CP.Byte.pack(0xFF)) + mock_serial().write.assert_called_with(CP.Byte.pack(0xFF)) + + +def test_send_byte(mock_serial, mock_handler): + mock_handler.send_byte(0xFF) + mock_serial().write.assert_called_with(CP.Byte.pack(0xFF)) + + +def test_receive(mock_serial, mock_handler): + mock_serial().read.return_value = CP.Byte.pack(0xFF) + r = mock_handler.get_byte() + mock_serial().read.assert_called_with(1) + assert r == 0xFF + + +def test_receive_failure(mock_serial, mock_handler): + mock_serial().read.return_value = b"" + with pytest.raises(SerialException): + mock_handler.get_byte() + + +def test_get_integer_unsupported_size(mock_serial, mock_handler): + with pytest.raises(ValueError): + mock_handler._get_integer_type(size=3) + + +def test_list_ports(mock_serial, mock_handler): + assert isinstance(mock_handler._list_ports(), list) diff --git a/tests/test_spi.py b/tests/test_spi.py new file mode 100644 index 00000000..ecd73d92 --- /dev/null +++ b/tests/test_spi.py @@ -0,0 +1,312 @@ +"""Tests for pslab.bus.spi. + +The PSLab's logic analyzer and PWM output are used to verify the function of the SPI +bus. Before running the tests, connect: + SCK -> LA1 + SDO -> LA2 + SDI -> SQ1 and LA4 + SPI.CS -> LA3 +""" + +import pytest +import re +from numpy import ndarray + +from pslab.bus.spi import SPIMaster, SPISlave +from pslab.instrument.logic_analyzer import LogicAnalyzer +from pslab.instrument.waveform_generator import PWMGenerator +from pslab.connection import SerialHandler + +SPI_SUPPORTED_DEVICES = [ + # "PSLab vMOCK", # Uncomment after adding recording json files. + "PSLab V6\n", +] + +WRITE_DATA8 = 0b10100101 +WRITE_DATA16 = 0xAA55 +SCK = "LA1" +SDO = "LA2" +SDI = ["LA4", "SQ1"] +CS = "LA3" +SPIMaster._primary_prescaler = PPRE = 0 +SPIMaster._secondary_prescaler = SPRE = 0 +PWM_FERQUENCY = SPIMaster._frequency * 2 / 3 +MICROSECONDS = 1e-6 +RELTOL = 0.05 +# Number of expected logic level changes. +CS_START = 1 +CS_STOP = 1 +SCK_WRITE8 = 16 +SCK_WRITE16 = 2 * 16 +SDO_WRITE_DATA8 = 8 +SDO_WRITE_DATA16 = 16 + + +@pytest.fixture +def master(handler: SerialHandler) -> SPIMaster: + if handler.version not in SPI_SUPPORTED_DEVICES: + pytest.skip("SPI not supported by this device.") + spi_master = SPIMaster(device=handler) + yield spi_master + spi_master.set_parameters() + + +@pytest.fixture +def slave(handler: SerialHandler) -> SPISlave: + if handler.version not in SPI_SUPPORTED_DEVICES: + pytest.skip("SPI not supported by this device.") + return SPISlave(device=handler) + + +@pytest.fixture +def la(handler: SerialHandler) -> LogicAnalyzer: + pwm = PWMGenerator(handler) + pwm.generate(SDI[1], PWM_FERQUENCY, 0.5) + return LogicAnalyzer(handler) + + +def verify_value( + value: int, + sck_timestamps: ndarray, + sdi_initstate: int, + sdi_timestamps: ndarray, + smp: int = 0, +): + sck_ts = sck_timestamps[smp::2] + pwm_half_period = ((1 / PWM_FERQUENCY) * 1e6) / 2 # microsecond + + pattern = "" + for t in sck_ts: + d, m = divmod(t - sdi_timestamps[0], pwm_half_period) + if m == pytest.approx(0, abs=0.1) or m == pytest.approx( + pwm_half_period, abs=0.1 + ): + pattern += "[0,1]" + elif d % 2: + pattern += "1" if sdi_initstate else "0" + else: + pattern += "0" if sdi_initstate else "1" + + pattern = re.compile(pattern) + bits = len(sck_ts) + value = bin(value)[2:].zfill(bits) + + return bool(pattern.match(value)) + + +def test_set_parameter_frequency(la: LogicAnalyzer, master: SPIMaster, slave: SPISlave): + # frequency 166666.66666666666 + ppre = 0 + spre = 2 + master.set_parameters(primary_prescaler=ppre, secondary_prescaler=spre) + la.capture(1, block=False) + slave.write8(0) + la.stop() + (sck,) = la.fetch_data() + write_start = sck[0] + write_stop = sck[-2] # Output data on rising edge only (in mode 0) + start_to_stop = 7 + period = (write_stop - write_start) / start_to_stop + assert (period * MICROSECONDS) ** -1 == pytest.approx(master._frequency, rel=RELTOL) + + +@pytest.mark.parametrize("ckp", [0, 1]) +def test_set_parameter_clock_polarity( + la: LogicAnalyzer, master: SPIMaster, slave: SPISlave, ckp: int +): + master.set_parameters(CKP=ckp) + assert la.get_states()[SCK] == bool(ckp) + + +@pytest.mark.parametrize("cke", [0, 1]) +def test_set_parameter_clock_edge( + la: LogicAnalyzer, master: SPIMaster, slave: SPISlave, cke: int +): + master.set_parameters(CKE=cke) + la.capture(2, block=False) + slave.write8(WRITE_DATA8) + la.stop() + (sck, sdo) = la.fetch_data() + idle_to_active = sck[0] + first_bit = sdo[0] + # Serial output data changes on transition + # {0: from Idle clock state to active state (first event before data change), + # 1: from active clock state to Idle state (data change before first event)}. + assert first_bit < idle_to_active == bool(cke) + + +@pytest.mark.parametrize("smp", [0, 1]) +def test_set_parameter_smp( + la: LogicAnalyzer, master: SPIMaster, slave: SPISlave, pwm: PWMGenerator, smp: int +): + master.set_parameters(SMP=smp) + la.capture([SCK, SDI[0]], block=False) + value = slave.read8() + la.stop() + (sck, sdi) = la.fetch_data() + sdi_initstate = la.get_initial_states()[SDI[0]] + + assert verify_value(value, sck, sdi_initstate, sdi, smp) + + +def test_chip_select(la: LogicAnalyzer, slave: SPISlave): + assert la.get_states()[CS] + + la.capture(CS, block=False) + slave._start() + slave._stop() + la.stop() + (cs,) = la.fetch_data() + assert len(cs) == (CS_START + CS_STOP) + + +def test_write8(la: LogicAnalyzer, slave: SPISlave): + la.capture(3, block=False) + slave.write8(WRITE_DATA8) + la.stop() + (sck, sdo, cs) = la.fetch_data() + + assert len(cs) == (CS_START + CS_STOP) + assert len(sck) == SCK_WRITE8 + assert len(sdo) == SDO_WRITE_DATA8 + + +def test_write16(la: LogicAnalyzer, slave: SPISlave): + la.capture(3, block=False) + slave.write16(WRITE_DATA16) + la.stop() + (sck, sdo, cs) = la.fetch_data() + + assert len(cs) == (CS_START + CS_STOP) + assert len(sck) == SCK_WRITE16 + assert len(sdo) == SDO_WRITE_DATA16 + + +def test_read8(la: LogicAnalyzer, slave: SPISlave): + la.capture(4, block=False) + value = slave.read8() + la.stop() + (sck, sdo, cs, sdi) = la.fetch_data() + sdi_initstate = la.get_initial_states()[SDI[0]] + + assert len(cs) == (CS_START + CS_STOP) + assert len(sck) == SCK_WRITE8 + assert len(sdo) == 0 + assert verify_value(value, sck, sdi_initstate, sdi) + + +def test_read16(la: LogicAnalyzer, slave: SPISlave): + la.capture(4, block=False) + value = slave.read16() + la.stop() + (sck, sdo, cs, sdi) = la.fetch_data() + sdi_initstate = la.get_initial_states()[SDI[0]] + + assert len(cs) == (CS_START + CS_STOP) + assert len(sck) == SCK_WRITE16 + assert len(sdo) == 0 + assert verify_value(value, sck, sdi_initstate, sdi) + + +def test_transfer8(la: LogicAnalyzer, slave: SPISlave): + la.capture(4, block=False) + value = slave.transfer8(WRITE_DATA8) + la.stop() + (sck, sdo, cs, sdi) = la.fetch_data() + sdi_initstate = la.get_initial_states()[SDI[0]] + + assert len(cs) == (CS_START + CS_STOP) + assert len(sck) == SCK_WRITE8 + assert len(sdo) == SDO_WRITE_DATA8 + assert verify_value(value, sck, sdi_initstate, sdi) + + +def test_transfer16(la: LogicAnalyzer, slave: SPISlave): + la.capture(4, block=False) + value = slave.transfer16(WRITE_DATA16) + la.stop() + (sck, sdo, cs, sdi) = la.fetch_data() + sdi_initstate = la.get_initial_states()[SDI[0]] + + assert len(cs) == (CS_START + CS_STOP) + assert len(sck) == SCK_WRITE16 + assert len(sdo) == SDO_WRITE_DATA16 + assert verify_value(value, sck, sdi_initstate, sdi) + + +def test_write8_bulk(la: LogicAnalyzer, slave: SPISlave): + la.capture(3, block=False) + slave.write8_bulk([WRITE_DATA8, WRITE_DATA8]) + la.stop() + (sck, sdo, cs) = la.fetch_data() + + assert len(cs) == (CS_START + CS_STOP) + assert len(sck) == SCK_WRITE8 + SCK_WRITE8 + assert len(sdo) == SDO_WRITE_DATA8 + SDO_WRITE_DATA16 + + +def test_write16_bulk(la: LogicAnalyzer, slave: SPISlave): + la.capture(3, block=False) + slave.write16_bulk([WRITE_DATA16, WRITE_DATA16]) + la.stop() + (sck, sdo, cs) = la.fetch_data() + + assert len(cs) == (CS_START + CS_STOP) + assert len(sck) == SCK_WRITE16 + SCK_WRITE16 + assert len(sdo) == SDO_WRITE_DATA16 + SDO_WRITE_DATA16 + + +def test_read8_bulk(la: LogicAnalyzer, slave: SPISlave): + la.capture(4, block=False) + value = slave.read8_bulk(2) + la.stop() + (sck, sdo, cs, sdi) = la.fetch_data() + sdi_initstate = la.get_initial_states()[SDI[0]] + + assert len(cs) == (CS_START + CS_STOP) + assert len(sck) == SCK_WRITE8 + SCK_WRITE8 + assert len(sdo) == 0 + assert verify_value(value[0], sck, sdi_initstate, sdi[:16]) + assert verify_value(value[1], sck, sdi_initstate, sdi[16:]) + + +def test_read16_bulk(la: LogicAnalyzer, slave: SPISlave): + la.capture(4, block=False) + value = slave.read16_bulk(2) + la.stop() + (sck, sdo, cs, sdi) = la.fetch_data() + sdi_initstate = la.get_initial_states()[SDI[0]] + + assert len(cs) == (CS_START + CS_STOP) + assert len(sck) == SCK_WRITE16 + SCK_WRITE16 + assert len(sdo) == 0 + assert verify_value(value[0], sck, sdi_initstate, sdi[:32]) + assert verify_value(value[1], sck, sdi_initstate, sdi[32:]) + + +def test_transfer8_bulk(la: LogicAnalyzer, slave: SPISlave): + la.capture(4, block=False) + value = slave.transfer8_bulk([WRITE_DATA8, WRITE_DATA8]) + la.stop() + (sck, sdo, cs, sdi) = la.fetch_data() + sdi_initstate = la.get_initial_states()[SDI[0]] + + assert len(cs) == (CS_START + CS_STOP) + assert len(sck) == SCK_WRITE8 + SCK_WRITE8 + assert len(sdo) == 0 + assert verify_value(value[0], sck, sdi_initstate, sdi[:16]) + assert verify_value(value[1], sck, sdi_initstate, sdi[16:]) + + +def test_transfer16_bulk(la: LogicAnalyzer, slave: SPISlave): + la.capture(4, block=False) + value = slave.transfer16_bulk([WRITE_DATA16, WRITE_DATA16]) + la.stop() + (sck, sdo, cs, sdi) = la.fetch_data() + sdi_initstate = la.get_initial_states()[SDI[0]] + + assert len(cs) == (CS_START + CS_STOP) + assert len(sck) == SCK_WRITE16 + SCK_WRITE16 + assert len(sdo) == 0 + assert verify_value(value[0], sck, sdi_initstate, sdi[:32]) + assert verify_value(value[1], sck, sdi_initstate, sdi[32:]) diff --git a/tests/test_uart.py b/tests/test_uart.py new file mode 100644 index 00000000..fd861e52 --- /dev/null +++ b/tests/test_uart.py @@ -0,0 +1,84 @@ +"""Tests for pslab.bus.uart. + +The PSLab's logic analyzer is used to verify the function of the UART bus. Before +running the tests, connect: + TxD2->LA1 | PGD2 -> LA1 (for v5) + RxD2->SQ1 | PGC2 -> SQ1 (for v5) +""" + +import pytest + +from pslab.bus.uart import UART +from pslab.instrument.logic_analyzer import LogicAnalyzer +from pslab.instrument.waveform_generator import PWMGenerator +from pslab.connection import SerialHandler + +WRITE_DATA = 0x55 +TXD2 = "LA1" +RXD2 = "SQ1" +PWM_FERQUENCY = UART._baudrate // 2 +MICROSECONDS = 1e-6 +RELTOL = 0.05 +# Number of expected logic level changes. +TXD_START = 1 +TXD_WRITE_DATA = 8 # if LSB is 1 +TXD_STOP = 1 # if data MSB is 0 + + +@pytest.fixture +def uart(handler: SerialHandler) -> UART: + return UART(device=handler) + + +@pytest.fixture +def la(handler: SerialHandler) -> LogicAnalyzer: + return LogicAnalyzer(handler) + + +@pytest.fixture +def pwm(handler: SerialHandler) -> None: + pwm = PWMGenerator(handler) + pwm.generate(RXD2, PWM_FERQUENCY, 0.5) + + +def test_configure(la: LogicAnalyzer, uart: UART): + baudrate = 1000000 + uart.configure(baudrate) + la.capture(1, block=False) + uart.write_byte(WRITE_DATA) + la.stop() + (txd2,) = la.fetch_data() + start_to_stop = 9 + period = (txd2[-1] - txd2[0]) / start_to_stop + + assert (period * MICROSECONDS) ** -1 == pytest.approx(baudrate, rel=RELTOL) + + +def test_write_byte(la: LogicAnalyzer, uart: UART): + la.capture(1, block=False) + uart.write_byte(WRITE_DATA) + la.stop() + (txd2,) = la.fetch_data() + + assert len(txd2) == (TXD_START + TXD_WRITE_DATA + TXD_STOP) + + +def test_write_int(la: LogicAnalyzer, uart: UART): + la.capture(1, block=False) + uart.write_int((WRITE_DATA << 8) | WRITE_DATA) + la.stop() + (txd2,) = la.fetch_data() + + assert len(txd2) == 2 * (TXD_START + TXD_WRITE_DATA + TXD_STOP) + + +def test_read_byte(pwm: PWMGenerator, uart: UART): + value = uart.read_byte() + + assert value in (0x55, 0xAA) + + +def test_read_int(pwm: PWMGenerator, uart: UART): + value = uart.read_int() + + assert value in (0x5555, 0x55AA, 0xAA55, 0xAAAA) diff --git a/tests/test_waveform_generator.py b/tests/test_waveform_generator.py new file mode 100644 index 00000000..3f12c05a --- /dev/null +++ b/tests/test_waveform_generator.py @@ -0,0 +1,257 @@ +"""Tests for pslab.waveform_generator. + +Before running the tests, connect: + SQ1 -> LA1 + SQ2 -> LA2 + SQ3 -> LA3 + SQ4 -> LA4 + SI1 -> CH1 + SI2 -> CH2 +""" + +import time + +import numpy as np +import pytest +from scipy.optimize import curve_fit +from _pytest.logging import LogCaptureFixture + +from pslab.instrument.logic_analyzer import LogicAnalyzer +from pslab.instrument.oscilloscope import Oscilloscope +from pslab.instrument.waveform_generator import PWMGenerator, WaveformGenerator +from pslab.connection import SerialHandler + + +MICROSECONDS = 1e-6 +RELTOL = 0.05 +GOOD_FIT = 0.99 + + +def r_squared(y: np.ndarray, y_fit: np.ndarray) -> float: + """Calculate the coefficient of determination.""" + ss_res = np.sum((y - y_fit) ** 2) # Residual sum of squares. + ss_tot = np.sum((y - y.mean()) ** 2) # Total sum of squares. + return 1 - (ss_res / ss_tot) + + +@pytest.fixture +def pwm(handler: SerialHandler) -> PWMGenerator: + return PWMGenerator(handler) + + +@pytest.fixture +def wave(handler: SerialHandler) -> WaveformGenerator: + return WaveformGenerator(handler) + + +@pytest.fixture +def la(handler: SerialHandler) -> LogicAnalyzer: + return LogicAnalyzer(handler) + + +@pytest.fixture +def scope(handler: SerialHandler) -> Oscilloscope: + return Oscilloscope(handler) + + +def test_sine_wave(wave: WaveformGenerator, scope: Oscilloscope): + frequency = 500 + wave.load_function("SI1", "sine") + wave.generate("SI1", frequency) + time.sleep(0.1) + x, y = scope.capture(1, 10000, 1, trigger=0) + + def expected_f(x, amplitude, frequency, phase): + return amplitude * np.sin(2 * np.pi * frequency * x + phase) + + amplitude = 3.3 + guess = [amplitude, frequency, 0] + [amplitude_est, frequency_est, phase_est], _ = curve_fit( + expected_f, x * MICROSECONDS, y, guess + ) + + assert amplitude_est == pytest.approx(amplitude, rel=RELTOL) + assert frequency_est == pytest.approx(frequency, rel=RELTOL) + + coeff_of_det = r_squared( + y, expected_f(x * MICROSECONDS, amplitude_est, frequency_est, phase_est) + ) + + assert coeff_of_det >= GOOD_FIT + + +def test_triangle_wave(wave: WaveformGenerator, scope: Oscilloscope): + frequency = 2000 + wave.load_function("SI1", "tria") + wave.generate("SI1", frequency) + time.sleep(0.1) + x, y = scope.capture(1, 10000, 1, trigger=0) + + def expected_f(x, amplitude, frequency, phase): + return ( + 2 * amplitude / np.pi * np.arcsin(np.sin(2 * np.pi * frequency * x + phase)) + ) + + amplitude = 3.3 + guess = [amplitude, frequency, 0] + [amplitude_est, frequency_est, phase_est], _ = curve_fit( + expected_f, x * MICROSECONDS, y, guess + ) + + assert amplitude_est == pytest.approx(amplitude, rel=RELTOL) + assert frequency_est == pytest.approx(frequency, rel=RELTOL) + + coeff_of_det = r_squared( + y, expected_f(x * MICROSECONDS, amplitude_est, frequency_est, phase_est) + ) + + assert coeff_of_det >= GOOD_FIT + + +def test_superposition(wave: WaveformGenerator, scope: Oscilloscope): + frequency = 1000 + amplitude1 = 2 + amplitude2 = 1 + + def super_sine(x): + return amplitude1 * np.sin(x) + amplitude2 * np.sin(5 * x) + + wave.load_function("SI1", super_sine, [0, 2 * np.pi]) + wave.generate("SI1", frequency) + time.sleep(0.1) + x, y = scope.capture(1, 10000, 1, trigger=0) + + def expected_f(x, amplitude1, amplitude2, frequency, phase): + return amplitude1 * np.sin( + 2 * np.pi * frequency * x + phase + ) + amplitude2 * np.sin(5 * (2 * np.pi * frequency * x + phase)) + + amplitude1 = 2 + amplitude2 = 1 + guess = [amplitude1, amplitude2, frequency, 0] + [amplitude1_est, amplitude2_est, frequency_est, phase_est], _ = curve_fit( + expected_f, x * MICROSECONDS, y, guess + ) + + assert amplitude1_est == pytest.approx(amplitude1, rel=RELTOL) + assert amplitude2_est == pytest.approx(amplitude2, rel=RELTOL) + assert frequency_est == pytest.approx(frequency, rel=RELTOL) + + coeff_of_det = r_squared( + y, + expected_f( + x * MICROSECONDS, amplitude1_est, amplitude2_est, frequency_est, phase_est + ), + ) + + assert coeff_of_det >= GOOD_FIT + + +def test_sine_phase(wave: WaveformGenerator, scope: Oscilloscope): + frequency = 500 + phase = 90 + wave.load_function("SI1", "sine") + wave.load_function("SI2", "sine") + wave.generate(["SI1", "SI2"], frequency, phase) + time.sleep(0.1) + x, y1, y2 = scope.capture(2, 5000, 2, trigger=0) + + def expected_f(x, amplitude, frequency, phase): + return amplitude * np.sin(2 * np.pi * frequency * x + phase) + + guess1 = [3.3, frequency, 0] + [_, _, phase1_est], _ = curve_fit(expected_f, x * MICROSECONDS, y1, guess1) + guess2 = [3.3, frequency, phase * np.pi / 180] + [_, _, phase2_est], _ = curve_fit(expected_f, x * MICROSECONDS, y2, guess2) + + assert phase2_est - phase1_est == pytest.approx(phase * np.pi / 180, rel=RELTOL) + + +def test_low_frequency_warning(caplog: LogCaptureFixture, wave: WaveformGenerator): + wave.generate("SI1", 1) + assert "AC coupling" in caplog.text + + +def test_low_frequency_error(wave: WaveformGenerator): + with pytest.raises(ValueError): + wave.generate("SI1", 0.05) + + +def test_high_frequency_warning(caplog: LogCaptureFixture, wave: WaveformGenerator): + wave.generate("SI1", 1e4) + assert "Frequencies above" + + +def test_dimension_mismatch(wave: WaveformGenerator): + with pytest.raises(ValueError): + wave.generate("SI2", [500, 1000]) + + +def test_pwm(pwm: PWMGenerator, la: LogicAnalyzer): + frequency = 5e4 + duty_cycle = 0.4 + pwm.generate("SQ1", frequency, duty_cycle) + time.sleep(0.1) + + assert la.measure_frequency("LA1") == pytest.approx(frequency, rel=RELTOL) + assert la.measure_duty_cycle("LA1")[1] == pytest.approx(duty_cycle, rel=RELTOL) + + +def test_pwm_phase(pwm: PWMGenerator, la: LogicAnalyzer): + frequency = 1e4 + duty_cycle = 0.5 + phase = 0.25 + pwm.generate(["SQ1", "SQ2"], frequency, duty_cycle, phase) + time.sleep(0.1) + interval = la.measure_interval(["LA1", "LA2"], ["rising", "rising"]) + + if interval < 0: + interval += frequency**-1 / MICROSECONDS + + assert interval * MICROSECONDS == pytest.approx(frequency**-1 * phase, rel=RELTOL) + + +def test_set_state(pwm: PWMGenerator, la: LogicAnalyzer): + states = [True, False, True, True] + pwm.set_state(*states) + time.sleep(0.1) + assert list(la.get_states().values()) == states + + +def test_unchanged_state(pwm: PWMGenerator, la: LogicAnalyzer): + frequency = 2.5e3 + duty_cycle = 0.9 + pwm.generate(["SQ1", "SQ4"], frequency, duty_cycle) + states = [None, True, False, None] + pwm.set_state(*states) + time.sleep(0.1) + + assert list(la.get_states().values())[1:3] == states[1:3] + assert la.measure_frequency("LA1") == pytest.approx(frequency, rel=RELTOL) + assert la.measure_frequency("LA4") == pytest.approx(frequency, rel=RELTOL) + assert la.measure_duty_cycle("LA1")[1] == pytest.approx(duty_cycle, rel=RELTOL) + assert la.measure_duty_cycle("LA4")[1] == pytest.approx(duty_cycle, rel=RELTOL) + + +def test_map_reference_clock(pwm: PWMGenerator, la: LogicAnalyzer): + prescaler = 5 + pwm.map_reference_clock(["SQ3"], prescaler) + assert la.measure_frequency("LA3") == pytest.approx( + 128e6 / (1 << prescaler), rel=RELTOL + ) + + +def test_pwm_high_frequency_error(pwm: PWMGenerator): + with pytest.raises(ValueError): + pwm.generate("SQ1", 2e7, 0.5) + + +def test_pwm_get_frequency(pwm: PWMGenerator): + frequency = 1500 + pwm.generate("SQ2", frequency, 0.1) + assert pwm.frequency == pytest.approx(frequency, rel=RELTOL) + + +def test_pwm_set_negative_frequency(pwm: PWMGenerator): + with pytest.raises(ValueError): + pwm.generate("SQ1", -1, 0.5) diff --git a/tox.ini b/tox.ini new file mode 100644 index 00000000..00be71d5 --- /dev/null +++ b/tox.ini @@ -0,0 +1,38 @@ +[tox] + +[gh-actions] + +[testenv:integration] +deps = + -rrequirements.txt + pytest + coverage +commands = coverage run --source pslab -m pytest + +[testenv:lint] +deps = -rlint-requirements.txt +setenv = + INCLUDE_PSL_FILES = pslab/bus/ pslab/connection pslab/instrument/ pslab/serial_handler.py pslab/cli.py pslab/external/motor.py pslab/external/gas_sensor.py pslab/external/hcsr04.py +commands = + black --check {env:INCLUDE_PSL_FILES} + flake8 --show-source {env:INCLUDE_PSL_FILES} + bandit -q -r {env:INCLUDE_PSL_FILES} + pydocstyle {env:INCLUDE_PSL_FILES} + +[testenv:docs] +deps = + sphinx>=1.8.4 + -rdoc-requirements.txt +commands = sphinx-build -d docs/_build/doctrees docs docs/_build/html + +[flake8] +max-line-length = 88 +max-complexity = 10 +select = B,C,E,F,W,T4,B9 +# These rules conflict with black. +ignore = E203,W503 + + +[pydocstyle] +convention = numpy +add-select = D212 pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy