diff --git a/.editorconfig b/.editorconfig index 639dc985493e6e062d13b44a1e84863cf9435d6c..654c8cad61540295d79b10c625583294aefe2704 100644 --- a/.editorconfig +++ b/.editorconfig @@ -19,6 +19,11 @@ charset = utf-8 indent_style = space indent_size = 4 +# 4 space indentation +[*.bas,*.inc] +indent_style = space +indent_size = 4 + [*.md] trim_trailing_whitespace = false diff --git a/Makefile b/Makefile index 0acdd27647ebe64c37c4dabbb4d4dd87b4a1fd2f..b41a5b6a942044ac01e4c5e8555e1fc57137277f 100644 --- a/Makefile +++ b/Makefile @@ -1,4 +1,4 @@ -PIPFLAGS = -r requirements.txt --extra-index-url https://las-nq-serv.physnet.uni-hamburg.de/pypiserver +PIPFLAGS = -r requirements.txt all: requirements test doc package diff --git a/compile_in_vm.sh b/compile_in_vm.sh index 5879cfcc7b8d7e2f9145f719d64f04a38d737df1..9411f69bfa79846b852fba54042f8159f1eb16ab 100755 --- a/compile_in_vm.sh +++ b/compile_in_vm.sh @@ -28,5 +28,5 @@ do inotifywait -e modify src/adbasic/* echo $vb VBoxManage guestcontrol Win7 --username Chris --password 'z' run --exe "$vbox_compiler" - rsync -u src/adbasic/nqontrol.TC1 src/nqontrol/ + # rsync -u src/adbasic/nqontrol.TC1 src/nqontrol/ done diff --git a/doc/Makefile b/doc/Makefile index de0c902961f918a8d10a70d8b329700dd7c5c414..8f3ff303ca77b1927d6b49d30a9abdbd263d7886 100644 --- a/doc/Makefile +++ b/doc/Makefile @@ -58,7 +58,7 @@ $(AUTODOCDIR): $(MODULEDIR) # $(AUTODOCBUILD) -f -o $@ $^ doc-requirements: $(AUTODOCDIR) - jupyter nbconvert documentation.ipynb --to rst + @cd jupyter; sh jupyter2markdown.sh; cd .. html: doc-requirements diff --git a/doc/gui.md b/doc/gui.md index 7d0257a2369932acb323c73c782ae7d55faf9510..1622c4efa0b4f9b695e3853dbc77466c18029d01 100644 --- a/doc/gui.md +++ b/doc/gui.md @@ -40,7 +40,7 @@ After starting the server you will see something like this, depending on how you  -The GUI will dynamically create as many components as you need. E.g. if you changed the `NUMBER_OF_SERVOS` variable in the `servo.py` module it will provide input sections accordingly. +The GUI will dynamically create as many components as you need. E.g. if you changed the `settings.NUMBER_OF_SERVOS` variable it will provide input sections accordingly. If you are running the GUI with multiple devices (which might be implemented in the future), the GUIs are just displayed one after the other. So each GUI comes with its own sections etc... diff --git a/doc/documentation.ipynb b/doc/jupyter/documentation.ipynb similarity index 100% rename from doc/documentation.ipynb rename to doc/jupyter/documentation.ipynb diff --git a/doc/documentation_data/SCRN0005.TXT b/doc/jupyter/documentation_data/SCRN0005.TXT similarity index 100% rename from doc/documentation_data/SCRN0005.TXT rename to doc/jupyter/documentation_data/SCRN0005.TXT diff --git a/doc/documentation_data/SCRN0006.TXT b/doc/jupyter/documentation_data/SCRN0006.TXT similarity index 100% rename from doc/documentation_data/SCRN0006.TXT rename to doc/jupyter/documentation_data/SCRN0006.TXT diff --git a/doc/documentation_data/SCRN0007.TXT b/doc/jupyter/documentation_data/SCRN0007.TXT similarity index 100% rename from doc/documentation_data/SCRN0007.TXT rename to doc/jupyter/documentation_data/SCRN0007.TXT diff --git a/doc/documentation_data/SCRN0008.TXT b/doc/jupyter/documentation_data/SCRN0008.TXT similarity index 100% rename from doc/documentation_data/SCRN0008.TXT rename to doc/jupyter/documentation_data/SCRN0008.TXT diff --git a/doc/documentation_data/SCRN0009.TXT b/doc/jupyter/documentation_data/SCRN0009.TXT similarity index 100% rename from doc/documentation_data/SCRN0009.TXT rename to doc/jupyter/documentation_data/SCRN0009.TXT diff --git a/doc/documentation_data/SCRN0010.TXT b/doc/jupyter/documentation_data/SCRN0010.TXT similarity index 100% rename from doc/documentation_data/SCRN0010.TXT rename to doc/jupyter/documentation_data/SCRN0010.TXT diff --git a/doc/documentation_data/SCRN0011.TXT b/doc/jupyter/documentation_data/SCRN0011.TXT similarity index 100% rename from doc/documentation_data/SCRN0011.TXT rename to doc/jupyter/documentation_data/SCRN0011.TXT diff --git a/doc/documentation_data/SCRN0012.TXT b/doc/jupyter/documentation_data/SCRN0012.TXT similarity index 100% rename from doc/documentation_data/SCRN0012.TXT rename to doc/jupyter/documentation_data/SCRN0012.TXT diff --git a/doc/documentation_data/SCRN0013.TXT b/doc/jupyter/documentation_data/SCRN0013.TXT similarity index 100% rename from doc/documentation_data/SCRN0013.TXT rename to doc/jupyter/documentation_data/SCRN0013.TXT diff --git a/doc/documentation_data/SCRN0014.TXT b/doc/jupyter/documentation_data/SCRN0014.TXT similarity index 100% rename from doc/documentation_data/SCRN0014.TXT rename to doc/jupyter/documentation_data/SCRN0014.TXT diff --git a/doc/documentation_data/SCRN0015.TXT b/doc/jupyter/documentation_data/SCRN0015.TXT similarity index 100% rename from doc/documentation_data/SCRN0015.TXT rename to doc/jupyter/documentation_data/SCRN0015.TXT diff --git a/doc/documentation_data/SCRN0016.TXT b/doc/jupyter/documentation_data/SCRN0016.TXT similarity index 100% rename from doc/documentation_data/SCRN0016.TXT rename to doc/jupyter/documentation_data/SCRN0016.TXT diff --git a/doc/jupyter/jupyter2markdown.sh b/doc/jupyter/jupyter2markdown.sh new file mode 100755 index 0000000000000000000000000000000000000000..cd6264553a788ae9ca2a979bad50a1d8143e61bf --- /dev/null +++ b/doc/jupyter/jupyter2markdown.sh @@ -0,0 +1,2 @@ +#!/usr/bin/env bash +jupyter nbconvert *.ipynb --to rst --output-dir=.. diff --git a/doc/jupyter/usage.ipynb b/doc/jupyter/usage.ipynb new file mode 100644 index 0000000000000000000000000000000000000000..e5b3dc55c124eb91fa4e694b1ecb8acaa0887503 --- /dev/null +++ b/doc/jupyter/usage.ipynb @@ -0,0 +1,272 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Basic Usage\n", + "\n", + "This page assumes a successful [installation](install.html) of NQontrol and all dependencies.\n", + "\n", + "## Hello Servo Example\n", + "\n", + "Here is a minimalistic, `hello world`-like example to show, how to control a servo using the python terminal or a little script." + ] + }, + { + "cell_type": "code", + "execution_count": 12, + "metadata": {}, + "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + "WARNING:root:Running with mock device!\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Uptime 0s\n" + ] + } + ], + "source": [ + "# Importing a ServoDevice is enough\n", + "from nqontrol import ServoDevice\n", + "\n", + "# Create a new servo device object, connecting to adwin with the device number 1.\n", + "sd = ServoDevice(0)\n", + "\n", + "# Print the timestamp\n", + "print('Uptime {}s'.format(sd.timestamp))\n", + "\n", + "# Get a servo object to control it.\n", + "s = sd.servo(1)\n", + "\n", + "# enable in and output\n", + "s.inputSw = True\n", + "s.outputSw = True" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Using a signal generator for the input you will now get the same signal on the output.\n", + "(That is true for signals below about 15 kHz.)\n", + "\n", + "## Apply a ServoDesign\n", + "\n", + "To use a servo for a real control loop we want to have some filters.\n", + "The full documentation is in the [OpenQlab docs](https://las-nq-serv.physnet.uni-hamburg.de/python/openqlab/servodesign.html)." + ] + }, + { + "cell_type": "code", + "execution_count": 13, + "metadata": {}, + "outputs": [ + { + "data": { + "image/png": "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\n", + "text/plain": [ + "<Figure size 576x576 with 2 Axes>" + ] + }, + "metadata": { + "needs_background": "light" + }, + "output_type": "display_data" + } + ], + "source": [ + "# Add an integrator and a lowpass\n", + "s.servoDesign.integrator(1e2)\n", + "s.servoDesign.lowpass(5e3)\n", + "\n", + "# Plot how it looks analytically\n", + "import matplotlib.pyplot as plt\n", + "design.plot()\n", + "plt.show()" + ] + }, + { + "cell_type": "code", + "execution_count": 15, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "[[1.0015692243574656, -0.9999968584122811, 0.0, -0.9968633318334381, 0.0],\n", + " [0.005519854739225482, -1.7786050226116845, 0.8007755579457131, 2.0, 1.0],\n", + " [1.0, 0, 0, 0, 0],\n", + " [1.0, 0, 0, 0, 0],\n", + " [1.0, 0, 0, 0, 0]]" + ] + }, + "execution_count": 15, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "# Apply it to our servo\n", + "s.applyServoDesign()\n", + "\n", + "# Control, what happens with the servo\n", + "s.filters" + ] + }, + { + "cell_type": "code", + "execution_count": 16, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "[True, True, False, False, False]\n", + "1.0\n" + ] + } + ], + "source": [ + "print(s.filterStates)\n", + "print(s.gain)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Control Filters" + ] + }, + { + "cell_type": "code", + "execution_count": 18, + "metadata": {}, + "outputs": [], + "source": [ + "# Disable all filters\n", + "s.filterStates = [False] * 5\n", + "\n", + "# Enable the second (index = 1) filter\n", + "s.filterState(1, True)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Enable a Ramp" + ] + }, + { + "cell_type": "code", + "execution_count": 22, + "metadata": {}, + "outputs": [], + "source": [ + "# Choose a slow ramp with a frequency of 1 Hz.\n", + "# Amplitude = 4\n", + "s.enableRamp(20, 4)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Start Realtime Plotting" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Start plotting in a background process\n", + "s.realtimePlot(multiprocessing=True)\n", + "\n", + "# disable plotting the output\n", + "s.realtime['ydata'] = ['input', 'aux']\n", + "\n", + "# set constant y limit from -3 to 5 \n", + "s.realtime['ylim'] = (-3, 5)\n", + "\n", + "# stop realtime plotting\n", + "s.stopRealtimePlot()" + ] + } + ], + "metadata": { + "hide_input": false, + "kernelspec": { + "display_name": "Python 3", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.7.2" + }, + "toc": { + "base_numbering": 1, + "nav_menu": {}, + "number_sections": true, + "sideBar": true, + "skip_h1_title": false, + "title_cell": "Table of Contents", + "title_sidebar": "Contents", + "toc_cell": false, + "toc_position": {}, + "toc_section_display": true, + "toc_window_display": true + }, + "varInspector": { + "cols": { + "lenName": 16, + "lenType": 16, + "lenVar": 40 + }, + "kernels_config": { + "python": { + "delete_cmd_postfix": "", + "delete_cmd_prefix": "del ", + "library": "var_list.py", + "varRefreshCmd": "print(var_dic_list())" + }, + "r": { + "delete_cmd_postfix": ") ", + "delete_cmd_prefix": "rm(", + "library": "var_list.r", + "varRefreshCmd": "cat(var_dic_list()) " + } + }, + "types_to_exclude": [ + "module", + "function", + "builtin_function_or_method", + "instance", + "_Feature" + ], + "window_display": false + } + }, + "nbformat": 4, + "nbformat_minor": 2 +} diff --git a/doc/usage.md b/doc/usage.md deleted file mode 100644 index b4421d23d2ee08dabf13ad1e638510520565357f..0000000000000000000000000000000000000000 --- a/doc/usage.md +++ /dev/null @@ -1,113 +0,0 @@ -# Basic Usage - -This page assumes a successful [installation](install.html) of NQontrol and all dependencies. - -## Hello Servo Example - -Here is a minimalistic, `hello world`-like example to show, how to control a servo using the python terminal or a little script. - -```python -# Importing a ServoDevice is enough -from nqontrol import ServoDevice - -# Create a new servo device object, connecting to adwin with the device number 1. -sd = ServoDevice(1) - -# Print the timestamp -print(sd.timeStamp) - -# Create the first servo on channel 1 from 8. -sd.addServo(1) - -# Get the new servo object to control it. -s = sd.servo(1) - -# enable in and output -s.inputSw = True -s.outputSw = True -``` - -Using a signal generator for the input you will now get the same signal on the output. -(That is true for signals below about 15 kHz.) - -## Apply a ServoDesign - -To use a servo for a real control loop we want to have some filters. -The full documentation is in the [OpenQlab docs](https://las-nq-serv.physnet.uni-hamburg.de/python/openqlab/servodesign.html). - -Input: -```python -from OpenQlab.analysis import ServoDesign - -# Create a ServoDesign object -design = ServoDesign() - -# Add an integrator and a lowpass -design.integrator(1e2) -design.lowpass(5e3) - -# Plot how it looks analytically -import matplotlib.pyplot as plt -design.plot() -plt.show() -``` -Output: - - -Input: -```python -# Apply it to our servo -s.applyServoDesign(design) - -# Control, what happens with the servo -print(s.filters) -``` -Output: -```bash -[[1.00313, -0.999993, 0.0, -0.99373, 0.0], - [0.01975, -1.56097, 0.64130, 2.0, 1.0], - [1.0, 0, 0, 0, 0], - [1.0, 0, 0, 0, 0], - [1.0, 0, 0, 0, 0]] -``` -Input: -```python -print(s.filterStates) -print(s.gain) -``` -Output: -```bash -[True, True, False, False, False] -1.0 -``` - -## Control Filters -```python -# Disable all filters -s.filterStates = [False] * 5 - -# Enable the second (index = 1) filter -s.filterState(1, True) -``` - -## Enable a Ramp -```python -# Choose a slow ramp with a frequency of 1 Hz. -# Amplitude = 4 -s.setRamp(1, 4) -``` - -## Start Realtime Plotting -```python -# Start plotting in a background process -s.realtimePlot() - -# disable plotting the output -s.realtime['ydata'] = ['input', 'aux'] - -# set constant y limit from -3 to 5 -s.realtime['ylim'] = (-3, 5) - -# stop realtime plotting -s.stopRealtimePlot() -``` diff --git a/requirements.txt b/requirements.txt index 6fa80bea2f7142d7f107133d8e6421db3c97ad4a..dbde50749859b928d0607bbe6d08b6045a779d93 100644 --- a/requirements.txt +++ b/requirements.txt @@ -4,10 +4,8 @@ adwin>=0.16.1 sphinx>=1.7.5 numpy>=1.14 -dash==0.36.0 # The core dash backend -dash-renderer==0.17.0 # The dash front-end -dash-html-components==0.13.5 # HTML components -dash-core-components==0.43.0 # Supercharged components +dash==0.38.0 # The core dash backend +dash-daq>=0.1.4 plotly pandas pandas-datareader>=0.5.0 @@ -24,7 +22,7 @@ wheel setuptools # setuptools_scm # python-dotenv -OpenQlab>=0.1 +OpenQlab>=0.1.4 jupyter_contrib_nbextensions pytest<4.0 pytest-cov diff --git a/setup.cfg b/setup.cfg index 3548b6c47dc7d7646fcf42a26ea9720dccf00ab5..15a592a064e52254ea260a9459f2a325841fbf1d 100644 --- a/setup.cfg +++ b/setup.cfg @@ -30,10 +30,8 @@ install_requires = adwin>=0.16.1 sphinx>=1.7.5 numpy>=1.14 - dash>=0.31.1 # The core dash backend - dash-renderer>=0.15.1 # The dash front-end - dash-html-components>=0.13.0 # HTML components - dash-core-components>=0.40.0 # Supercharged components + dash==0.38.0 # The core dash backend + dash-daq>=0.1.4 plotly pandas pandas-datareader>=0.5.0 @@ -48,7 +46,7 @@ install_requires = sphinx_rtd_theme wheel setuptools - OpenQlab>=0.1 + OpenQlab>=0.1.4 jupyter_contrib_nbextensions pytest<4.0 pytest-cov diff --git a/src/adbasic/.gitignore b/src/adbasic/.gitignore index 01134cbd9a66ce612664259c7b58ae1c1a0a2911..f469dc4c715de50bc79e6e95f99caabd64f08251 100644 --- a/src/adbasic/.gitignore +++ b/src/adbasic/.gitignore @@ -1,2 +1 @@ -*.TC1 *.ERR diff --git a/src/adbasic/filter_module.inc b/src/adbasic/filter_module.inc index 77f43c33c214ca5dc9b969fb3bd96d115b9cb29d..cce847d70bf77ccf2919e47ca4c22fb146c786dc 100644 --- a/src/adbasic/filter_module.inc +++ b/src/adbasic/filter_module.inc @@ -51,7 +51,7 @@ #Define FCR_SW_INPUT 1 #Define FCR_SW_OUTPUT 2 #Define FCR_SW_OFFSET 4 -#Define FCR_SW_SNAP 8 +' #Define FCR_SW_SNAP 8 #Define FCR_SW_SOS1 16 #Define FCR_SW_SOS2 32 #Define FCR_SW_SOS3 64 @@ -60,10 +60,10 @@ #Define FCR_SW_AUX 512 ' Snap control -#Define snap_config Data_7 -Dim snap_config[NUM_FILTERS] as Long -#Define snap_value_mask 0ffffh -#Define snap_lg 10000h ' Look for signal lower (0) or greater (1) than the threshold +' #Define snap_config Data_7 +' Dim snap_config[NUM_FILTERS] as Long +' #Define snap_value_mask 0ffffh +' #Define snap_lg 10000h ' Look for signal lower (0) or greater (1) than the threshold ' Global storage of filter coefficient for transfer from PC #Define filter_coeffs_PC Data_1 @@ -105,7 +105,7 @@ Function FilterModule(input_int, control, filter_index, aux) as Float Dim sos_output as Float Dim sos_index as Long Dim input as Float - Dim snap_value as Long + ' Dim snap_value as Long input = input_int ' offset switch @@ -136,23 +136,23 @@ Function FilterModule(input_int, control, filter_index, aux) as Float filter_output = input EndIf - ' Snap state that waits for aux signal - If ((control And FCR_SW_SNAP) > 0) Then - filter_output = 0.0 - snap_value = aux - (snap_config[filter_index+1] And snap_value_mask) - - If ((snap_config[filter_index+1] And snap_lg) > 0) Then - snap_value = -1 * snap_value - EndIf - If (snap_value <= 0) Then - ' Disable snapping - control = (control And Not(FCR_SW_SNAP)) - ' Enable output - control = (control Or FCR_SW_OUTPUT) - ' Stop ramp - rcr = rcr And Not(0fh) - EndIf - EndIf + ' ' Snap state that waits for aux signal + ' If ((control And FCR_SW_SNAP) > 0) Then + ' filter_output = 0.0 + ' snap_value = aux - (snap_config[filter_index+1] And snap_value_mask) + ' + ' If ((snap_config[filter_index+1] And snap_lg) > 0) Then + ' snap_value = -1 * snap_value + ' EndIf + ' If (snap_value <= 0) Then + ' ' Disable snapping + ' control = (control And Not(FCR_SW_SNAP)) + ' ' Enable output + ' control = (control Or FCR_SW_INPUT) + ' ' Stop ramp + ' rcr = rcr And Not(0fh) + ' EndIf + ' EndIf if ((control And FCR_SW_AUX) > 0) Then filter_output = filter_output + aux - 8000h @@ -191,13 +191,13 @@ Sub RunFilters(input[], output[]) 'For filter_index = 0 To (NUM_FILTERS-1) ' output[filter_index+1] = FilterModule(input[2*filter_index+1], Par_3, filter_index) 'Next filter_index - output[1] = FilterModule(input[1], fcr_1, 0,input[ 9])+8000h - output[2] = FilterModule(input[2], fcr_2, 1,input[10])+8000h - output[3] = FilterModule(input[3], fcr_3, 2,input[11])+8000h - output[4] = FilterModule(input[4], fcr_4, 3,input[12])+8000h - output[5] = FilterModule(input[5], fcr_5, 4,input[13])+8000h - output[6] = FilterModule(input[6], fcr_6, 5,input[14])+8000h - output[7] = FilterModule(input[7], fcr_7, 6,input[15])+8000h - output[8] = FilterModule(input[8], fcr_8, 7,input[16])+8000h + output[1] = FilterModule(input[1], fcr_1, 0, input[ 9]) + 8000h + output[2] = FilterModule(input[2], fcr_2, 1, input[10]) + 8000h + output[3] = FilterModule(input[3], fcr_3, 2, input[11]) + 8000h + output[4] = FilterModule(input[4], fcr_4, 3, input[12]) + 8000h + output[5] = FilterModule(input[5], fcr_5, 4, input[13]) + 8000h + output[6] = FilterModule(input[6], fcr_6, 5, input[14]) + 8000h + output[7] = FilterModule(input[7], fcr_7, 6, input[15]) + 8000h + output[8] = FilterModule(input[8], fcr_8, 7, input[16]) + 8000h EndSub diff --git a/src/adbasic/nqontrol.TC1 b/src/adbasic/nqontrol.TC1 new file mode 100644 index 0000000000000000000000000000000000000000..5286d8d2449444a68a6d39e3304689bc1285fd92 Binary files /dev/null and b/src/adbasic/nqontrol.TC1 differ diff --git a/src/adbasic/nqontrol.bas b/src/adbasic/nqontrol.bas index bb2f8567e954cd8f866c2a632dbd1a945645ec0e..002a3ab73b7facaa5e8692d732855a160f292a7a 100644 --- a/src/adbasic/nqontrol.bas +++ b/src/adbasic/nqontrol.bas @@ -1,268 +1,393 @@ -'<ADbasic Header, Headerversion 001.001> -' Process_Number = 1 -' Initial_Processdelay = 1000 -' Eventsource = Timer -' Control_long_Delays_for_Stop = No -' Priority = High -' Version = 1 -' ADbasic_Version = 6.2.0 -' Optimize = Yes -' Optimize_Level = 3 -' Stacksize = 1000 -' Info_Last_Save = X220 X220\adwin -'<Header End> -#Include ADwinPro_All.inc -#Include .\filter_module.inc -Import Math.lic - -#Define AIN_MODULE_1 1 -#Define AIN_MODULE_2 2 -#Define AOUT_MODULE_1 3 -#Define AOUT_MODULE_2 4 -#Define NUM_INPUT_CHANNELS 8 -#Define NUM_OUTPUT_CHANNELS 8 -#Define NUM_ADC_CHANNELS 16 -#Define NUM_DAC_CHANNELS 16 -#Define CPU_CLK 1000000000 -#Define PROC_CLK 200000 -#Define FIFO_BUFFER_SIZE 10003 -#Define NUM_RAMP_VALUES 131072 -#Define TTL_HIGH 0C000h ' +5V -#Define TTL_LOW 8000h ' 0V - -#Define timer PAR_1 -' monitor control Parameter, values from 1 to 8 -#Define monitor_sel Data_6 -Dim monitor_sel[NUM_FILTERS] as Long -' TTL -Dim TTL_current as Long -#Define TTL_DIG_CHANNEL 0 -' Ramp Control Register -#Define rcr Par_3 -' Ramp amplitude. 1 means ±10V -#Define ramp_amplitude FPar_1 -'ramp Data array -Dim ramp_data[NUM_RAMP_VALUES], ramp_idx, ramp_chan as Long - -Dim timer_last, timer_now, t1, t2, new_time As Long - -' The inputs straight from the ADC -Dim adc_input[NUM_ADC_CHANNELS] As Long -' The outputs straight into the DAC -Dim dac_output[NUM_DAC_CHANNELS] As Long - -#Define input_sensitivity Par_8 -Dim input_sensitivity_last as Long - -#Define fifo_data Data_3 -#Define fifo_sel Par_4 -#Define fifo_counter_max Par_6 -Dim fifo_data[FIFO_BUFFER_SIZE] as Float as FIFO -Dim fifo_counter = 1 as Long at Dm_Local - -#Define last_output Data_4 -Dim last_output[NUM_FILTERS] as Long - -Dim i as long -Dim ADCF_Mode as Long - - -Sub Blinkenlights() - Dim even_odd as Long - timer_now = Read_Timer() - If ((timer_now - timer_last) > CPU_CLK) Then - Inc(timer) - even_odd = Mod(timer, 2) - P2_Set_Led(AIN_MODULE_1, even_odd) - P2_Set_Led(AOUT_MODULE_1, even_odd) - P2_Set_Led(AIN_MODULE_2, 1-even_odd) - P2_Set_Led(AOUT_MODULE_2, 1-even_odd) - timer_last = timer_now - EndIf -EndSub - -Sub SetInputSensitivity() - Dim inputMode as long - Dim auxMode as long - - if (input_sensitivity_last <> input_sensitivity) then - For i=1 to NUM_INPUT_CHANNELS - inputMode = Shift_Right(input_sensitivity And Shift_Left(3, 2*i-2), 2*i-2) - auxMode = Shift_Right(input_sensitivity And Shift_Left(3, 2*i+14), 2*i+14) - - P2_Set_Gain(AIN_MODULE_1, i, inputMode) - P2_Set_Gain(AIN_MODULE_2, i, auxMode) - next i - input_sensitivity_last = input_sensitivity - endif -EndSub - -Sub SendToMonitors() - Dim mon as Long - for mon=1 to NUM_INPUT_CHANNELS - If (monitor_sel[mon] > 0) Then - If (monitor_sel[mon] > 20) Then - If (monitor_sel[mon] < 29) Then - ' Output signal - dac_output[mon+NUM_OUTPUT_CHANNELS] = dac_output[monitor_sel[mon]-20] +'<ADbasic Header, Headerversion 001.001> +' Process_Number = 1 +' Initial_Processdelay = 1000 +' Eventsource = Timer +' Control_long_Delays_for_Stop = No +' Priority = High +' Version = 1 +' ADbasic_Version = 6.2.0 +' Optimize = Yes +' Optimize_Level = 3 +' Stacksize = 1000 +' Info_Last_Save = VBOX vBox\Chris +' Foldings = 65,79,95,115 +'<Header End> +#Include ADwinPro_All.inc +#Include .\filter_module.inc +Import Math.lic + +#Define AIN_MODULE_1 1 +#Define AIN_MODULE_2 2 +#Define AOUT_MODULE_1 3 +#Define AOUT_MODULE_2 4 +#Define NUM_INPUT_CHANNELS 8 +#Define NUM_OUTPUT_CHANNELS 8 +#Define NUM_ADC_CHANNELS 16 +#Define NUM_DAC_CHANNELS 16 +#Define CPU_CLK 1000000000 +#Define PROC_CLK 200000 +#Define FIFO_BUFFER_SIZE 10003 +#Define NUM_RAMP_VALUES 131072 +#Define TTL_HIGH 0C000h ' +5V +#Define TTL_LOW 8000h ' 0V + +#Define timer PAR_1 +' monitor control Parameter, values from 1 to 8 +#Define monitor_sel Data_6 +Dim monitor_sel[NUM_FILTERS] as Long +' TTL +Dim TTL_current as Long +#Define TTL_DIG_CHANNEL 0 +' Ramp Control Register +#Define rcr Par_3 +' Ramp amplitude. 1 means ±10V +#Define ramp_amplitude FPar_1 +'ramp Data array +Dim ramp_data[NUM_RAMP_VALUES], ramp_idx, ramp_chan as Long + +Dim timer_last, timer_now, t1, t2, new_time As Long + +' The inputs straight from the ADC +Dim adc_input[NUM_ADC_CHANNELS] As Long +' The outputs straight into the DAC +Dim dac_output[NUM_DAC_CHANNELS] As Long + +#Define input_sensitivity Par_8 +Dim input_sensitivity_last as Long + +#Define fifo_data Data_3 +#Define fifo_sel Par_4 +#Define fifo_counter_max Par_6 +Dim fifo_data[FIFO_BUFFER_SIZE] as Float as FIFO +Dim fifo_counter = 1 as Long at Dm_Local + +#Define last_output Data_4 +Dim last_output[NUM_FILTERS] as Long + +' autolock stuff +#Define LOCK Data_8 +#Define NUM_INDICES 40 ' change this later on +Dim LOCK[NUM_INDICES] as Long +Dim lockIter[NUM_FILTERS] as Long +#Define threshold_value_mask 0ffffh +#Define threshold_direction 10000h ' Look for signal lower (0) or greater (1) than the threshold +Dim LOCK_COUNTER[NUM_FILTERS] as Long +#Define lock_mask 3h +#Define relock_mask 4h + +Dim i as long +Dim ADCF_Mode as Long + + +Sub Blinkenlights() + Dim even_odd as Long + timer_now = Read_Timer() + If ((timer_now - timer_last) > CPU_CLK) Then + Inc(timer) + even_odd = Mod(timer, 2) + P2_Set_Led(AIN_MODULE_1, even_odd) + P2_Set_Led(AOUT_MODULE_1, even_odd) + P2_Set_Led(AIN_MODULE_2, 1-even_odd) + P2_Set_Led(AOUT_MODULE_2, 1-even_odd) + timer_last = timer_now + EndIf +EndSub + +Sub SetInputSensitivity() + Dim inputMode as long + Dim auxMode as long + + if (input_sensitivity_last <> input_sensitivity) then + For i=1 to NUM_INPUT_CHANNELS + inputMode = Shift_Right(input_sensitivity And Shift_Left(3, 2*i-2), 2*i-2) + auxMode = Shift_Right(input_sensitivity And Shift_Left(3, 2*i+14), 2*i+14) + + P2_Set_Gain(AIN_MODULE_1, i, inputMode) + P2_Set_Gain(AIN_MODULE_2, i, auxMode) + next i + input_sensitivity_last = input_sensitivity + endif +EndSub + +Sub SendToMonitors() + Dim mon as Long + for mon=1 to NUM_INPUT_CHANNELS + If (monitor_sel[mon] > 0) Then + If (monitor_sel[mon] > 20) Then + If (monitor_sel[mon] < 29) Then + ' Output signal + dac_output[mon+NUM_OUTPUT_CHANNELS] = dac_output[monitor_sel[mon]-20] + Else + ' TTL signal + dac_output[mon+NUM_OUTPUT_CHANNELS] = TTL_current + EndIf + Else + ' Input and aux signal + dac_output[mon+NUM_OUTPUT_CHANNELS] = adc_input[monitor_sel[mon]] + EndIf + EndIf + next mon +EndSub + +Sub SendToFifo() + Dim combined as Float + If (fifo_sel > 0 ) Then + If (fifo_counter >= fifo_counter_max) Then ' There is a `&&` or `AndAlso` operator missing... + ' send values for current channel to PC + ' Note: we do this before filters, as filters do calculations on adc_input + ' We want to ensure a fixed phase between the channels. + ' Therefore we combine all in one fifo array field. + + ' Saving 3 16bit channels in a 64bit long variable + ' Byte | 7 6 | 5 4 | 3 2 | 1 0 | + ' Channel | | input | aux | output | + + combined = (adc_input[fifo_sel]) * 4294967296 ' This number is 2^32 and is a workaround for Shift_Left, which is not working for results > 32bit + combined = combined + Shift_Left(adc_input[fifo_sel+8], 16) + combined = combined + dac_output[fifo_sel] + + fifo_data = combined + + fifo_counter = 0 + EndIf + fifo_counter = fifo_counter + 1 + EndIf +EndSub + +Sub RunRamp() + ramp_chan = rcr And 0fh + If (ramp_chan > 0) Then + dac_output[ramp_chan] = (ramp_data[ramp_idx] * ramp_amplitude) + 8000h + ramp_idx = ramp_idx + Shift_Right(rcr And 0ff00h, 8) + If (ramp_idx > NUM_RAMP_VALUES) Then + ramp_idx = 1 + CPU_Digout(TTL_DIG_CHANNEL, 1) ' TTL high + TTL_current = TTL_HIGH + Else + CPU_Digout(TTL_DIG_CHANNEL, 0) ' TTL low + TTL_current = TTL_LOW + EndIf + EndIf +EndSub + +Sub AutoLock() + Dim channel as Long ' servo looping variable + Dim indexoffset as Long ' variable for array index offset + Dim state as Long + Dim relock as Long + Dim threshold as Long + Dim greater as Long + Dim offset as Long + Dim searchStart as Long + Dim searchEnd as Long + Dim aux as Long + + for channel=1 to NUM_FILTERS + indexoffset = (channel - 1) * 5 ' each servo occupies 5 channels currently + state = (LOCK[1 + indexoffset] And lock_mask) + relock = (LOCK[1 + indexoffset] And relock_mask) + threshold = (LOCK[2 + indexoffset] And threshold_value_mask) + greater = (LOCK[2 + indexoffset] And threshold_direction) + offset = LOCK[5 + indexoffset] ' this is the voltage that is added onto the output after a peak was found and fitlers have been activated + searchStart = LOCK[3 + indexoffset] + searchEnd = LOCK[4 + indexoffset] + aux = adc_input[channel + NUM_FILTERS] + + ' LOCK-OFF STATE + If (state = 0) Then + lockIter[channel] = 0 + LOCK[5 + indexoffset] = 0 ' locking offset to 0 in case lock is off + EndIf + + ' LOCK STATE + If (state = 2) Then + ' add offset (lastFound LOCK[5 + offset]) to dac_output after filters have been applied + dac_output[channel] = dac_output[channel] + offset + If (LOCK_COUNTER[channel] > 0) Then + LOCK_COUNTER[channel] = LOCK_COUNTER[channel] - 1 + Else + ' Different cases depending on whether peaks are expected as maxima or minima + ' For a maximum, we want to check whether the aux falls below the threshold before triggering a reload, for a minimum, we want to check whether it is above the threshold + If ((greater > 0) And (aux < threshold * 0.5)) Then + ' If (relock > 0) Then + ' LOCK[1 + indexoffset] = 1 + relock ' trigger relock in case aux is below threshold and direction is set to 'greater' + ' Else + LOCK[1 + indexoffset] = 0 + ' EndIf Else - ' TTL signal - dac_output[mon+NUM_OUTPUT_CHANNELS] = TTL_current + If ((greater = 0) And(aux > threshold * 0.5)) Then + ' If (relock > 0) Then + ' LOCK[1 + indexoffset] = 1 + relock ' trigger relock in case aux is above threshold and direction is set to 'lesser' + ' Else + LOCK[1 + indexoffset] = 0 + ' EndIf + EndIf EndIf - Else - ' Input and aux signal - dac_output[mon+NUM_OUTPUT_CHANNELS] = adc_input[monitor_sel[mon]] + EndIf + + EndIf + + ' LOCK SEARCH STATE + If (state = 1) Then + ' the 0 is basically a reset indicator, or rather a "i havent started yet indicator" + If (lockIter[channel] = 0) Then + lockIter[channel] = searchStart ' this is the search minimum + Dim x as Long + ' relevant index is sos_index + filter_index * num_SOS + ' sos index von 0 bis 4 (5-1) + ' filter_index von 0 bis 7 + ' filter_history[idx*NUM_HISTORY+1] bzw +2 + ' also zb channel 1, bei NUM_SOS=5 hat dann myindices: [0-4] + 0 * 5, also 0-4 + ' und NUMHISTORY = 2, also hisotry[0-4 * 2 +1/2], also 0 bis 10 + ' what about filter_coeffs? + for x = ((channel-1) * 10) to (10 * channel) + filter_history[x] = 0.0 + next x EndIf - EndIf - next mon -EndSub - -Sub SendToFifo() - Dim combined as Float - If (fifo_sel > 0 ) Then - If (fifo_counter >= fifo_counter_max) Then ' There is a `&&` or `AndAlso` operator missing... - ' send values for current channel to PC - ' Note: we do this before filters, as filters do calculations on adc_input - ' We want to ensure a fixed phase between the channels. - ' Therefore we combine all in one fifo array field. - - ' Saving 3 16bit channels in a 64bit long variable - ' Byte | 7 6 | 5 4 | 3 2 | 1 0 | - ' Channel | | input | aux | output | - - combined = (adc_input[fifo_sel]) * 4294967296 ' This number is 2^32 and is a workaround for Shift_Left, which is not working for results > 32bit - combined = combined + Shift_Left(adc_input[fifo_sel+8], 16) - combined = combined + dac_output[fifo_sel] - - fifo_data = combined - - fifo_counter = 0 - EndIf - fifo_counter = fifo_counter + 1 - EndIf -EndSub - -Sub RunRamp() - ramp_chan = rcr And 0fh - If (ramp_chan > 0) Then - dac_output[ramp_chan] = (ramp_data[ramp_idx] * ramp_amplitude) + 8000h - ramp_idx = ramp_idx + Shift_Right(rcr And 0ff00h, 8) - If (ramp_idx > NUM_RAMP_VALUES) Then - ramp_idx = 1 - CPU_Digout(TTL_DIG_CHANNEL, 1) ' TTL high - TTL_current = TTL_HIGH - Else - CPU_Digout(TTL_DIG_CHANNEL, 0) ' TTL low - TTL_current = TTL_LOW - EndIf - EndIf -EndSub - -Sub LimitToOutputRange() - Dim iij as Long - 'setting values which are above 65535 or below 0 to the maximum(65535) respectively minimum value(0) of the P2_Write_DAC8 command, - 'since it only takes the last 16 bits of integers out of its boundaries - for iij=1 to NUM_DAC_CHANNELS - if (dac_output[iij]>65535) then - dac_output[iij]=65535 - endif - if (dac_output[iij]<0) then - dac_output[iij]=0 - endif - next iij -EndSub - -Sub UpdateLastOutput() - for i=1 to NUM_FILTERS - last_output[i] = dac_output[i] - next i - ' Alternative: - ' P2_Write_DAC8(last_output, dac_output, 1) -EndSub - - -Init: - Processdelay = CPU_CLK/PROC_CLK ' 200 KHz - 'setting the values of the filter control register to zero - Par_11=0 - Par_12=0 - Par_13=0 - Par_14=0 - Par_15=0 - Par_16=0 - Par_17=0 - Par_18=0 - 'prepoluate dac_output - - input_sensitivity = 0 - input_sensitivity_last = 1 - timer = 0 - timer_last = Read_Timer() - ramp_chan = 0 - - Fifo_Clear(3) - - CPU_Dig_IO_Config(110011b) - - ' TODO nicer? - for i=1 to 8 - monitor_sel[i] = 0 - next i - - 'populates ramp_data with the values from -10V to 10V - For ramp_idx = 0 To 0ffffh - ramp_data[ramp_idx+1] = ramp_idx - 8000h - ramp_data[20000h-ramp_idx] = ramp_data[ramp_idx+1] - Next ramp_idx - ramp_idx = 1 - - P2_Set_Average_Filter(AIN_MODULE_1, 0) ' use 5 to average 32 measurements - - ' Pseudo code: - ' ADCF_Mode = 2^(AIN_MODULE_1-1) + 2^(AIN_MODULE_2-1) - ADCF_Mode = Shift_Left(1, AIN_MODULE_1-1) + Shift_Left(1, AIN_MODULE_2-1) - - P2_ADCF_Mode(ADCF_Mode, 1) ' Enable Timer mode on Module 2 & 3 (bit flag for module address!) - - -Event: - t1 = Read_Timer() - - ' Start DAC conversion on all DACs simultaneously - P2_Sync_All(Shift_Left(1, AOUT_MODULE_1-1) Or Shift_Left(1, AOUT_MODULE_2-1)) - - ' Read in values from ADC modules, these have already been converted because of the - ' ADCF timer mode - P2_Read_ADCF8(AIN_MODULE_1, adc_input, 1) - P2_Read_ADCF8(AIN_MODULE_2, adc_input, 9) - + + ' Different cases depending on whether peaks are expected as maxima or minima + ' Basically the peak direction: If the peak is a minimum, we want to check whether the aux signal falls below the threshold, in case of a maximum, we want the aux to surpass the threshold + + If (((greater > 0) And (aux > threshold)) Or ((greater = 0) And (aux < threshold))) Then ' LOCK IS FOUND + LOCK[1 + indexoffset] = 2 ' set the new lock state + LOCK[5 + indexoffset] = lockIter[channel] ' save the current voltage value as output offset + LOCK_COUNTER[channel] = 100 + ' activate filters/activate input + ' fcr_1-8 corresponts to PAR_11-18 + par[10 + channel] = (par[10 + channel] Or FCR_SW_INPUT) + par[10 + channel] = (par[10 + channel] Or FCR_SW_OUTPUT) + ' reset the iterator if threshold was breached, so that on next iteration/or when restarting it will be set to "searchStart" again + lockIter[channel] = 0 + Else ' SEARCHING FOR PEAK + ' Turn off input and output channels when starting new search + If (lockIter[channel] = searchStart) Then + par[10 + channel] = (par[10 + channel] And Not(FCR_SW_INPUT)) + par[10 + channel] = (par[10 + channel] And Not(FCR_SW_OutPUT)) + EndIf + + If (lockIter[channel] >= searchEnd) Then ' max range is reached + lockIter[channel] = searchStart ' restart scan if maximum is reached + TTL_current = TTL_HIGH + Else + lockIter[channel] = lockIter[channel] + 1 + TTL_current = TTL_LOW + EndIf + EndIf + + ' makes sense to move the actual output to the end, as the voltage will only apply on the next cycle anyway + dac_output[channel] = lockIter[channel] + EndIf + + next channel +EndSub + +Sub LimitToOutputRange() + Dim iij as Long + 'setting values which are above 65535 or below 0 to the maximum(65535) respectively minimum value(0) of the P2_Write_DAC8 command, + 'since it only takes the last 16 bits of integers out of its boundaries + for iij=1 to NUM_DAC_CHANNELS + if (dac_output[iij]>65535) then + dac_output[iij]=65535 + endif + if (dac_output[iij]<0) then + dac_output[iij]=0 + endif + next iij +EndSub + +Sub UpdateLastOutput() + for i=1 to NUM_FILTERS + last_output[i] = dac_output[i] + next i + ' Alternative: + ' P2_Write_DAC8(last_output, dac_output, 1) +EndSub + + +Init: + Processdelay = CPU_CLK/PROC_CLK ' 200 KHz + 'setting the values of the filter control register to zero + Par_11=0 + Par_12=0 + Par_13=0 + Par_14=0 + Par_15=0 + Par_16=0 + Par_17=0 + Par_18=0 + 'prepoluate dac_output + + input_sensitivity = 0 + input_sensitivity_last = 1 + timer = 0 + timer_last = Read_Timer() + ramp_chan = 0 + + Fifo_Clear(3) + + CPU_Dig_IO_Config(110011b) + + ' TODO nicer? + for i=1 to 8 + monitor_sel[i] = 0 + next i + + 'populates ramp_data with the values from -10V to 10V + For ramp_idx = 0 To 0ffffh + ramp_data[ramp_idx+1] = ramp_idx - 8000h + ramp_data[20000h-ramp_idx] = ramp_data[ramp_idx+1] + Next ramp_idx + ramp_idx = 1 + + P2_Set_Average_Filter(AIN_MODULE_1, 0) ' use 5 to average 32 measurements + + ' Pseudo code: + ' ADCF_Mode = 2^(AIN_MODULE_1-1) + 2^(AIN_MODULE_2-1) + ADCF_Mode = Shift_Left(1, AIN_MODULE_1-1) + Shift_Left(1, AIN_MODULE_2-1) + + P2_ADCF_Mode(ADCF_Mode, 1) ' Enable Timer mode on Module 2 & 3 (bit flag for module address!) + + +Event: + t1 = Read_Timer() + + ' Start DAC conversion on all DACs simultaneously + P2_Sync_All(Shift_Left(1, AOUT_MODULE_1-1) Or Shift_Left(1, AOUT_MODULE_2-1)) + + ' Read in values from ADC modules, these have already been converted because of the + ' ADCF timer mode + P2_Read_ADCF8(AIN_MODULE_1, adc_input, 1) + P2_Read_ADCF8(AIN_MODULE_2, adc_input, 9) + SendToMonitors() SendToFifo() - RunFilters(adc_input, dac_output) - - RunRamp() - - LimitToOutputRange() - - UpdateLastOutput() - - ' pre-populate DACs with new values - P2_Write_DAC8(AOUT_MODULE_1, dac_output, 1) - P2_Write_DAC8(AOUT_MODULE_2, dac_output, 9) - - Blinkenlights() - - SetInputSensitivity() - - t2 = Read_Timer() - new_time = t2 - t1 -4 - Par_7 = new_time - -Finish: - ' Switch off LEDs - P2_Set_Led(AIN_MODULE_1, 0) - P2_Set_Led(AIN_MODULE_2, 0) - P2_Set_Led(AOUT_MODULE_1, 0) - P2_Set_Led(AOUT_MODULE_2, 0) + RunFilters(adc_input, dac_output) + + AutoLock() ' has to be before limiting output range but after RunFilters + + RunRamp() + + LimitToOutputRange() + + UpdateLastOutput() + + ' pre-populate DACs with new values + P2_Write_DAC8(AOUT_MODULE_1, dac_output, 1) + P2_Write_DAC8(AOUT_MODULE_2, dac_output, 9) + + Blinkenlights() + + SetInputSensitivity() + + t2 = Read_Timer() + new_time = t2 - t1 -4 + Par_7 = new_time + +Finish: + ' Switch off LEDs + P2_Set_Led(AIN_MODULE_1, 0) + P2_Set_Led(AIN_MODULE_2, 0) + P2_Set_Led(AOUT_MODULE_1, 0) + P2_Set_Led(AOUT_MODULE_2, 0) diff --git a/src/nqontrol/controller.py b/src/nqontrol/controller.py index 4ce01e40623f27032c56f639591783dbcc3a3a46..59499e2e9bbc9b21e9449844ab53646e8eb8f6bd 100644 --- a/src/nqontrol/controller.py +++ b/src/nqontrol/controller.py @@ -375,8 +375,8 @@ def getOutputStates(deviceNumber, servoNumber): servo = device(deviceNumber).servo(servoNumber) if servo.auxSw: checklist.append('aux') - if servo.snapSw: - checklist.append('snap') + # if servo.snapSw: + # checklist.append('snap') if servo.outputSw: checklist.append('output') return checklist @@ -548,7 +548,7 @@ def getMonitorsServo(deviceNumber, channel): :obj:`int` Monitor channel index or None. - """.format(device(deviceNumber).NUMBER_OF_MONITORS) + """.format(settings.NUMBER_OF_MONITORS) dev = device(deviceNumber) channelData = dev.monitors[channel - 1] # channel data is either a dict or None @@ -575,7 +575,7 @@ def getMonitorsCard(deviceNumber, channel): :obj:`String` Card specifier or `None`. - """.format(device(deviceNumber).NUMBER_OF_MONITORS) + """.format(settings.NUMBER_OF_MONITORS) dev = device(deviceNumber) channelData = dev.monitors[channel - 1] if channelData is not None: @@ -606,8 +606,50 @@ def getSDGain(deviceNumber): return servoDesign.gain -def getSnapLimit(deviceNumber, servo): - """Return the initial value of the snap voltage limit of a servo. Part of the servo section. +# def getSnapLimit(deviceNumber, servo): +# """Return the initial value of the snap voltage limit of a servo. Part of the servo section. +# +# Parameters +# ---------- +# deviceNumber : :obj:`int` +# :obj:`ServoDevice.deviceNumber` +# servo : :obj:`int` +# Servo index :obj:`servo.channel` +# +# Returns +# ------- +# :obj:`float` +# The snap voltage limit as a float. +# +# """ +# servo = device(deviceNumber).servo(servo) +# return servo.snap +# +# +# def getSnapGreater(deviceNumber, servo): +# """Return the initial value of the snap condition of a servo. The boolean translates to greater (True) or lesser than (False) the limit. Part of the servo section. +# +# Parameters +# ---------- +# deviceNumber : :obj:`int` +# :obj:`ServoDevice.deviceNumber` +# servo : :obj:`int` +# Servo index :obj:`servo.channel` +# +# Returns +# ------- +# :obj:`bool` +# The snap condition as a boolean. +# +# """ +# servo = device(deviceNumber).servo(servo) +# return servo.snapGreater + + +def getLockRange(deviceNumber, servo): + """Returns a list containing minimum and maximum value of the autolock sections RangeSlider.abs + + The AutoLock options are located in the servo section. Parameters ---------- @@ -618,16 +660,38 @@ def getSnapLimit(deviceNumber, servo): Returns ------- - :obj:`float` - The snap voltage limit as a float. + obj:`list` + [min, max] """ servo = device(deviceNumber).servo(servo) - return servo.snap + return [servo._autolock['min'], servo._autolock['max']] -def getSnapGreater(deviceNumber, servo): - """Return the initial value of the snap condition of a servo. The boolean translates to greater (True) or lesser than (False) the limit. Part of the servo section. +def getLockThreshold(deviceNumber, servo): + """Returns the threshold value of the autolock. + + The AutoLock options are located in the servo section. + + Parameters + ---------- + deviceNumber : :obj:`int` + :obj:`ServoDevice.deviceNumber` + servo : :obj:`int` + Servo index :obj:`servo.channel` + + Returns + ------- + obj:`float` + The voltage value. + + """ + servo = device(deviceNumber).servo(servo) + return servo._autolock['threshold'] + + +def getLockGreater(deviceNumber, servo): + """Return the initial value of the lock condition of a servo. The boolean translates to greater (True) or lesser than (False) the threshold. Part of the servo section. Parameters ---------- @@ -639,12 +703,70 @@ def getSnapGreater(deviceNumber, servo): Returns ------- :obj:`bool` - The snap condition as a boolean. + The lock condition as a boolean. """ servo = device(deviceNumber).servo(servo) - return servo.snapGreater + return servo._autolock['greater'] + +def getAutolockList(deviceNumber): + """Returns a list containing the channel indices of all active servo locks for the AutoLock checklist. + + The AutoLock checklist is part of the app header. + + Parameters + ---------- + deviceNumber : :obj:`int` + :obj:`ServoDevice.deviceNumber` + + Returns + ------- + obj:`list` + List containing servo channel indices. + + """ + locks = [] + for i in range(1, settings.NUMBER_OF_SERVOS + 1): + state = device(deviceNumber).servo(i)._autolock['state'] + if state: + locks.append(i) + return locks + + +def getLockRelock(deviceNumber, servo): + """Return whether auto-relock is on or off for a given servo. + + Parameters + ---------- + deviceNumber : :obj:`int` + :obj:`ServoDevice.deviceNumber` + servo : :obj:`int` + Servo index :obj:`servo.channel` + + Returns + ------- + :obj:`list` + Empty list means `False`, element in list means `True`. + + """ + result = [] + if device(deviceNumber).servo(servo).relock: + result.append('on') + return result + + +def getLockStatus(deviceNumber, servo): + servo = device(deviceNumber).servo(servo) + lockstatus = servo.lockState + greater = servo.lockGreater + if greater: + greater = '>' + else: + greater = '<' + min, max = servo.lockSearchMin, servo.lockSearchMax + threshold = servo.lockThreshold + return 'Autolock: {} | {}{} | [{} → {}]'.format(lockstatus, greater, threshold, min, max) # # # @@ -944,11 +1066,13 @@ def callOffset(deviceNumber, servoNumber, offset): return 'Offset (' + str(servo.offset) + 'V)' -def callGain(deviceNumber, servoNumber, gain, n_submit, timestamp, timestamp_old): +def callGain(context, deviceNumber, servoNumber, gain): """Handle the servo gain input callback for the UI's servo input section. Parameters ---------- + context : :obj:'json' + Dash callback context. Please check the dash docs for more info. deviceNumber : :obj:`int` :obj:`ServoDevice.deviceNumber` servoNumber : :obj:`int` @@ -963,7 +1087,11 @@ def callGain(deviceNumber, servoNumber, gain, n_submit, timestamp, timestamp_old """ servo = device(deviceNumber).servo(servoNumber) - if n_submit is not None and timestamp != timestamp_old: + + # determining context of input + triggered = context.triggered[0]['prop_id'].split('.')[0] + + if 'gain' in triggered: # case when gain is changed by submitting the input with Enter try: gain = fast_real(gain, raise_on_invalid=True) @@ -971,7 +1099,7 @@ def callGain(deviceNumber, servoNumber, gain, n_submit, timestamp, timestamp_old raise PreventUpdate('Empty or no real number input.') if servo.gain != gain: servo.gain = gain - else: + elif 'sos' in triggered: # Case when gain is changed by applying a servo design servoDesign = device(deviceNumber).servoDesign servo.gain = servoDesign.gain @@ -1040,10 +1168,10 @@ def callServoChannels(deviceNumber, servoNumber, inputValues): else: servo.auxSw = False - if 'snap' in inputValues: - servo.snapSw = True - else: - servo.snapSw = False + # if 'snap' in inputValues: + # servo.snapSw = True + # else: + # servo.snapSw = False if 'output' in inputValues: servo.outputSw = True @@ -1209,9 +1337,10 @@ def callPlantParse(deviceNumber, filename, contents, n_clicks, timestamp, timest content_type, content_string = contents.split(',') decoded = base64.b64decode(content_string).decode('utf-8', 'ignore') try: - df = io.read(decoded) + df = io.read(files=decoded) servoDesign.plant = df except Exception as e: + log.warn(e) raise PreventUpdate(str(e)) return timestamp @@ -1409,53 +1538,136 @@ def callToggleRamp(targetInput, deviceNumber): Information string as callback needs some output. Basically a dummy. """ - if targetInput is False: - device(deviceNumber).servo(1).disableRamp() # Not soo nice, but works. Better idea? + servo = device(deviceNumber).servo(targetInput) + if not targetInput: + device(deviceNumber).servo(1).disableRamp() return 'Disabled' + elif servo._autolock['state']: + log.warn('Autolock is active, ramp was not activated.') + return 'Could not update ramp. lock was active.' else: - servo = device(deviceNumber).servo(targetInput) if not servo.rampEnabled: servo.enableRamp() return 'Ramp on channel ' + str(targetInput) -def callRampAmplitude(amp, deviceNumber, servoNumber): - """Send ramp parameters entered in servo control section of the UI to the corresponding :obj:`nqontrol.Servo`. +def callAutolock(values, deviceNumber): + """Enables the auto-lock feature on all selected servos. Ramp is not compatible with Autolock (Essentially, the autolock should be a better ramp). + + GUI wise, the autolock-checklist is located in the header section. Parameters ---------- - amp : :obj:`float` - Ramp amplitude. - freq : :obj:`float` - Ramp frequency. + values : :obj:`list` + The GUI returns a list containing all channel numbers where the autolock should be active. deviceNumber : :obj:`int` :obj:`ServoDevice.deviceNumber` - servoNumber : :obj:`int` - Servo index :obj:`servo.channel` Returns ------- - :obj:`String` - UI label string describing current ramp state. + :obj:`bool` + Returns False in order to disable Ramp in the GUI in case there is an active autolock. """ - servo = device(deviceNumber).servo(servoNumber) - servo.rampAmplitude = amp - return 'Amplitude: ' + str(amp) + # check whether list is empty + ramp = device(deviceNumber).rampEnabled + for i in range(1, settings.NUMBER_OF_SERVOS + 1): + servo = device(deviceNumber).servo(i) + if i in values: + # in order to retrigger a lock one has to uncheck the box first and recheck it + if not servo._autolock['state']: # only set this to 1 if it was 0 before (inactive before) + servo.autolock(1) # setting state to 1 starts searching for peak + if ramp == i: + ramp = 0 # setting the ramp to 0 for the GUI in case RAMP was active on an autolock channel + else: + servo.autolock(0) # setting state to zero turns off the lock + servo._readFilterControl() + return ramp # returning the ramp value for the GUI, if the ramp doesnt interfere with any lock, nothing changes + + +def lockStatus(lockstate, threshold, sliderlist, greater, context, deviceNumber, servo): + servo = device(deviceNumber).servo(servo) + # determining context of input + triggered = context.triggered[0]['prop_id'].split('.')[0] + + # checking for input changes, sending to servo accordingly + if 'lockSlider' in triggered and sliderlist: + servo.lockSearchMin = sliderlist[0] + servo.lockSearchMax = sliderlist[1] + + if 'threshold' in triggered: + try: + threshold = fast_real(threshold, raise_on_invalid=True) + except (ValueError, TypeError): + raise PreventUpdate('Empty or no real number input.') + if -10 >= threshold >= 10: + log.warn('Value out of bounds!') + raise PreventUpdate('Value has to be between -10 and 10V!!') + servo.lockThreshold = threshold + + if 'lockGreater' in triggered: + if not isinstance(greater, bool): + raise PreventUpdate('Has to be a boolean.') + servo.lockGreater = greater + + # getting all values, sending to GUI + lockstatus = servo.lockState + greater = servo.lockGreater + if greater: + greater = '>' + else: + greater = '<' + min, max = servo.lockSearchMin, servo.lockSearchMax + threshold = servo.lockThreshold + auto = servo.relock + if auto: + auto = '(relock)' + else: + auto = '' + return 'Autolock: {} | {}{} | [{} → {}] | {}'.format(lockstatus, greater, threshold, min, max, auto) -def callRampFrequency(freq, deviceNumber, servoNumber): + +# def callAutolockRelock(values, deviceNumber, servo): +# """Set whether the AutoLock should relock automatically whenever falling above/below threshold for a given servo. +# +# Parameters +# ---------- +# values : :obj:`list` +# As with all Dash checklists, even though this is for a single element, the callback input is a list. Empty list means off, none-empty means on. +# deviceNumber : :obj:`int` +# :obj:`ServoDevice.deviceNumber` +# servoNumber : :obj:`int` +# Servo index :obj:`servo.channel` +# +# Returns +# ------- +# :obj:`bool` +# The relock value, since the UI requires a return. +# +# """ +# servo = device(deviceNumber).servo(servo) +# if values: +# servo.relock = True +# else: +# servo.relock = False +# return servo.relock + + +def callRamp(amp, freq, context, deviceNumber, servoNumber): """Send ramp parameters entered in servo control section of the UI to the corresponding :obj:`nqontrol.Servo`. Parameters ---------- - amp: :obj:`float` + amp : :obj:`float` Ramp amplitude. - freq: :obj:`float` + freq : :obj:`float` Ramp frequency. - deviceNumber: :obj:`int` + context : :obj:'json' + Dash callback context. Please check the dash docs for more info. + deviceNumber : :obj:`int` :obj:`ServoDevice.deviceNumber` - servoNumber: :obj:`int` + servoNumber : :obj:`int` Servo index :obj:`servo.channel` Returns @@ -1465,8 +1677,15 @@ def callRampFrequency(freq, deviceNumber, servoNumber): """ servo = device(deviceNumber).servo(servoNumber) - servo.rampFrequency = freq - return 'Frequency: ' + str(round(freq, 2)) + + triggered = context.triggered[0]['prop_id'].split('.')[0] + if 'ramp' in triggered: + servo.rampAmplitude = amp + if 'freq' in triggered: + servo.rampFrequency = freq + amp = servo.rampAmplitude + freq = servo.rampFrequency + return 'Amplitude: {} | Frequency: {}'.format(amp, round(freq, 2)) def callADwinMonitor(channel, servo, card, deviceNumber): @@ -1707,35 +1926,35 @@ def callTempButton(clicks, dt, num, port, interval, voltLim, servo, deviceNumber return 'Start' # new label text for the button -def callSnapParam(limit, greater, servo, deviceNumber): - """Set the snapping parameters for the servo. - - Parameters - ---------- - limit : :obj:`float` - Voltage limit for snapping. - greater : :obj:`bool` - Boolean specifying whether to snap when greater than or lower than limit. - deviceNumber: :obj:`int` - :obj:`ServoDevice.deviceNumber` - servo: :obj:`int` - Servo index :obj:`servo.channel` - - Returns - ------- - :obj:`String` - Updates the snap label with the current limit value. - - """ - servo = device(deviceNumber).servo(servo) - try: - limit = fast_real(limit, raise_on_invalid=True) - except (ValueError, TypeError): - raise PreventUpdate('A snap value has to be provided.') - servo.snap = limit - servo.snapGreater = greater - servo.snapSend(limit, greater) - return f'Snap ({limit} V)' +# def callSnapParam(limit, greater, servo, deviceNumber): +# """Set the snapping parameters for the servo. +# +# Parameters +# ---------- +# limit : :obj:`float` +# Voltage limit for snapping. +# greater : :obj:`bool` +# Boolean specifying whether to snap when greater than or lower than limit. +# deviceNumber: :obj:`int` +# :obj:`ServoDevice.deviceNumber` +# servo: :obj:`int` +# Servo index :obj:`servo.channel` +# +# Returns +# ------- +# :obj:`String` +# Updates the snap label with the current limit value. +# +# """ +# servo = device(deviceNumber).servo(servo) +# try: +# limit = fast_real(limit, raise_on_invalid=True) +# except (ValueError, TypeError): +# raise PreventUpdate('A snap value has to be provided.') +# servo.snap = limit +# servo.snapGreater = greater +# servo.snapSend(limit, greater) +# return f'Snap ({limit} V)' # # # diff --git a/src/nqontrol/general.py b/src/nqontrol/general.py index 1e832d7840f0c47f45da03d422adcf3d26e1d8cb..6302640f98cf4b03ee3d07262797d022a42fcbb9 100644 --- a/src/nqontrol/general.py +++ b/src/nqontrol/general.py @@ -1,7 +1,3 @@ -import os -import json - - def setBit(x, offset): mask = 1 << offset return (x | mask) @@ -36,4 +32,4 @@ def readBit(x, offset): def rearrange_filter_coeffs(b, a): """Rearrage coefficients from `a, b` to `c`.""" - return [b[0], a[1], a[2], b[1]/b[0], b[2]/b[0]] + return [b[0], a[1], a[2], b[1] / b[0], b[2] / b[0]] diff --git a/src/nqontrol/guniconfig.py.ini b/src/nqontrol/guniconfig.py.ini index ec00837523d15b4b258170cb6181517de15c85bf..635f9810183908b422c8089193475a1d56fb1fa3 100644 --- a/src/nqontrol/guniconfig.py.ini +++ b/src/nqontrol/guniconfig.py.ini @@ -2,6 +2,8 @@ workers = 1 bind = '0.0.0.0:8000' timeout = 30 -worker_class = 'sync' +worker_class = 'gthread' +threads = 5 debug = True spew = False +preload_app = True diff --git a/src/nqontrol/mockAdwin.py b/src/nqontrol/mockAdwin.py index 4237e8d1190bfd2f31e13b92c6ba637bc0a7d274..4b5526e16fe4abf3c35fd9cdd2a55ae96717b026 100644 --- a/src/nqontrol/mockAdwin.py +++ b/src/nqontrol/mockAdwin.py @@ -23,6 +23,7 @@ class MockADwin(): self._data_long[4 - 1] = [0] * 8 self._data_long[6 - 1] = [0] * 8 self._data_long[7 - 1] = [0] * 8 + self._data_long[settings.DATA_LOCK - 1] = [0] * (8 * 5) self.Set_Par(7, 3000) def Boot(self, file): @@ -114,7 +115,14 @@ class MockADwin(): def _constructOutput(self, amount, input, aux): channel = self.Get_Par(4) inputSw, offsetSw, auxSw, outputSw, snapSw = self._readSwitches(channel) - output = np.full(amount, 0x8000).astype(int) + + # lock simulation + data = self._data_long[7] + lockoffset = data[(channel - 1) * 5 + 4] + if data[(channel - 1) * 5] == 1: + output = np.full(amount, lockoffset).astype(int) + else: + output = np.full(amount, 0x8000).astype(int) if not outputSw: return output if inputSw: @@ -180,7 +188,7 @@ class MockADwin(): # snapping emulation aux = 0x8000 for servo in range(1, 9): - control = self._par[9+servo] + control = self._par[9 + servo] snap_enabled = control & 8 if snap_enabled: @@ -191,10 +199,48 @@ class MockADwin(): if snap_value <= 0: control = general.clearBit(control, 3) # disable snapSw control = general.setBit(control, 1) # enable outputSw - self._par[9+servo] = control + self._par[9 + servo] = control self._par[2] &= ~(15) # stop the ramp - self.Set_Par + # locking emulation + aux = 0x8000 + data = self._data_long[7] + for servo in range(1, 9): + indexoffset = (servo - 1) * 5 + state = (data[indexoffset] & 0x3) + relock = (data[indexoffset] & 0x4) + threshold = (data[indexoffset + 1] & 0xffff) + greater = general.readBit(data[indexoffset + 1], 16) + start = data[indexoffset + 2] + + control = self._par[9 + servo] + + if state == 0: + data[indexoffset + 4] = 0 + + if state == 2: + if greater and (aux < threshold * .9): + if relock: + data[indexoffset] = 1 + relock + else: + data[indexoffset] = 0 + relock + elif (not greater) and (aux > threshold * 0.9): + if relock: + data[indexoffset] = 1 + relock + else: + data[indexoffset] = 0 + relock + + if state == 1: + if (greater and aux > threshold) or ((aux < threshold) and not greater): + data[indexoffset] = 2 + relock + control = general.setBit(control, 0) # enable input + control = general.setBit(control, 1) # enable output + else: + data[indexoffset + 4] = start + control = general.clearBit(control, 0) # input + control = general.clearBit(control, 1) # output + self._par[9 + servo] = control + self._data_long[7] = data def _fpar_special_functions(self): pass diff --git a/src/nqontrol/nqontrol.TC1 b/src/nqontrol/nqontrol.TC1 deleted file mode 100755 index 98da1a6e79882693e1061c0637f128d0d9a21ec8..0000000000000000000000000000000000000000 Binary files a/src/nqontrol/nqontrol.TC1 and /dev/null differ diff --git a/src/nqontrol/nqontrol.TC1 b/src/nqontrol/nqontrol.TC1 new file mode 120000 index 0000000000000000000000000000000000000000..15b4876bdbd3dcfcfb681cff7d8895d6b43bc952 --- /dev/null +++ b/src/nqontrol/nqontrol.TC1 @@ -0,0 +1 @@ +../adbasic/nqontrol.TC1 \ No newline at end of file diff --git a/src/nqontrol/nqontrolUI.py b/src/nqontrol/nqontrolUI.py index 7a4254df93e767e80494a509a83a811f24068ccc..2bdba327019ade25216f8ac881d07bf1db2a59dc 100644 --- a/src/nqontrol/nqontrolUI.py +++ b/src/nqontrol/nqontrolUI.py @@ -8,9 +8,10 @@ # # https://dash.plot.ly/ # ---------------------------------------------------------------------------------------- - +import dash import dash_core_components as dcc import dash_html_components as html +from dash_daq import ToggleSwitch from dash.dependencies import Input, Output, State from abc import ABCMeta, abstractmethod from nqontrol import settings, controller @@ -109,7 +110,7 @@ class UIDevice(UIComponent): **UIDevice** (see :obj:`nqontrol.settings.DEVICES_LIST`) _(header, servoDetails and rest refer to local variables within the layout property for better source code navigation)_ header (defined within UIDevice, see layout property) - servoDetails (as many as defined in :obj:`nqontrol.ServoDevice.NUMBER_OF_SERVOS`, defined within UIDevice, see layout property) + servoDetails (as many as defined in :obj:`settings.NUMBER_OF_SERVOS`, defined within UIDevice, see layout property) :obj:`nqontrol.nqontrolUI.UIServoSection` :obj:`nqontrol.nqontrolUI.UIRamp` Servo Naming element (defined within UIDevice) @@ -144,7 +145,7 @@ class UIDevice(UIComponent): def __init__(self, app, deviceNumber): super().__init__(app, deviceNumber) - self._numberOfServos = controller.device(self._deviceNumber).NUMBER_OF_SERVOS + self._numberOfServos = settings.NUMBER_OF_SERVOS self.__setUpComponents() def __setUpComponents(self): @@ -152,10 +153,12 @@ class UIDevice(UIComponent): self._uiServoSection = [] self._uiRamps = [] self._uiTempFeedbacks = [] + # self._uiAutoLocks = [] for servoNumber in range(1, self._numberOfServos + 1): self._uiServoSection.append(UIServoSection(self._app, self._deviceNumber, servoNumber)) self._uiRamps.append(UIRamp(self._app, self._deviceNumber, servoNumber)) self._uiTempFeedbacks.append(UITempFeedback(self._app, self._deviceNumber, servoNumber)) + # self._uiAutoLocks.append(UIAutoLock(self._app, self._deviceNumber, servoNumber)) self._uiSDPlot = UIServoDesignPlot(self._app, self._deviceNumber) self._monitor = UIMonitor(self._app, self._deviceNumber) @@ -192,14 +195,27 @@ class UIDevice(UIComponent): children=[ html.Div(children=['Ramp'], className='col-2 align-self-center'), dcc.RadioItems( - options=[{'label': 'Off', 'value': False}] + [{'label': i, 'value': i} - for i in range(1, self._numberOfServos + 1)], + options=[{'label': 'Off', 'value': 0}] + [{'label': i, 'value': i} + for i in range(1, self._numberOfServos + 1)], value=controller.getCurrentRampLocation(self._deviceNumber), id='rampTarget_{}'.format(self._deviceNumber), className='col-10', inputClassName='form-check-input', labelClassName='form-check form-check-inline' - )], + ), + # html.Div( + # children=['AutoLock'], + # className='col-2 align-self-center' + # ), + # dcc.Checklist( + # options=[{'label': i, 'value': i} for i in range(1, self._numberOfServos + 1)], + # values=controller.getAutolockList(self._deviceNumber), + # id='autolockChecklist_{}'.format(self._deviceNumber), + # className='pl-0 col-10', + # inputClassName='form-check-input', + # labelClassName='form-check form-check-inline' + # ) + ], className='row' ) ], @@ -251,9 +267,18 @@ class UIDevice(UIComponent): html.Details( children=[ html.Summary( - [controller.getServoName(self._deviceNumber, i)], - className='col-12', - id='servoName_{0}_{1}'.format(self._deviceNumber, i) + children=[ + html.Span( + [controller.getServoName(self._deviceNumber, i)], id='servoName_{0}_{1}'.format(self._deviceNumber, i), + style={'width': '50%'} + ), + # html.Span( + # [controller.getLockStatus(self._deviceNumber, i)], + # id='servoStatus_{0}_{1}'.format(self._deviceNumber, i), + # style={'width': '50%', 'text-align': 'right'} + # ) + ], + className='col-12 d-flex', ), # within the details component there needs to be another div wrapper for some reason. if removed, the servo and ramp sections will align as if on separate rows. Since bootstrap also requires the nesting of col- classes within row-classes, the structure looks a bit unreadable html.Div( @@ -267,7 +292,20 @@ class UIDevice(UIComponent): ], className='row' ), - self._uiTempFeedbacks[i - 1].layout, + # this is an ugly version to wrap it... buuut hey. + html.Div( + children=[ + html.Div( + [self._uiTempFeedbacks[i - 1].layout], + className='col-12 col-md-6 p-0 m-0' + ), + # html.Div( + # [self._uiAutoLocks[i - 1].layout], + # className='col-12 col-md-6 p-0 m-0' + # ) + ], + className='row' + ), html.Div( children=[ html.P('Name', className='col-auto mb-0'), @@ -303,7 +341,7 @@ class UIDevice(UIComponent): # Update timer dcc.Interval( id='update_stamps_{}'.format(self._deviceNumber), - interval=1000, + interval=2000, n_intervals=0 ) ] @@ -324,14 +362,28 @@ class UIDevice(UIComponent): [State('servoNameInput_{0}_{1}'.format(self._deviceNumber, i), 'value')] )(dynamically_generated_function) + # # Servo status callback + # dynamically_generated_function = self.__createLockStatusCallback(i) + # self._app.callback( + # Output('servoStatus_{0}_{1}'.format(self._deviceNumber, i), 'children'), + # [Input('update_stamps_{}'.format(self._deviceNumber), 'n_intervals'), + # Input('lockSlider_{0}_{1}'.format(self._deviceNumber, i), 'value'), + # Input()] + # )(dynamically_generated_function) + self.__setDeviceCallbacks() - for componentList in (self._uiServoSection, self._uiRamps, self._uiTempFeedbacks): + for componentList in (self._uiServoSection, self._uiRamps, self._uiTempFeedbacks): # , self._uiAutoLocks is commented out for unit in componentList: unit.setCallbacks() self._monitor.setCallbacks() self._uiSDPlot.setCallbacks() + # def __createLockStatusCallback(self, servoNumber): + # def callback(n_interval): + # return controller.getLockStatus(self._deviceNumber, servoNumber) + # return callback + # Callbacks for the device control, e.g. timestamp and workload. def __setDeviceCallbacks(self): @@ -339,7 +391,7 @@ class UIDevice(UIComponent): dynamically_generated_function = self.__createWorkTimeCallback(worktime) self._app.callback( Output(worktime, 'children'), - [Input('update_stamps_{}'.format(self._deviceNumber), 'n_intervals')] + [Input('update_monitor_{}'.format(self._deviceNumber), 'n_intervals')] )(dynamically_generated_function) reboot = 'device_reboot_button_{}'.format(self._deviceNumber) @@ -356,6 +408,15 @@ class UIDevice(UIComponent): [Input(ramp_servo_target, 'value')] )(dynamically_generated_function) + # autolockChecklist = 'autolockChecklist_{}'.format(self._deviceNumber) + # dynamically_generated_function = self.__createAutolockCallback() + # self._app.callback( + # Output('rampTarget_{}'.format(self._deviceNumber), 'value'), # enabling an autolock disables the ramp + # [Input(autolockChecklist, 'values'), + # # Input(ramp_servo_target, 'value') + # ] + # )(dynamically_generated_function) + saveTextArea = 'save_name_{}'.format(self._deviceNumber) saveButton = 'device_save_button_{}'.format(self._deviceNumber) dynamically_generated_function = self.__createSaveCallback() @@ -381,7 +442,13 @@ class UIDevice(UIComponent): # Callback for the RAMP switch def __createRampCallback(self): def callback(targetInput): - controller.callToggleRamp(targetInput, self._deviceNumber) + return controller.callToggleRamp(targetInput, self._deviceNumber) + return callback + + # AutoLock callback + def __createAutolockCallback(self): + def callback(values): + return controller.callAutolock(values, self._deviceNumber) return callback # Reboot button @@ -426,9 +493,7 @@ class UIServoSection(UIComponent): Output('gain_label_{0}_{1}'.format(self._deviceNumber, self._servoNumber), 'children'), [Input(gain, 'n_submit'), Input('sosSwitchStorage_{}'.format(self._deviceNumber), 'data')], - [State(gain, 'value'), - State(gain, 'n_submit_timestamp'), - State(gainStore, 'data')] + [State(gain, 'value')] )(dynamically_generated_function) # Gain Store Callback @@ -479,17 +544,17 @@ class UIServoSection(UIComponent): )(dynamically_generated_function) # Snap parameters callback - snapGreater = 'snapGreater_{0}_{1}'.format(self._deviceNumber, self._servoNumber) - snapLimit = 'snapLimit_{0}_{1}'.format(self._deviceNumber, self._servoNumber) - snapLabel = 'snapLabel_{0}_{1}'.format(self._deviceNumber, self._servoNumber) - dynamically_generated_function = self.__createSnapCallback() - self._app.callback( - Output(snapLabel, 'children'), - [Input(snapLimit, 'n_submit'), - Input(snapGreater, 'value')], - [State(snapLimit, 'value'), - State(snapGreater, 'value')] - )(dynamically_generated_function) + # snapGreater = 'snapGreater_{0}_{1}'.format(self._deviceNumber, self._servoNumber) + # snapLimit = 'snapLimit_{0}_{1}'.format(self._deviceNumber, self._servoNumber) + # snapLabel = 'snapLabel_{0}_{1}'.format(self._deviceNumber, self._servoNumber) + # dynamically_generated_function = self.__createSnapCallback() + # self._app.callback( + # Output(snapLabel, 'children'), + # [Input(snapLimit, 'n_submit'), + # Input(snapGreater, 'value')], + # [State(snapLimit, 'value'), + # State(snapGreater, 'value')] + # )(dynamically_generated_function) @property def layout(self): @@ -536,7 +601,7 @@ class UIServoSection(UIComponent): className='row' ), ], - className='col-6 col-xl-3', + className='col-3', ), # Offset and Gain, also part of the input html.Div( @@ -556,26 +621,26 @@ class UIServoSection(UIComponent): id='gain_{0}_{1}'.format(self._deviceNumber, self._servoNumber), className='form-control' ), - # Snap - html.P('Snap', id='snapLabel_{0}_{1}'.format(self._deviceNumber, self._servoNumber)), - # The snapping condition dropdown - dcc.Dropdown( - options=[ - {'label': '>', 'value': True}, - {'label': '<', 'value': False} - ], - value=controller.getSnapGreater(self._deviceNumber, self._servoNumber), - clearable=False, - id='snapGreater_{0}_{1}'.format(self._deviceNumber, self._servoNumber), - className='w-100 m-0' - ), - # the input field for the snap voltage limit - dcc.Input( - placeholder='snap limit', - id='snapLimit_{0}_{1}'.format(self._deviceNumber, self._servoNumber), - value=controller.getSnapLimit(self._deviceNumber, self._servoNumber), - className='form-control w-100' - ), + # # Snap + # html.P('Snap', id='snapLabel_{0}_{1}'.format(self._deviceNumber, self._servoNumber)), + # # The snapping condition dropdown + # dcc.Dropdown( + # options=[ + # {'label': '>', 'value': True}, + # {'label': '<', 'value': False} + # ], + # value=controller.getSnapGreater(self._deviceNumber, self._servoNumber), + # clearable=False, + # id='snapGreater_{0}_{1}'.format(self._deviceNumber, self._servoNumber), + # className='w-100 m-0' + # ), + # # the input field for the snap voltage limit + # dcc.Input( + # placeholder='snap limit', + # id='snapLimit_{0}_{1}'.format(self._deviceNumber, self._servoNumber), + # value=controller.getSnapLimit(self._deviceNumber, self._servoNumber), + # className='form-control w-100' + # ), # Store component in order to determine how callGain was triggered. Saves previous timestamp dcc.Store( id='gainStore_{0}_{1}'.format(self._deviceNumber, self._servoNumber) @@ -583,7 +648,7 @@ class UIServoSection(UIComponent): # Storage component to use as input channels checklist target in callbacks dcc.Store(id='channelChecklistStorage_{0}_{1}'.format(self._deviceNumber, self._servoNumber)), ], - className='col-6 col-xl-3' + className='col-3' ), # Filter section of the servo controls html.Div( @@ -601,7 +666,7 @@ class UIServoSection(UIComponent): # Storage component to use as target for filter checklist target in callback dcc.Store(id='filterChecklistStorage_{0}_{1}'.format(self._deviceNumber, self._servoNumber)), ], - className='col-6 col-xl-3' + className='col-3' ), # Output section of the servo controls html.Div( @@ -611,7 +676,7 @@ class UIServoSection(UIComponent): dcc.Checklist( options=[ {'label': 'Aux', 'value': 'aux'}, - {'label': 'Snap', 'value': 'snap'}, + # {'label': 'Snap', 'value': 'snap'}, {'label': 'Output', 'value': 'output'}, ], values=controller.getOutputStates(self._deviceNumber, self._servoNumber), @@ -639,19 +704,19 @@ class UIServoSection(UIComponent): className='row' ), ], - className='col-6 col-xl-3 ', + className='col-3', )], className='row' ) ], - className='col-12 col-sm-6 d-inline' + className='col-12 col-xl-6 d-inline' ) # Callback for the snap section - def __createSnapCallback(self): - def callback(submit, dropdown, limit, greater): - return controller.callSnapParam(limit, greater, self._servoNumber, self._deviceNumber) - return callback + # def __createSnapCallback(self): + # def callback(submit, dropdown, limit, greater): + # return controller.callSnapParam(limit, greater, self._servoNumber, self._deviceNumber) + # return callback # Callback for the Offset Input Field def __createOffsetCallback(self): @@ -661,8 +726,9 @@ class UIServoSection(UIComponent): # Callback for the Gain Input Field def __createGainCallback(self): - def callback(n_submit, sosTrigger, inputValue, timestamp, timestamp_old): - return controller.callGain(self._deviceNumber, self._servoNumber, inputValue, n_submit, timestamp, timestamp_old) + def callback(n_submit, sosTrigger, inputValue): + context = dash.callback_context + return controller.callGain(context, self._deviceNumber, self._servoNumber, inputValue) return callback def __storeLastGainTimestamp(self): @@ -713,6 +779,7 @@ class UIRamp(UIComponent): html.Div( children=[ html.H3('Ramp', className='col-auto mt-0'), + html.Span(id='current_ramp_{0}_{1}'.format(self._deviceNumber, self._servoNumber), className='col-auto') ], className='row justify-content-between align-items-center' ), @@ -721,7 +788,6 @@ class UIRamp(UIComponent): children=[ html.P( 'Amplitude', - id='current_amp_{0}_{1}'.format(self._deviceNumber, self._servoNumber), className='col-12' ), dcc.Slider( @@ -742,7 +808,6 @@ class UIRamp(UIComponent): children=[ html.P( 'Frequency', - id='current_freq_{0}_{1}'.format(self._deviceNumber, self._servoNumber), className='col-12' ), dcc.Slider( @@ -760,38 +825,27 @@ class UIRamp(UIComponent): className='row justify-content-center', ) ], - className='col-12 col-sm-6 d-inline' + className='col-12 col-xl-6 d-inline' ) # Callback for the Ramp unit's amplitude slider - def __createRampAmpCallback(self): - def callback(amp): - return controller.callRampAmplitude(amp, self._deviceNumber, self._servoNumber) - return callback - - # Callback for the Ramp unit's amplitude slider - def __createRampFreqCallback(self): - def callback(freq): - return controller.callRampFrequency(freq, self._deviceNumber, self._servoNumber) + def __createRampCallback(self): + def callback(amp, freq): + context = dash.callback_context + return controller.callRamp(amp, freq, context, self._deviceNumber, self._servoNumber) return callback def setCallbacks(self): ramp_freq_slider = 'ramp_freq_slider_{0}_{1}'.format(self._deviceNumber, self._servoNumber) ramp_amp_slider = 'ramp_amp_slider_{0}_{1}'.format(self._deviceNumber, self._servoNumber) - amp_label = 'current_amp_{0}_{1}'.format(self._deviceNumber, self._servoNumber) - freq_label = 'current_freq_{0}_{1}'.format(self._deviceNumber, self._servoNumber) - - dynamically_generated_function = self.__createRampAmpCallback() - self._app.callback( - Output(amp_label, 'children'), - [Input(ramp_amp_slider, 'value')], - )(dynamically_generated_function) + amp_out = 'current_ramp_{0}_{1}'.format(self._deviceNumber, self._servoNumber) - dynamically_generated_function = self.__createRampFreqCallback() + dynamically_generated_function = self.__createRampCallback() self._app.callback( - Output(freq_label, 'children'), - [Input(ramp_freq_slider, 'value')], + Output(amp_out, 'children'), + [Input(ramp_amp_slider, 'value'), + Input(ramp_freq_slider, 'value')], )(dynamically_generated_function) @@ -839,9 +893,6 @@ class UIServoDesignPlot(UIComponent): for i in range(5): inputs.append(Input('filter_update_{0}_{1}'.format(self._deviceNumber, i), 'data')) - # inputs.append(Input('filter_unit_dropdown_{0}_{1}'.format(self._deviceNumber, i), 'value')) - # inputs.append(Input('filter_frequency_input_{0}_{1}'.format(self._deviceNumber, i), 'value')) - # inputs.append(Input('filter_optional_input_{0}_{1}'.format(self._deviceNumber, i), 'value')) dynamically_generated_function = self._createGraphCallback() self._app.callback( @@ -877,7 +928,7 @@ class UISecondOrderSection(UIComponent): dcc.Input( type='number', min=1, - max=controller.device(self._deviceNumber).NUMBER_OF_SERVOS, + max=settings.NUMBER_OF_SERVOS, value=1, id='sos_servo_{}'.format(self._deviceNumber), className='form-control' @@ -993,7 +1044,7 @@ class UISecondOrderSection(UIComponent): for filter in self._uiFilters: filter.setCallbacks() - for i in range(1, controller.device(self._deviceNumber).NUMBER_OF_SERVOS + 1): + for i in range(1, settings.NUMBER_OF_SERVOS + 1): sectionCheck = 'filterSectionCheck_{0}_{1}'.format(self._deviceNumber, i) # Apply the values @@ -1204,7 +1255,7 @@ class UIMonitor(UIComponent): html.Div('Servo', className='col-2 align-self-center'), dcc.RadioItems( options=[{'label': i, 'value': i} - for i in range(1, controller.device(self._deviceNumber).NUMBER_OF_SERVOS + 1)], + for i in range(1, settings.NUMBER_OF_SERVOS + 1)], value=1, id='monitorTarget_{}'.format(self._deviceNumber), className='col-10', @@ -1255,7 +1306,7 @@ class UIMonitor(UIComponent): # Update timer dcc.Interval( id='update_monitor_{}'.format(self._deviceNumber), - interval=1 * 500, + interval=1 * 1000, n_intervals=0 ), # Physical ADwin monitor channels @@ -1336,7 +1387,7 @@ class UIADwinMonitorChannels(UIComponent): children=[ dcc.Dropdown( options=[{'label': 'Servo {}'.format(j), 'value': j} for j in range( - 1, controller.device(self._deviceNumber).NUMBER_OF_SERVOS + 1)], + 1, settings.NUMBER_OF_SERVOS + 1)], value=controller.getMonitorsServo(self._deviceNumber, i), placeholder='Servo channel', id='adwin_monitor_channel_target_{0}_{1}'.format(self._deviceNumber, i) @@ -1363,7 +1414,7 @@ class UIADwinMonitorChannels(UIComponent): ), dcc.Store(id='store_adwin_monitor_channel_{0}_{1}'.format(self._deviceNumber, i))], className='row' - ) for i in range(1, controller.device(self._deviceNumber).NUMBER_OF_MONITORS + 1) + ) for i in range(1, settings.NUMBER_OF_MONITORS + 1) ], className='col-12', ) @@ -1384,7 +1435,7 @@ class UIADwinMonitorChannels(UIComponent): """The function initiating all callbacks for the given element. """ - for i in range(1, controller.device(self._deviceNumber).NUMBER_OF_MONITORS + 1): + for i in range(1, settings.NUMBER_OF_MONITORS + 1): # setting the function for each individual i dynamically_generated_function = self.__setADwinMonitorCallback(i) # all HTML IDs relevant to the callback @@ -1422,7 +1473,7 @@ class UITempFeedback(UIComponent): children=[ html.Summary( ['Temperature Feedback Control'], - className='col-12' + className='col-6' ), html.Div( children=[ @@ -1515,7 +1566,7 @@ class UITempFeedback(UIComponent): className='row align-items-center' ), ], - className='col-12 col-md-6' + className='col-12' ) ], className='row p-0 justify-items-start align-items-center', # The detail itself is a row @@ -1607,3 +1658,164 @@ class UITempFeedback(UIComponent): def callback(clicks, dt, num, port, interval, voltLim): return controller.callTempButton(clicks, dt, num, port, interval, voltLim, self._servoNumber, self._deviceNumber) return callback + + +class UIAutoLock(UIComponent): + + def __init__(self, app, deviceNumber, servoNumber): + super().__init__(app, deviceNumber) + self._servoNumber = servoNumber + + @property + def layout(self): + """Returns the elements' structure to be passed to a Dash style layout, usually with html.Div() as a top level container. For additional information read the Dash documentation at https://dash.plot.ly/. + + Returns + ------- + html.Div + The html/dash layout. + + """ + self._layout = html.Details( + children=[ + html.Summary( + ['AutoLock Parameters'], + className='col-6' + ), + html.Div( + children=[ + html.Div( + children=[ + html.Div(['Activate'], className='col-12 col-sm-4'), + html.Div( + children=[ + ToggleSwitch( + value=controller.getAutolockList(self._deviceNumber), + id='autolock_{}_{}'.format(self._deviceNumber, self._servoNumber), + ), + ], + className='col-12 col-sm-4' + ), + ], + className='row align-items-center' + ), + html.Div( + children=[ + html.Div(['Threshold'], className='col-12 col-sm-4'), + html.Div( + children=[ + dcc.Input( + placeholder='Voltage', + className='w-100 form-control', + value=controller.getLockThreshold(self._deviceNumber, self._servoNumber), + id='threshold_{0}_{1}'.format(self._deviceNumber, self._servoNumber), + ), + ], + className='col-12 col-sm-4' + ), + ], + className='row align-items-center' + ), + html.Div( + children=[ + html.Div(['Greater?'], className='col-12 col-sm-4'), + html.Div( + children=[ + dcc.Dropdown( + options=[ + {'label': '>', 'value': True}, + {'label': '<', 'value': False} + ], + value=controller.getLockGreater(self._deviceNumber, self._servoNumber), + clearable=False, + id='lockGreater_{0}_{1}'.format(self._deviceNumber, self._servoNumber), + className='w-100 m-0' + ), + ], + className='col-12 col-sm-4' + ) + ], + className='row align-items-center' + ), + # html.Div( + # children=[ + # html.Div( + # children=[ + # dcc.Checklist( + # options=[ + # {'label': 'Auto-Relock', 'value': 'on'}, + # ], + # values=controller.getLockRelock(self._deviceNumber, self._servoNumber), + # id='autoRelock_{0}_{1}'.format(self._deviceNumber, self._servoNumber), + # className='w-100 m-0', + # inputClassName='form-check-input', + # labelClassName='form-check form-check-inline' + # ), + # ], + # className='col-12 col-sm-4' + # ) + # ], + # className='row align-items-center' + # ), + html.Div( + children=[ + html.Div(['Search Range'], className='col-12'), + dcc.RangeSlider( + count=1, + min=-10, + max=10, + step=.5, + value=controller.getLockRange(self._deviceNumber, self._servoNumber), + allowCross=False, + id='lockSlider_{0}_{1}'.format(self._deviceNumber, self._servoNumber), + className='col-10 mt-1', + pushable=1, + updatemode='drag', + marks={i: '{}'.format(i) for i in range(-10, 11)}, + ), + ], + className='row justify-content-center align-items-center' + ), + ], + className='col-12' + ) + ], + className='row p-0 justify-items-start align-items-center', # The detail itself is a row + style={'margin': '.1vh .5vh'} + ) + return self._layout + + def setCallbacks(self): + """The function initiating all callbacks for the given element. + """ + # relock = 'autoRelock_{0}_{1}'.format(self._deviceNumber, self._servoNumber) + # relockStore = 'autoRelockStore_{0}_{1}'.format(self._deviceNumber, self._servoNumber) + # + # dynamically_generated_function = self.__createRelockCallback() + # self._app.callback( + # Output(relockStore, 'data'), + # [Input(relock, 'values')] + # )(dynamically_generated_function) + + # Servo status callback + dynamically_generated_function = self.__createLockStatusCallback(self._servoNumber) + self._app.callback( + Output('servoStatus_{0}_{1}'.format(self._deviceNumber, self._servoNumber), 'children'), + [Input('update_stamps_{}'.format(self._deviceNumber), 'n_intervals'), + Input('autolock_{}_{}'.format(self._deviceNumber, self._servoNumber), 'value'), + Input('lockSlider_{0}_{1}'.format(self._deviceNumber, self._servoNumber), 'value'), + Input('threshold_{0}_{1}'.format(self._deviceNumber, self._servoNumber), 'n_submit'), + Input('lockGreater_{0}_{1}'.format(self._deviceNumber, self._servoNumber), 'value')], + [State('threshold_{0}_{1}'.format(self._deviceNumber, self._servoNumber), 'value')] + )(dynamically_generated_function) + + def __createLockStatusCallback(self, servoNumber): + def callback(n_interval, lockstate, sliderlist, n_submit_threshold, greater, threshold): + context = dash.callback_context + return controller.lockStatus(lockstate, threshold, sliderlist, greater, context, self._deviceNumber, servoNumber) + return callback + + # def __createRelockCallback(self): + # def callback(values): + # return controller.callAutolockRelock(values, self._deviceNumber, self._servoNumber) + # return callback diff --git a/src/nqontrol/servo.py b/src/nqontrol/servo.py index b69b76b0d28c7ed6ba40b19f9c23abb976f4380f..fa17bfe3096e65a9faf60444016e4c79f6f174f0 100644 --- a/src/nqontrol/servo.py +++ b/src/nqontrol/servo.py @@ -113,12 +113,12 @@ class Servo: JSONPICKLE = ['servoDesign'] MIN_REFRESH_TIME = .02 DEFAULT_FILTERS = [ - [1, 0, 0, 0, 0], - [1, 0, 0, 0, 0], - [1, 0, 0, 0, 0], - [1, 0, 0, 0, 0], - [1, 0, 0, 0, 0] - ] + [1, 0, 0, 0, 0], + [1, 0, 0, 0, 0], + [1, 0, 0, 0, 0], + [1, 0, 0, 0, 0], + [1, 0, 0, 0, 0] + ] DEFAULT_COLUMNS = ['input', 'aux', 'output'] _manager = mp.Manager() realtime = _manager.dict({ @@ -194,17 +194,25 @@ class Servo: 'offsetSw': False, 'outputSw': False, 'inputSw': False, - 'snapSw': False, - }) - self._snap = self._manager.dict({ - 'limit': 0, - 'greater': True, + # 'snapSw': False, }) + # self._snap = self._manager.dict({ + # 'limit': 0, + # 'greater': True, + # }) self._ramp = self._manager.dict({ 'amplitude': .1, 'minimum': 0, 'stepsize': 20, }) + self._autolock = self._manager.dict({ + 'state': 0, + 'threshold': 0, + 'min': -5, + 'max': 5, + 'greater': True, + 'relock': False, + }) self._fifo = self._manager.dict({ 'stepsize': self.DEFAULT_FIFO_STEPSIZE, 'maxlen': settings.FIFO_MAXLEN, @@ -264,39 +272,47 @@ class Servo: def _triggerReload(self): """Trigger bit to trigger reloading of parameters.""" - par = self._adw.Get_Par(2) + par = self._adw.Get_Par(settings.PAR_RELOADBIT) # only trigger if untriggered if not general.readBit(par, self._channel - 1): par = general.changeBit(par, self._channel - 1, True) - self._adw.Set_Par(2, par) + self._adw.Set_Par(settings.PAR_RELOADBIT, par) else: raise Exception("ADwin has been triggered to reload the shared RAM within 10µs or the realtime program doesn't run properly.") def _readFilterControl(self): - c = self._adw.Get_Par(10 + self._channel) + c = self._adw.Get_Par(settings.PAR_FCR + self._channel) # read control bits self._state['auxSw'] = general.readBit(c, 9) for i in list(range(5)): self._state['filtersEnabled'][i] = general.readBit(c, 4 + i) - self._state['snapSw'] = general.readBit(c, 3) + # self._state['snapSw'] = general.readBit(c, 3) self._state['offsetSw'] = general.readBit(c, 2) self._state['outputSw'] = general.readBit(c, 1) self._state['inputSw'] = general.readBit(c, 0) + def _readLockControl(self): + indexoffset = (self._channel - 1) * 5 + + state = self._adw.GetData_Long(settings.DATA_LOCK, 1 + indexoffset, 1)[0] + if state in range(8): + self._autolock['state'] = (state & 0x3) + self._autolock['relock'] = general.readBit(state, 2) + def _sendFilterControl(self): # read current state - c = self._adw.Get_Par(10 + self._channel) + c = self._adw.Get_Par(settings.PAR_FCR + self._channel) # set control bits c = general.changeBit(c, 9, self._state['auxSw']) for i in list(range(5)): c = general.changeBit(c, 4 + i, self._state['filtersEnabled'][i]) - c = general.changeBit(c, 3, self._state['snapSw']) + # c = general.changeBit(c, 3, self._state['snapSw']) c = general.changeBit(c, 2, self._state['offsetSw']) c = general.changeBit(c, 1, self._state['outputSw']) c = general.changeBit(c, 0, self._state['inputSw']) - self._adw.Set_Par(10 + self._channel, c) + self._adw.Set_Par(settings.PAR_FCR + self._channel, c) ######################################## # Change servo state @@ -321,6 +337,8 @@ class Servo: Defaults to :obj:`True`. Possible not to enable the FIFO buffering for this servo. """ + if self._autolock['state']: + raise UserInputError('Autolock is active, ramp cannot be activated on this channel.') if frequency is None: stepsize = self._ramp['stepsize'] @@ -340,8 +358,8 @@ class Servo: control = stepsize * 0x100 control += self._channel - self._adw.Set_Par(3, control) - self._adw.Set_FPar(1, amplitude / 10) + self._adw.Set_Par(settings.PAR_RCR, control) + self._adw.Set_FPar(settings.FPAR_RAMPAMP, amplitude / 10) if enableFifo: factor = 1.2 @@ -359,7 +377,7 @@ class Servo: def disableRamp(self): """Stop the ramp.""" self._ramp['minimum'] = 0 - self._adw.Set_Par(3, 0) + self._adw.Set_Par(settings.PAR_RCR, 0) @property def filterStates(self): @@ -412,7 +430,7 @@ class Servo: @property def rampEnabled(self): - control = self._adw.Get_Par(3) + control = self._adw.Get_Par(settings.PAR_RCR) if control & 15 == self._channel: return True else: @@ -488,7 +506,7 @@ class Servo: Before using it ensure to block the beam. It takes the mean value of {} data points. After changing the input amplification it may be necessary to adjust the offset. - """.format(10*settings.FIFO_MAXLEN) + """.format(10 * settings.FIFO_MAXLEN) self.enableFifo(1) n = 10000 self._waitForBufferFilling(n) @@ -497,101 +515,231 @@ class Servo: self.offset = - df['input'].mean() @property - def snapSw(self): - """ - Enable or disable the automatic snapping. + def lockState(self): + """Return the lock state. - :getter: Return the state of the switch. - :setter: Enable or disable the output. - :type: :obj:`bool` - """ - self._readFilterControl() - return self._state['snapSw'] + '0': off + `1`: search + `2`: lock - @snapSw.setter - def snapSw(self, enabled): - if not isinstance(enabled, bool): - raise TypeError('the value must be a bool.') + Returns + ------- + :obj:`int` + The lock state. - self._state['snapSw'] = enabled - self._sendFilterControl() + """ + self._readLockControl() + return self._autolock['state'] @property - def snap(self): + def lockThreshold(self): + """Get or set the autolock threshold. + + :getter: Return the threshold. + :setter: Set the threshold. + :type: :obj:`float` """ - Set or read the snap limit. + return self._autolock['threshold'] + + @lockThreshold.setter + def lockThreshold(self, threshold): + try: + float(threshold) + except ValueError: + raise TypeError('threshold must be a float or int.') + index_offset = (self._channel - 1) * 5 # the lock state parameter is set on index 1, 6, 12 etc., as each servo channel occupies 5 indices (as of current version) + self._autolock['threshold'] = threshold + threshold = _convertVolt2Int(threshold, self.auxSensitivity, True) + threshold = general.changeBit(threshold, 16, self.lockGreater) + # Sending values to ADwin + self._adw.SetData_Long([threshold], settings.DATA_LOCK, 2 + index_offset, 1) - :getter: Return the current limit. - :setter: Set the limit to a value. + @property + def lockSearchMin(self): + """Get or set the autolock search range minimum. + + :getter: Return the threshold. + :setter: Set the threshold. :type: :obj:`float` """ - self._snapRead() - return self._snap['limit'] + return self._autolock['min'] - @snap.setter - def snap(self, value): + @lockSearchMin.setter + def lockSearchMin(self, value): try: float(value) except ValueError: raise TypeError('value must be a float or int.') + if not -10 <= value <= 10: + raise ValueError('Search minimum has to be between -10 and 10 volts.') + if value > self._autolock['max']: + raise ValueError('Please make sure the maximum is greater than the minimum or try setting the maximum first.') + index_offset = (self._channel - 1) * 5 # the lock state parameter is set on index 1, 6, 12 etc., as each servo channel occupies 5 indices (as of current version) + self._autolock['min'] = value + min = _convertVolt2Int(value, self.auxSensitivity, True) + # Sending values to ADwin + self._adw.SetData_Long([min], settings.DATA_LOCK, 3 + index_offset, 1) + + @property + def lockSearchMax(self): + """Get or set the autolock search range maximum. - self._snap['limit'] = value - self.snapSend() + :getter: Return the threshold. + :setter: Set the threshold. + :type: :obj:`float` + """ + return self._autolock['max'] + + @lockSearchMax.setter + def lockSearchMax(self, value): + try: + float(value) + except ValueError: + raise TypeError('value must be a float or int.') + if not -10 <= value <= 10: + raise ValueError('Search maximum has to be between -10 and 10 volts.') + if value < self._autolock['min']: + raise ValueError('Please make sure the maximum is greater than the minimum or try setting the minimum first.') + index_offset = (self._channel - 1) * 5 # the lock state parameter is set on index 1, 6, 12 etc., as each servo channel occupies 5 indices (as of current version) + self._autolock['max'] = value + max = _convertVolt2Int(value, self.auxSensitivity, True) + self._adw.SetData_Long([max], settings.DATA_LOCK, 4 + index_offset, 1) @property - def snapGreater(self): + def lockGreater(self): """ - Set the snap direction to either greater (True) or lesser (False) than the limit. + Set the lock direction to either greater (True) or lesser (False) than the threshold. :getter: Return the current value. :setter: Set the condition. :type: :obj:`bool` """ - self._snapRead() - return self._snap['greater'] + return self._autolock['greater'] - @snapGreater.setter - def snapGreater(self, value): - if not isinstance(value, bool): + @lockGreater.setter + def lockGreater(self, greater): + if not isinstance(greater, bool): raise TypeError('value must be a bool.') + index_offset = (self._channel - 1) * 5 # the lock state parameter is set on index 1, 6, 12 etc., as each servo channel occupies 5 indices (as of current version) + threshold = _convertVolt2Int(self.lockThreshold, self.auxSensitivity, True) + threshold = general.changeBit(threshold, 16, greater) + # Sending values to ADwin + self._adw.SetData_Long([threshold], settings.DATA_LOCK, 2 + index_offset, 1) + self._autolock['greater'] = greater - self._snap['greater'] = value - self.snapSend() - - def snapSend(self, limit=None, greater=None): + @property + def relock(self): """ - Value to enable locking. + Set the lock to trigger a relock automatically when falling below or above threshold (according to `greater` setting). - Parameters - ---------- - limit: :obj:`float` - Threshold limit to start locking. - greater: :obj:`bool` - Start locking when the aux value is lower or greater than :obj:`limit`. + :getter: Return the current value. + :setter: Set the condition. + :type: :obj:`bool` """ - if limit is None: - limit = self._snap['limit'] - if greater is None: - greater = self._snap['greater'] - - try: - float(limit) - except ValueError: - raise TypeError('limit must be a float or int.') - if not isinstance(greater, bool): - raise TypeError('greater must be a bool.') - - self._snap['limit'] = limit - self._snap['greater'] = greater - - limit = _convertVolt2Int(limit, self.auxSensitivity, True) - limit = general.changeBit(limit, 16, greater) - self._adw.SetData_Long([limit], 7, self._channel, 1) + return self._autolock['relock'] - def _snapRead(self): - snapping_config = self._adw.GetData_Long(7, self._channel, 1)[0] - self._snap['limit'] = _convertInt2Volt(snapping_config & 0xffff, mode=self.auxSensitivity) - self._snap['greater'] = general.readBit(snapping_config, 16) + @relock.setter + def relock(self, value): + if not isinstance(value, bool): + raise TypeError('value must be a bool.') + self._autolock['relock'] = value + + # @property + # def snapSw(self): + # """ + # Enable or disable the automatic snapping. + # + # :getter: Return the state of the switch. + # :setter: Enable or disable the output. + # :type: :obj:`bool` + # """ + # self._readFilterControl() + # return self._state['snapSw'] + # + # @snapSw.setter + # def snapSw(self, enabled): + # if not isinstance(enabled, bool): + # raise TypeError('the value must be a bool.') + # + # self._state['snapSw'] = enabled + # self._sendFilterControl() + # + # @property + # def snap(self): + # """ + # Set or read the snap limit. + # + # :getter: Return the current limit. + # :setter: Set the limit to a value. + # :type: :obj:`float` + # """ + # self._snapRead() + # return self._snap['limit'] + # + # @snap.setter + # def snap(self, value): + # try: + # float(value) + # except ValueError: + # raise TypeError('value must be a float or int.') + # + # self._snap['limit'] = value + # self.snapSend() + # + # @property + # def snapGreater(self): + # """ + # Set the snap direction to either greater (True) or lesser (False) than the limit. + # + # :getter: Return the current value. + # :setter: Set the condition. + # :type: :obj:`bool` + # """ + # self._snapRead() + # return self._snap['greater'] + # + # @snapGreater.setter + # def snapGreater(self, value): + # if not isinstance(value, bool): + # raise TypeError('value must be a bool.') + # + # self._snap['greater'] = value + # self.snapSend() + # + # def snapSend(self, limit=None, greater=None): + # """ + # Value to enable locking. + # + # Parameters + # ---------- + # limit: :obj:`float` + # Threshold limit to start locking. + # greater: :obj:`bool` + # Start locking when the aux value is lower or greater than :obj:`limit`. + # """ + # if limit is None: + # limit = self._snap['limit'] + # if greater is None: + # greater = self._snap['greater'] + # + # try: + # float(limit) + # except ValueError: + # raise TypeError('limit must be a float or int.') + # if not isinstance(greater, bool): + # raise TypeError('greater must be a bool.') + # + # self._snap['limit'] = limit + # self._snap['greater'] = greater + # + # limit = _convertVolt2Int(limit, self.auxSensitivity, True) + # limit = general.changeBit(limit, 16, greater) + # self._adw.SetData_Long([limit], settings.DATA_SNAP, self._channel, 1) + # + # def _snapRead(self): + # snapping_config = self._adw.GetData_Long(settings.DATA_SNAP, self._channel, 1)[0] + # self._snap['limit'] = _convertInt2Volt(snapping_config & 0xffff, mode=self.auxSensitivity) + # self._snap['greater'] = general.readBit(snapping_config, 16) @property def outputSw(self): @@ -647,7 +795,7 @@ class Servo: self._state['offset'] = offset index = self._channel + 8 offsetInt = _convertVolt2Int(offset, self.inputSensitivity) - self._adw.SetData_Double([offsetInt], 2, index, 1) + self._adw.SetData_Double([offsetInt], settings.DATA_OFFSETGAIN, index, 1) @property def gain(self): @@ -665,7 +813,7 @@ class Servo: self._state['gain'] = gain index = self._channel effectiveGain = 1.0 * self.gain / pow(2, self.inputSensitivity) - self._adw.SetData_Double([effectiveGain], 2, index, 1) + self._adw.SetData_Double([effectiveGain], settings.DATA_OFFSETGAIN, index, 1) @property def inputSensitivity(self): @@ -699,13 +847,13 @@ class Servo: self._state['inputSensitivity'] = mode - currentRegister = self._adw.Get_Par(8) + currentRegister = self._adw.Get_Par(settings.PAR_SENSITIVITY) register = general.clearBit(currentRegister, self._channel * 2 - 2) register = general.clearBit(register, self._channel * 2 - 1) register += mode << self._channel * 2 - 2 - self._adw.Set_Par(8, register) + self._adw.Set_Par(settings.PAR_SENSITIVITY, register) # Update gain to correct gain change from input sensitivity self.gain = self.gain @@ -743,13 +891,13 @@ class Servo: self._state['auxSensitivity'] = mode - currentRegister = self._adw.Get_Par(8) + currentRegister = self._adw.Get_Par(settings.PAR_SENSITIVITY) register = general.clearBit(currentRegister, self._channel * 2 + 14) register = general.clearBit(register, self._channel * 2 + 15) register += mode << self._channel * 2 + 14 - self._adw.Set_Par(8, register) + self._adw.Set_Par(settings.PAR_SENSITIVITY, register) @property def filters(self): @@ -788,7 +936,7 @@ class Servo: for i in f: data.append(i) - self._adw.SetData_Double(data, 1, startIndex, len(data)) + self._adw.SetData_Double(data, settings.DATA_FILTERCOEFFS, startIndex, len(data)) self._triggerReload() @@ -849,7 +997,7 @@ class Servo: @property def _fifoBufferSize(self): """Get the current size of the fifo buffer on ADwin.""" - return self._adw.Fifo_Full(3) + return self._adw.Fifo_Full(settings.DATA_FIFO) @property def fifoStepsize(self): @@ -887,7 +1035,7 @@ class Servo: @property def fifoEnabled(self): - if self._adw.Get_Par(4) == self._channel: + if self._adw.Get_Par(settings.PAR_ACTIVE_CHANNEL) == self._channel: return True else: return False @@ -913,8 +1061,8 @@ class Servo: else: self._fifo['stepsize'] = stepsize # Enable on adwin - self._adw.Set_Par(4, self._channel) - self._adw.Set_Par(6, stepsize) + self._adw.Set_Par(settings.PAR_ACTIVE_CHANNEL, self._channel) + self._adw.Set_Par(settings.PAR_FIFOSTEPSIZE, stepsize) # set refresh time self._calculateRefreshTime() # Create local buffer @@ -924,8 +1072,8 @@ class Servo: """Disable the FiFo output if it is enabled on this channel.""" if self.fifoEnabled: # Disable on adwin only if this channel is activated - self._adw.Set_Par(4, 0) - self._adw.Set_Par(6, 0) + self._adw.Set_Par(settings.PAR_ACTIVE_CHANNEL, 0) + self._adw.Set_Par(settings.PAR_FIFOSTEPSIZE, 0) # Destroy local buffer self._fifoBuffer = None @@ -943,7 +1091,7 @@ class Servo: # Saving 3 16bit channels in a 64bit long variable # Byte | 7 6 | 5 4 | 3 2 | 1 0 | # Channel | | input | aux | output | - combined = np.array(self._adw.GetFifo_Double(3, n)[:], dtype='int') + combined = np.array(self._adw.GetFifo_Double(settings.DATA_FIFO, n)[:], dtype='int') def extract_value(combined, offset=0): shifted = np.right_shift(combined, offset) @@ -1314,6 +1462,63 @@ class Servo: return data[self.__class__.__name__] + ######################################## + # Autolock + ######################################## + def autolock(self, lock, threshold=None, min=None, max=None, greater=None, relock=None): + if not isinstance(lock, int): + raise TypeError('lock has to be an integer.') + if lock not in range(2): + raise ValueError('The lock state is given using integers, where `0: autolock-off`, `1: start/search-peak`, `2: lock-mode`. The user input should either be `0` or `1`, as the rest ist determined by the locking algorithm.') + if threshold is None: + threshold = self._autolock['threshold'] + if min is None: + min = self.lockSearchMin + if max is None: + max = self.lockSearchMax + if greater is None: + greater = self.lockGreater + if relock is None: + relock = self.relock + if not isinstance(greater, bool): + raise TypeError('greater must be a bool.') + if not isinstance(relock, bool): + raise TypeError('greater must be a bool.') + try: + float(threshold) + float(min) + float(max) + except ValueError: + raise TypeError('parameters must be floats or ints.') + self._autolock['state'] = lock + + # disable ramp when locking (should be disabled by the GUI, this is mostly for use with a terminal) + if lock and self.rampEnabled: + self.disableRamp() + + # disabling input and output while searching + if lock: + self.outputSw = False + self.inputSw = False + else: + self.inputSw = True + self.outputSw = True + + index_offset = (self._channel - 1) * 5 # the lock state parameter is set on index 1, 6, 12 etc., as each servo channel occupies 5 indices (as of current version) + # the fifth array index is occupied by the a "lastFound" value, which can be use in case of a relock. it is set within the autolock, not as part of the python program + + # set lockmode to 0 while sending new parameters + self._adw.SetData_Long([0], settings.DATA_LOCK, 1 + index_offset, 1) + # send all values to ADwin + self.lockSearchMin = min + self.lockSearchMax = max + self.lockThreshold = threshold + self.lockGreater = greater + self.relock = relock + lock = general.changeBit(lock, 2, relock) # adding relock bit + # activating lock + self._adw.SetData_Long([lock], settings.DATA_LOCK, 1 + index_offset, 1) # setting the lock state last + class FeedbackController(Thread): def __init__(self, servo, dT, mtd, voltage_limit, server, port, update_interval=1): @@ -1344,7 +1549,7 @@ class FeedbackController(Thread): return self._dT @dT.setter - def dT(self, value): + def dT(self, value): self._dT = value self._servo._tempFeedbackSettings['dT'] = value @@ -1353,7 +1558,7 @@ class FeedbackController(Thread): return self._mtd @mtd.setter - def mtd(self, value): + def mtd(self, value): value = tuple(value) self._mtd = value self._servo._tempFeedbackSettings['mtd'] = value @@ -1363,7 +1568,7 @@ class FeedbackController(Thread): return self._voltage_limit @voltage_limit.setter - def voltage_limit(self, value): + def voltage_limit(self, value): self._voltage_limit = value self._servo._tempFeedbackSettings['voltage_limit'] = value @@ -1372,7 +1577,7 @@ class FeedbackController(Thread): return self._update_interval @update_interval.setter - def update_interval(self, value): + def update_interval(self, value): self._update_interval = value self._servo._tempFeedbackSettings['update_interval'] = value diff --git a/src/nqontrol/servoDevice.py b/src/nqontrol/servoDevice.py index f6d78ef32d17d1eaaf833efdd86d9da6aeb17bd9..0b308f9ebedc7661a3efc32384e936b05a699695 100644 --- a/src/nqontrol/servoDevice.py +++ b/src/nqontrol/servoDevice.py @@ -8,7 +8,6 @@ from nqontrol.servo import Servo import json import logging as log from OpenQlab.analysis.servo_design import ServoDesign -from OpenQlab import io import jsonpickle from nqontrol import settings from shutil import copyfile @@ -38,8 +37,6 @@ class ServoDevice: DONT_SERIALIZE = ['adw', '_servos', 'deviceNumber'] DEFAULT_PROCESS = "nqontrol.TC1" JSONPICKLE = ['_servoDesign'] - NUMBER_OF_SERVOS = 8 - NUMBER_OF_MONITORS = 8 # Path of the compiled binary. It comes usually with the source code, but can be exchanged. def __init__(self, deviceNumber=None, @@ -54,10 +51,10 @@ class ServoDevice: else: self.adw = ADwin(deviceNumber, raiseExceptions) self._servoDesign = ServoDesign() # The dummy servo design object - self._servos = [None] * self.NUMBER_OF_SERVOS + self._servos = [None] * settings.NUMBER_OF_SERVOS self.deviceNumber = deviceNumber - self._monitors = [None] * self.NUMBER_OF_MONITORS + self._monitors = [None] * settings.NUMBER_OF_MONITORS try: self._bootAdwin(process, reboot=reboot) @@ -70,7 +67,7 @@ class ServoDevice: # Adding servos names = settings.SERVO_NAMES - for i in range(1, self.NUMBER_OF_SERVOS + 1): + for i in range(1, settings.NUMBER_OF_SERVOS + 1): # Check if a servo name is set in SETTINGS if (deviceNumber in names) and (i in names[deviceNumber]): self.addServo(channel=i, name=names[deviceNumber][i]) @@ -130,7 +127,7 @@ class ServoDevice: :getter: Runtime in seconds. :type: :obj:`int` """ - return int(self.adw.Get_Par(1)) + return int(self.adw.Get_Par(settings.PAR_TIMER)) @property def rampEnabled(self): @@ -140,12 +137,12 @@ class ServoDevice: :getter: :obj:`False` if disabled, channel number if enabled. :type: :obj:`False` or :obj:`int` """ - control = self.adw.Get_Par(3) + control = self.adw.Get_Par(settings.PAR_RCR) channel = control & 15 if channel > 0: return channel else: - return False + return 0 # 0 is now the false channel def _bootAdwin(self, process=DEFAULT_PROCESS, @@ -208,7 +205,7 @@ class ServoDevice: ------- :obj:`Servo` Servo object. - """.format(self.NUMBER_OF_SERVOS) + """.format(settings.NUMBER_OF_SERVOS) return self._servos[channel - 1] def addServo(self, channel, applySettings=None, name=None): @@ -221,9 +218,9 @@ class ServoDevice: Physical channel from 1 to {}. applySettings: :obj:`str` or :obj:`dict` You can directly apply settings from a json file or a dict. - """.format(self.NUMBER_OF_SERVOS) - if channel > self.NUMBER_OF_SERVOS or channel < 1: - raise IndexError("Choose a channel from 1 to {}!".format(self.NUMBER_OF_SERVOS)) + """.format(settings.NUMBER_OF_SERVOS) + if channel > settings.NUMBER_OF_SERVOS or channel < 1: + raise IndexError("Choose a channel from 1 to {}!".format(settings.NUMBER_OF_SERVOS)) if self._servos[channel - 1] is None: self._servos[channel - 1] = Servo(channel, self.adw, applySettings=applySettings, name=name) else: @@ -237,7 +234,7 @@ class ServoDevice: ---------- channel: :obj:`int` Number of the physical channel to remove from 1 to {}. - """.format(self.NUMBER_OF_SERVOS) + """.format(settings.NUMBER_OF_SERVOS) self._servos[channel - 1] = None def disableMonitor(self, monitor_channel): @@ -250,7 +247,7 @@ class ServoDevice: Channel to disable the output. """ self.monitors[monitor_channel - 1] = None - self.adw.SetData_Long([0], 6, monitor_channel, 1) + self.adw.SetData_Long([0], settings.DATA_MONITORS, monitor_channel, 1) def enableMonitor(self, monitor_channel, servo, card): """ @@ -281,7 +278,7 @@ class ServoDevice: raise ValueError('You should choose one of the possible cards for the monitor output.') self.monitors[monitor_channel - 1] = dict({'servo': servo, 'card': card}) - self.adw.SetData_Long([monitor], 6, monitor_channel, 1) + self.adw.SetData_Long([monitor], settings.DATA_MONITORS, monitor_channel, 1) def _backupSettingsFile(self, filename): if os.path.isfile(filename): @@ -345,7 +342,7 @@ class ServoDevice: Physical channel from 1 to {}. applySettings: :obj:`str` or :obj:`dict` Settings to apply to the selected :obj:`Servo`. - """.format(self.NUMBER_OF_SERVOS) + """.format(settings.NUMBER_OF_SERVOS) self.servo(channel).loadSettings(applySettings) def loadDeviceFromJson(self, filename=settings.SETTINGS_FILE): @@ -372,7 +369,8 @@ class ServoDevice: servos = data[self.__class__.__name__]['_servos'] for s in servos: channel = servos[s]['_channel'] - self.loadServoFromJson(channel, servos[s]) + if channel <= settings.NUMBER_OF_SERVOS: + self.loadServoFromJson(channel, servos[s]) # Loading device parameters self._applySettingsDict(data) diff --git a/src/nqontrol/settings.py b/src/nqontrol/settings.py index 013a2c1cc8d7f540a7e325a1e96e8a5d73a93522..58665afecba78a641116b4db176ab1567aebe0e7 100644 --- a/src/nqontrol/settings.py +++ b/src/nqontrol/settings.py @@ -44,6 +44,8 @@ LOG_FORMAT = user_config.get('LOG_FORMAT', '%(levelname)s: %(module)s: %(message DEBUG = user_config.get('DEBUG', False) SERVO_NAMES = user_config.get('SERVO_NAMES', {}) # currently only for one device +NUMBER_OF_SERVOS = user_config.get('NUMBER_OF_SERVOS', 8) +NUMBER_OF_MONITORS = user_config.get('NUMBER_OF_MONITORS', 8) # Temperature feedback DEFAULT_TEMP_HOST = user_config.get('DEFAULT_TEMP_HOST', '127.0.0.1') @@ -55,6 +57,28 @@ RAMP_DATA_POINTS = 0x20000 FIFO_BUFFER_SIZE = 30003 # Buffer size that is choosen on the adwin system. FIFO_MAXLEN = 1000 +########################################################################################### +# ADwin parameter index assignments (don't change, these are mostly for better readability) +########################################################################################### +PAR_TIMER = 1 +PAR_RELOADBIT = 2 +PAR_RCR = 3 # Ramp Control Register +PAR_ACTIVE_CHANNEL = 4 +PAR_FIFOSTEPSIZE = 6 +PAR_TIMEDIFF = 7 +PAR_SENSITIVITY = 8 +PAR_FCR = 10 # Filter Control Register, used as `10 + servo_channel` + +FPAR_RAMPAMP = 1 # Ramp amplitude + +DATA_FILTERCOEFFS = 1 +DATA_OFFSETGAIN = 2 +DATA_FIFO = 3 +DATA_MONITORS = 6 +DATA_SNAP = 7 +DATA_LOCK = 8 + + ########################################################## ################# local settings import ################## ########################################################## diff --git a/src/nqontrol/settings_local.sample.py b/src/nqontrol/settings_local.sample.py index 6388e37f4039c2d69722ed05f795fa20eea61047..e9c95ff2b1ae1511511358dae29d6331a1d5117d 100644 --- a/src/nqontrol/settings_local.sample.py +++ b/src/nqontrol/settings_local.sample.py @@ -3,7 +3,7 @@ #################### Local configuration #################### # Use 0 to enable a dummy device. Otherwise use the channel number -# DEVICES_LIST = [0] # The 0 is reserved to mock device for testing! +DEVICES_LIST = [0] # The 0 is reserved to mock device for testing! # DEVICES_LIST = [1] # SETTINGS_FILE = 'test.json' @@ -30,4 +30,8 @@ # When initialized, the ServoDevice will check whether names are available for its deviceNumber. # DEBUG = True + +# NUMBER_OF_SERVOS = 8 # only for one device currently +# NUMBER_OF_MONITORS = 8 # same + # LOG_LEVEL = 'INFO' diff --git a/src/tests/test_servo.py b/src/tests/test_servo.py index ef11977ab1cbcf2b9170635ce7d956f5bfcbff89..f1fbda9fdd9b552f603f3f6310a8b589f2e16211 100644 --- a/src/tests/test_servo.py +++ b/src/tests/test_servo.py @@ -66,7 +66,7 @@ class TestVoltConvertion(unittest.TestCase): mode = 0 self.assertAlmostEqual(servo._convertVolt2Int(10, mode), 0x7fff) self.assertAlmostEqual(servo._convertVolt2Int(10, mode, True), 0xffff) - self.assertAlmostEqual(servo._convertVolt2Int(3.7, mode), round(0x8000*.37), 0) + self.assertAlmostEqual(servo._convertVolt2Int(3.7, mode), round(0x8000 * .37), 0) self.assertAlmostEqual(servo._convertVolt2Int(0, mode), 0) self.assertAlmostEqual(servo._convertVolt2Int(0, mode, True), 0x8000) self.assertAlmostEqual(servo._convertVolt2Int(-10, mode), -0x8000) @@ -77,7 +77,7 @@ class TestVoltConvertion(unittest.TestCase): mode = 3 self.assertAlmostEqual(servo._convertVolt2Int(1.25, mode), 0x7fff) - self.assertAlmostEqual(servo._convertVolt2Int(0.4625, mode), round(0x8000*.37), 0) + self.assertAlmostEqual(servo._convertVolt2Int(0.4625, mode), round(0x8000 * .37), 0) self.assertAlmostEqual(servo._convertVolt2Int(0, mode), 0) self.assertAlmostEqual(servo._convertVolt2Int(-1.25, mode), -0x8000) # out of range @@ -105,16 +105,19 @@ class TestVoltConvertion(unittest.TestCase): class TestServo(unittest.TestCase): def setUp(self): + settings.NUMBER_OF_SERVOS = 8 # ensure this is 8 for all the servo tests, since sometimes numbers are explicitly set to 7, 8 etc.. + settings.NUMBER_OF_MONITORS = 8 # same thing self.sd = ServoDevice(settings.DEVICES_LIST[0], reboot=True) - # self.sd.reboot() - self.s = self.sd.servo(2) + self.sd.reboot() + self.testchannel = 2 + self.s = self.sd.servo(self.testchannel) import nqontrol log.warning('nqontrol path: {}'.format(nqontrol.__path__[0])) if 'site-packages' in nqontrol.__path__[0]: raise Exception('Not running the development code!!!') def test_checkNumberAndChannel(self): - self.assertEqual(self.s._channel, 2) + self.assertEqual(self.s._channel, self.testchannel) def test_filters(self): # default filter state @@ -125,7 +128,7 @@ class TestServo(unittest.TestCase): [1, 0, 0, 0, 0], [1, 0, 0, 0, 0] ] - filtersEnabled = [False]*5 + filtersEnabled = [False] * 5 self.assertEqual(self.s._state['filters'], filters) self.assertEqual(self.s._state['filtersEnabled'], filtersEnabled) @@ -143,7 +146,7 @@ class TestServo(unittest.TestCase): self.s._filters = [[0.0] * 5] * 5 self.assertEqual(self.s.filters, filters) self.assertEqual(self.s._state['filters'], filters) - for i in range(3, 9): + for i in range(3, settings.NUMBER_OF_SERVOS): self.assertEqual(self.sd.servo(i).filters, self.s.DEFAULT_FILTERS) def test_filterControlRegister(self): @@ -187,8 +190,8 @@ class TestServo(unittest.TestCase): # enable FIFO and test correct Parameters stepsize = 100 self.s.enableFifo(stepsize) - self.assertEqual(2, self.s._adw.Get_Par(4)) # Test channel - self.assertEqual(stepsize, self.s._adw.Get_Par(6)) # Test stepsize + self.assertEqual(2, self.s._adw.Get_Par(settings.PAR_ACTIVE_CHANNEL)) # Test channel + self.assertEqual(stepsize, self.s._adw.Get_Par(settings.PAR_FIFOSTEPSIZE)) # Test stepsize self.s.disableFifo() self.assertFalse(self.s.fifoEnabled) self.s.enableFifo(1) @@ -198,30 +201,30 @@ class TestServo(unittest.TestCase): def test_monitor(self): # assign different channels - self.sd.enableMonitor(8, 2, card='input') - self.sd.enableMonitor(5, 2, card='aux') - self.sd.enableMonitor(1, 2, card='output') - self.sd.enableMonitor(2, 2, card='input') - self.sd.enableMonitor(3, 2, card='ttl') - self.assertEqual(self.s._adw.GetData_Long(6, 8, 1)[0], 2) - self.assertEqual(self.s._adw.GetData_Long(6, 5, 1)[0], 10) - self.assertEqual(self.s._adw.GetData_Long(6, 1, 1)[0], 22) - self.assertEqual(self.s._adw.GetData_Long(6, 2, 1)[0], 2) - self.assertEqual(self.s._adw.GetData_Long(6, 3, 1)[0], 30) + self.sd.enableMonitor(8, self.testchannel, card='input') + self.sd.enableMonitor(5, self.testchannel, card='aux') + self.sd.enableMonitor(1, self.testchannel, card='output') + self.sd.enableMonitor(2, self.testchannel, card='input') + self.sd.enableMonitor(3, self.testchannel, card='ttl') + self.assertEqual(self.s._adw.GetData_Long(settings.DATA_MONITORS, 8, 1)[0], 2) + self.assertEqual(self.s._adw.GetData_Long(settings.DATA_MONITORS, 5, 1)[0], 10) + self.assertEqual(self.s._adw.GetData_Long(settings.DATA_MONITORS, 1, 1)[0], 22) + self.assertEqual(self.s._adw.GetData_Long(settings.DATA_MONITORS, 2, 1)[0], 2) + self.assertEqual(self.s._adw.GetData_Long(settings.DATA_MONITORS, 3, 1)[0], 30) # disable monitor self.sd.disableMonitor(1) self.sd.disableMonitor(8) - self.assertEqual(self.s._adw.GetData_Long(6, 1, 1)[0], 0) - self.assertEqual(self.s._adw.GetData_Long(6, 8, 1)[0], 0) + self.assertEqual(self.s._adw.GetData_Long(settings.DATA_MONITORS, 1, 1)[0], 0) + self.assertEqual(self.s._adw.GetData_Long(settings.DATA_MONITORS, 8, 1)[0], 0) # try wrong channels with self.assertRaises(IndexError): - self.sd.enableMonitor(0, 2, card='input') + self.sd.enableMonitor(0, self.testchannel, card='input') with self.assertRaises(IndexError): - self.sd.enableMonitor(9, 2, card='input') + self.sd.enableMonitor(9, self.testchannel, card='input') with self.assertRaises(IndexError): - self.sd.enableMonitor(-1, 2, card='input') + self.sd.enableMonitor(-1, self.testchannel, card='input') def test_saveLoadJson(self): # TODO better test @@ -249,14 +252,14 @@ class TestServo(unittest.TestCase): def test_offsetGain(self): self.s.offset = 9.4 self.s.gain = 1.5 - self.assertEqual(self.s._adw.GetData_Double(2, 2, 1)[:], [1.5]) - self.assertEqual(self.s._adw.GetData_Double(2, 2+8, 1)[0], 30802) + self.assertEqual(self.s._adw.GetData_Double(settings.DATA_OFFSETGAIN, self.testchannel, 1)[:], [1.5]) + self.assertEqual(self.s._adw.GetData_Double(settings.DATA_OFFSETGAIN, self.testchannel + 8, 1)[0], 30802) self.s.gain = 1.0 self.s.inputSensitivity = 3 - self.assertEqual(self.s._adw.GetData_Double(2, 2, 1)[:], [0.125]) + self.assertEqual(self.s._adw.GetData_Double(settings.DATA_OFFSETGAIN, self.testchannel, 1)[:], [0.125]) self.s.offset = 1 - self.assertEqual(self.s._adw.GetData_Double(2, 2+8, 1)[:], [26214]) + self.assertEqual(self.s._adw.GetData_Double(settings.DATA_OFFSETGAIN, self.testchannel + 8, 1)[:], [26214]) def test_offsetLimits(self): for mode in range(4): @@ -280,8 +283,8 @@ class TestServo(unittest.TestCase): self.s.enableRamp(5, 5) self.assertEqual(self.s._ramp['amplitude'], 5) - self.assertEqual(self.s._adw.Get_Par(3), 1282) - self.assertEqual(self.s._adw.Get_FPar(1), 0.5) + self.assertEqual(self.s._adw.Get_Par(settings.PAR_RCR), 1282) + self.assertEqual(self.s._adw.Get_FPar(settings.FPAR_RAMPAMP), 0.5) self.s.realtimePlot() settings.SAMPLING_RATE = 200e3 sleep(.1) @@ -291,8 +294,8 @@ class TestServo(unittest.TestCase): self.s.enableRamp(255, 10) self.assertEqual(self.s._ramp['amplitude'], 10) self.assertTrue(self.s.rampEnabled) - self.assertEqual(self.s._adw.Get_Par(3), 65282) - self.assertEqual(self.s._adw.Get_FPar(1), 1.0) + self.assertEqual(self.s._adw.Get_Par(settings.PAR_RCR), 65282) + self.assertEqual(self.s._adw.Get_FPar(settings.FPAR_RAMPAMP), 1.0) settings.SAMPLING_RATE = 200e3 self.s.realtimePlot() sleep(.1) @@ -314,16 +317,16 @@ class TestServo(unittest.TestCase): self.s.enableRamp() def test_inputGain(self): - self.assertEqual(self.s._adw.Get_Par(8), 0) + self.assertEqual(self.s._adw.Get_Par(settings.PAR_SENSITIVITY), 0) self.s.inputSensitivity = 2 self.sd.servo(8).inputSensitivity = 3 - self.assertEqual(self.s._adw.Get_Par(8), 49160) + self.assertEqual(self.s._adw.Get_Par(settings.PAR_SENSITIVITY), 49160) self.s.auxSensitivity = 3 - self.assertEqual(self.s._adw.Get_Par(8), 835592) + self.assertEqual(self.s._adw.Get_Par(settings.PAR_SENSITIVITY), 835592) self.sd.servo(7).auxSensitivity = 2 - self.assertEqual(self.s._adw.Get_Par(8), 537706504) + self.assertEqual(self.s._adw.Get_Par(settings.PAR_SENSITIVITY), 537706504) def test_runtime(self): self.s.enableFifo(1) @@ -332,10 +335,10 @@ class TestServo(unittest.TestCase): self.sd.enableMonitor(i, i, card='input') self.sd.servo(i).offset = 3 self.sd.servo(i).auxSw = True - self.sd.servo(i).filters = [[7]*5]*5 + self.sd.servo(i).filters = [[7] * 5] * 5 sleep(.1) - self.assertLess(self.s._adw.Get_Par(7), 4500) - self.assertGreater(self.s._adw.Get_Par(7), 2500) + self.assertLess(self.s._adw.Get_Par(settings.PAR_TIMEDIFF), 4500) + self.assertGreater(self.s._adw.Get_Par(settings.PAR_TIMEDIFF), 2500) def test_sampling_rate(self): CPU_CLK = 1e9 @@ -519,11 +522,11 @@ class TestServo(unittest.TestCase): def test_send_state_after_reboot(self): sleep(.1) time = self.sd.timestamp - self.assertEqual(self.s._adw.GetData_Double(2, 2, 1)[:], [1.0]) + self.assertEqual(self.s._adw.GetData_Double(settings.DATA_OFFSETGAIN, self.testchannel, 1)[:], [1.0]) self.s.gain = 1.5 - self.assertEqual(self.s._adw.GetData_Double(2, 2, 1)[:], [1.5]) + self.assertEqual(self.s._adw.GetData_Double(settings.DATA_OFFSETGAIN, self.testchannel, 1)[:], [1.5]) self.sd.reboot() - self.assertEqual(self.s._adw.GetData_Double(2, 2, 1)[:], [1.5]) + self.assertEqual(self.s._adw.GetData_Double(settings.DATA_OFFSETGAIN, self.testchannel, 1)[:], [1.5]) self.assertLessEqual(self.sd.timestamp, time) def test_saving_and_loading_with_plant_and_temp_feedback(self): @@ -547,55 +550,15 @@ class TestServo(unittest.TestCase): self.assertEqual(self.s._tempFeedbackSettings['voltage_limit'], 3.6) self.assertEqual(self.s._tempFeedbackSettings['update_interval'], 1.3) - def test_snapping_disables_output(self): - def out(): - return self.s._readoutNewData(10000)['output'].iloc[-1] - - self.s.enableFifo() - self.s.offset = 9 - self.s.offsetSw = True - self.s.inputSw = True - self.s.outputSw = True - sleep(.01) - self.assertGreater(out(), 2) - self.s.snapSend(5, True) - self.s.snapSw = True - sleep(.01) - self.assertEqual(out(), 0) - - def test_enable_snapping(self): - self.assertEqual(self.s._adw.Get_Par(12), 0) - self.s.snapSend(-5, False) - self.s.snapSw = True - self.assertEqual(self.s._adw.Get_Par(12), 8) - # Check if it snaps - self.s.snapSend(5, False) - sleep(1e-3) - self.assertEqual(self.s._adw.Get_Par(12), 2) - # Snap when signal is higher - self.s.snapSend(-5, True) - self.s.snapSw = True - sleep(1e-3) - self.assertEqual(self.s._adw.Get_Par(12), 2) - - def test_stop_ramp_when_snapping(self): - self.s.enableRamp(50, 10, False) - self.s.snapSend(5, False) - self.s.snapSw = True - # Check if it snaps - sleep(1e-3) - # Ramp stopped? - self.assertFalse(self.s.rampEnabled) - def test_apply_old_settings(self): self.s.outputSw = True dict = self.s.getSettingsDict() - del dict['_state']['snapSw'] - self.s.snapSw = True + # del dict['_state']['snapSw'] + # self.s.snapSw = True self.s.outputSw = False self.assertFalse(self.s.outputSw) self.s.loadSettings(dict) - self.assertTrue(self.s.snapSw) + # self.assertTrue(self.s.snapSw) self.assertTrue(self.s.outputSw) def test_offset_autoset(self): @@ -622,18 +585,18 @@ class TestServo(unittest.TestCase): self.s._waitForBufferFilling() mean_corrected = self.s._readoutNewData(10000)['output'].mean() self.assertNotEqual(mean_corrected, 0) - self.assertAlmostEqual(mean_corrected, 0, places=2) + self.assertAlmostEqual(mean_corrected, 0, places=1) def test_check_control_switch_updates(self): self.assertFalse(self.s.inputSw) self.assertFalse(self.s.outputSw) self.assertFalse(self.s.offsetSw) - self.assertFalse(self.s.snapSw) + # self.assertFalse(self.s.snapSw) self.assertFalse(self.s.auxSw) self.s.inputSw = True self.s.outputSw = True self.s.offsetSw = True - self.s.snapSw = True + # self.s.snapSw = True self.s.auxSw = True self.s._state['inputSw'] = False self.assertTrue(self.s.inputSw) @@ -641,33 +604,151 @@ class TestServo(unittest.TestCase): self.assertTrue(self.s.outputSw) self.s._state['offsetSw'] = False self.assertTrue(self.s.offsetSw) - self.s._state['snapSw'] = False - self.assertTrue(self.s.snapSw) + # self.s._state['snapSw'] = False + # self.assertTrue(self.s.snapSw) self.s._state['auxSw'] = False self.assertTrue(self.s.auxSw) - def test_snapping_getters_and_setters(self): + def test_lock_getters_and_setters(self): + with self.assertRaises(TypeError): + self.s.lockThreshold = 'wrong type' + with self.assertRaises(TypeError): + self.s.lockSearchMin = 'wrong type' + with self.assertRaises(TypeError): + self.s.lockSearchMax = 'wrong type' + with self.assertRaises(ValueError): + self.s.lockSearchMin = -12 + with self.assertRaises(ValueError): + self.s.lockSearchMax = 13 + self.s.lockSearchMax = 3 + with self.assertRaises(ValueError): + self.s.lockSearchMin = 5 + self.s.lockSearchMin = 2 + with self.assertRaises(ValueError): + self.s.lockSearchMax = 0 + with self.assertRaises(TypeError): + self.s.lockGreater = 345 + with self.assertRaises(TypeError): + self.s.lockGreater = 'hello' + with self.assertRaises(TypeError): + self.s.relock = 345 + with self.assertRaises(TypeError): + self.s.relock = 'hello' + + def test_servo_autolock(self): + with self.assertRaises(TypeError): + self.s.autolock('tach') + with self.assertRaises(ValueError): + self.s.autolock(5) with self.assertRaises(TypeError): - self.s.snapSw = 'wrong type' + self.s.autolock(1, greater='bla') with self.assertRaises(TypeError): - self.s.snapSw = 345 + self.s.autolock(0, threshold='bla') with self.assertRaises(TypeError): - self.s.snapGreater = 345 + self.s.autolock(0, min='bla') with self.assertRaises(TypeError): - self.s.snapGreater = 'bla' - - # Test it reads the real values from adwin - self.s.snap = 4.7 - self.s._snap['limit'] = 'something different' - self.assertAlmostEqual(self.s.snap, 4.7, places=4) - # Different sensitivity - self.auxSensitivity = 3 - self.s.snap = .7 - self.s._snap['limit'] = 'something different' - self.assertAlmostEqual(self.s.snap, .7, places=3) - self.s.snapGreater = False - self.s._snap['greater'] = 'something different' - self.assertEqual(self.s.snapGreater, False) + self.s.autolock(0, max='bla') + + indexoffset = (self.s._channel - 1) * 5 + print(indexoffset) + self.s.autolock(1, 9, 0, 5, True, True) # setting threshold to 9 should make sure it never runs out of lock state 1 + threshold = servo._convertVolt2Int(9, self.s.auxSensitivity, True) + threshold = general.changeBit(threshold, 16, True) + min = servo._convertVolt2Int(0, self.s.auxSensitivity, True) + max = servo._convertVolt2Int(5, self.s.auxSensitivity, True) + lock = general.changeBit(1, 2, True) + # testing the lock parameters + sleep(.1) + self.assertEqual(self.s._adw.GetData_Long(settings.DATA_LOCK, 1 + indexoffset, 1)[:], [lock]) # will this work? + self.assertEqual(self.s._adw.GetData_Long(settings.DATA_LOCK, 2 + indexoffset, 3)[:], [threshold, min, max]) + + def test_lock_search_state(self): + # set autolock to value that cant be reached, so lock continues looping + + # lock greater + self.s.autolock(1, 8, -1, 1, True, False) + # switches need to be off for output and Input + sleep(.1) + self.assertFalse(self.s.inputSw) + self.assertFalse(self.s.outputSw) + # check lock state, should be `1` + indexoffset = (self.s._channel - 1) * 5 + lock = self.s._adw.GetData_Long(settings.DATA_LOCK, 1 + indexoffset, 1)[0] + lock = (lock & 0x3) + self.assertEqual(lock, 1) + + # lock smaller + self.s.autolock(1, -9, -1, 1, False, True) + sleep(.1) + self.assertFalse(self.s.inputSw) + self.assertFalse(self.s.outputSw) + # check lock state, should be `1` + lock = self.s._adw.GetData_Long(settings.DATA_LOCK, 1 + indexoffset, 1)[0] + lock = (lock & 0x3) + self.assertEqual(lock, 1) + + def test_lock_found_state(self): + # set autolock to value that will always be reached, so lock gets activated + + # lock greater + self.s.autolock(1, -9, -1, 1, True, False) + sleep(.4) + # state should be `2` + # input/output should be enabled + self.assertTrue(self.s.inputSw) + self.assertTrue(self.s.outputSw) + indexoffset = (self.s._channel - 1) * 5 + lock = self.s._adw.GetData_Long(settings.DATA_LOCK, 1 + indexoffset, 1)[0] + lock = (lock & 0x3) + self.assertEqual(lock, 2) + + # lock smaller + self.s.autolock(1, 9, -1, 1, False, False) + sleep(.3) + self.assertTrue(self.s.inputSw) + self.assertTrue(self.s.outputSw) + # check lock state, should be `2` + lock = self.s._adw.GetData_Long(settings.DATA_LOCK, 1 + indexoffset, 1)[0] + lock = (lock & 0x3) + self.assertEqual(lock, 2) + + def test_lock_off_state(self): + self.s.inputSw = True + self.s.outputSw = True + self.s.autolock(0) + sleep(.1) + self.assertTrue(self.s.inputSw) + self.assertTrue(self.s.outputSw) + # check lock state + indexoffset = (self.s._channel - 1) * 5 + lock = self.s._adw.GetData_Long(settings.DATA_LOCK, 1 + indexoffset, 1)[0] + lock = (lock & 0x3) + self.assertEqual(lock, 0) + + def test_lock_disables_ramp(self): + self.s.enableRamp() + self.s.autolock(1) + sleep(1e-3) + self.assertFalse(self.s.rampEnabled) + + def test_lock_disables_output_input(self): + def out(): + return self.s._readoutNewData(10000)['output'].iloc[-1] + + self.s.enableFifo() + self.assertEqual(out(), 0) + self.s.offset = 5 + self.s.offsetSw = True + self.s.inputSw = True + self.s.outputSw = True + sleep(.01) + self.assertGreater(out(), 2) + self.s.autolock(1, 9, 1, 2, True, False) + sleep(.01) + self.assertFalse(self.s.inputSw) + self.assertFalse(self.s.outputSw) + self.assertLessEqual(out(), 2) + self.assertGreaterEqual(out(), 1) class TestServoDevice(unittest.TestCase): @@ -750,6 +831,7 @@ class TestServoDevice(unittest.TestCase): def test_servo_names(self): settings.DEVICES_LIST = [0] + settings.NUMBER_OF_SERVOS = 3 settings.SERVO_NAMES[0] = { 1: 'Cavity', 2: 'Testing', @@ -808,6 +890,19 @@ class TestTemperatureFeedback(unittest.TestCase): self.assertIn('OK.', self.s._tempFeedback.last_answer) self.s.tempFeedbackStop() + def test_adding_plant_data_string(self): + tests_dir = os.path.dirname(os.path.abspath(__file__)) + testfile = '{}/fra.csv'.format(tests_dir) + compare = io.read(testfile) + + #file is encoded in 'cp1252' + with open(testfile, mode='rb') as file: + data_string = file.read().decode('cp1252') + + data = io.read(data_string) + + self.assertTrue(data.equals(compare)) + if __name__ == '__main__': # t = TestTemperatureFeedback()