├── .devcontainer ├── devcontainer.json └── noop.txt ├── .dockerignore ├── .gitattributes ├── .gitignore ├── .pylintrc ├── 3RDPARTYLICENSES ├── Dockerfile ├── LICENSE ├── MANIFEST.in ├── README.md ├── bin ├── docker-entrypoint-web └── pip3-install ├── doc ├── Installation - Phidget Driver.md ├── Manufacturer - Gilson.md ├── Manufacturer - Heidolph.md ├── Manufacturer - Kern.md ├── Manufacturer - Knauer.md ├── Manufacturer - Mettler Toledo.md ├── Manufacturer - Phidgets.md ├── Manufacturer - ThalesNano.md ├── Manufacturer - Vapourtec.md ├── Manufacturer - Vici.md ├── Manufacturer - WPI.md └── Octopus Documentation.md ├── docker-compose.yml ├── examples ├── sequence.py ├── test_abort.py ├── test_bind.py ├── test_call.py ├── test_dep.py ├── test_on.py ├── test_statemonitor.py ├── test_tick.py ├── test_while.py └── variables1.py ├── octopus-plugins.txt.example ├── octopus ├── blocks │ └── .keep ├── blocktopus │ ├── __init__.py │ ├── block_registry.py │ ├── blockly │ │ ├── blocks │ │ │ ├── colour.js │ │ │ ├── control.js │ │ │ ├── images.js │ │ │ ├── lexical-procedures.js │ │ │ ├── lexical-variables.js │ │ │ ├── lists.js │ │ │ ├── logic.js │ │ │ ├── loops.js │ │ │ ├── math.js │ │ │ ├── mixins.js │ │ │ ├── octopus-connections.js │ │ │ ├── octopus-machines.js │ │ │ ├── text.js │ │ │ └── timing.js │ │ ├── colourscheme.js │ │ ├── constants.js │ │ ├── core │ │ │ ├── block.js │ │ │ ├── block_svg.js │ │ │ ├── blockly.js │ │ │ ├── blocks.js │ │ │ ├── bubble.js │ │ │ ├── comment.js │ │ │ ├── connection.js │ │ │ ├── contextmenu.js │ │ │ ├── field.js │ │ │ ├── field_angle.js │ │ │ ├── field_checkbox.js │ │ │ ├── field_colour.js │ │ │ ├── field_dropdown.js │ │ │ ├── field_flydown.js │ │ │ ├── field_global_flydown.js │ │ │ ├── field_image.js │ │ │ ├── field_label.js │ │ │ ├── field_lexical_parameter_flydown.js │ │ │ ├── field_lexical_variable.js │ │ │ ├── field_machine_flydown.js │ │ │ ├── field_parameter_flydown.js │ │ │ ├── field_textinput.js │ │ │ ├── field_variable.js │ │ │ ├── flydown.js │ │ │ ├── flyout.js │ │ │ ├── generator.js │ │ │ ├── icon.js │ │ │ ├── inject.js │ │ │ ├── input.js │ │ │ ├── msg.js │ │ │ ├── mutator.js │ │ │ ├── names.js │ │ │ ├── procedures.js │ │ │ ├── scrollbar.js │ │ │ ├── toolbox.js │ │ │ ├── tooltip.js │ │ │ ├── trashcan.js │ │ │ ├── utils.js │ │ │ ├── validators.js │ │ │ ├── variables.js │ │ │ ├── warning.js │ │ │ ├── widgetdiv.js │ │ │ ├── workspace.js │ │ │ └── xml.js │ │ ├── generators │ │ │ ├── python-octo-blocks.js │ │ │ ├── python-octo-constants.js │ │ │ ├── python-octo-methods.js │ │ │ ├── python-octo.js │ │ │ └── python-octo │ │ │ │ ├── colour.js │ │ │ │ ├── connections.js │ │ │ │ ├── control.js │ │ │ │ ├── images.js │ │ │ │ ├── lists.js │ │ │ │ ├── logic.js │ │ │ │ ├── loops.js │ │ │ │ ├── machines.js │ │ │ │ ├── math.js │ │ │ │ ├── procedures.js │ │ │ │ ├── text.js │ │ │ │ └── variables.js │ │ ├── msg │ │ │ ├── json │ │ │ │ ├── ar.json │ │ │ │ ├── az.json │ │ │ │ ├── be-tarask.json │ │ │ │ ├── br.json │ │ │ │ ├── ca.json │ │ │ │ ├── cs.json │ │ │ │ ├── da.json │ │ │ │ ├── de.json │ │ │ │ ├── el.json │ │ │ │ ├── en.json │ │ │ │ ├── es.json │ │ │ │ ├── fa.json │ │ │ │ ├── fi.json │ │ │ │ ├── fr.json │ │ │ │ ├── he.json │ │ │ │ ├── hi.json │ │ │ │ ├── hrx.json │ │ │ │ ├── hu.json │ │ │ │ ├── ia.json │ │ │ │ ├── id.json │ │ │ │ ├── is.json │ │ │ │ ├── it.json │ │ │ │ ├── ja.json │ │ │ │ ├── ko.json │ │ │ │ ├── lb.json │ │ │ │ ├── lrc.json │ │ │ │ ├── ms.json │ │ │ │ ├── nb.json │ │ │ │ ├── nl.json │ │ │ │ ├── oc.json │ │ │ │ ├── pl.json │ │ │ │ ├── pms.json │ │ │ │ ├── pt-br.json │ │ │ │ ├── pt.json │ │ │ │ ├── qqq.json │ │ │ │ ├── ro.json │ │ │ │ ├── ru.json │ │ │ │ ├── sc.json │ │ │ │ ├── sq.json │ │ │ │ ├── sr.json │ │ │ │ ├── sv.json │ │ │ │ ├── synonyms.json │ │ │ │ ├── th.json │ │ │ │ ├── tl.json │ │ │ │ ├── tlh.json │ │ │ │ ├── tr.json │ │ │ │ ├── uk.json │ │ │ │ ├── vi.json │ │ │ │ ├── zh-hans.json │ │ │ │ └── zh-hant.json │ │ │ └── messages.js │ │ └── overrides.js │ ├── blocks │ │ ├── __init__.py │ │ ├── colour.py │ │ ├── controls.py │ │ ├── dependents.py │ │ ├── images.py │ │ ├── logic.py │ │ ├── machines.py │ │ ├── mathematics.py │ │ ├── test │ │ │ ├── __init__.py │ │ │ └── test_logic.py │ │ ├── text.py │ │ └── variables.py │ ├── database │ │ ├── createdb.py │ │ ├── dbutil.py │ │ ├── upgradedb-1.py │ │ ├── upgradedb-2.py │ │ └── upgradedb-3.py │ ├── experiment.py │ ├── plugins.py │ ├── resources │ │ ├── blockly │ │ │ ├── blockly-ext.css │ │ │ ├── blockly.css │ │ │ └── media │ │ │ │ ├── 1x1.gif │ │ │ │ ├── click.mp3 │ │ │ │ ├── click.ogg │ │ │ │ ├── click.wav │ │ │ │ ├── delete.mp3 │ │ │ │ ├── delete.ogg │ │ │ │ ├── delete.wav │ │ │ │ ├── handclosed.cur │ │ │ │ ├── handopen.cur │ │ │ │ └── sprites.png │ │ ├── colpick │ │ │ ├── colpick.css │ │ │ └── colpick.js │ │ ├── contextmenu │ │ │ ├── contextmenu.js │ │ │ ├── dropdown.css │ │ │ └── dropdown.less │ │ ├── download.js │ │ ├── experiment-graph.js │ │ ├── experiment-popup.css │ │ ├── experiment-popup.js │ │ ├── experiment-result.css │ │ ├── experiment-result.js │ │ ├── experiment-running.js │ │ ├── gridstack │ │ │ ├── gridstack.css │ │ │ └── gridstack.js │ │ ├── jquery-ui │ │ │ └── jquery-ui.min.js │ │ ├── prettify │ │ │ ├── prettify.css │ │ │ └── prettify.js │ │ ├── root.css │ │ ├── root.js │ │ ├── sketch-edit.css │ │ └── sketch-edit.js │ ├── server │ │ ├── __init__.py │ │ ├── __main__.py │ │ ├── protocol │ │ │ ├── __init__.py │ │ │ ├── block.py │ │ │ ├── experiment.py │ │ │ ├── runtime.py │ │ │ └── sketch.py │ │ ├── server.py │ │ ├── template.py │ │ ├── transport │ │ │ ├── __init__.py │ │ │ └── base.py │ │ └── websocket.py │ ├── sketch.py │ ├── templates │ │ ├── experiment-download.xml │ │ ├── experiment-result.xml │ │ ├── experiment-running.xml │ │ ├── root.xml │ │ ├── sketch-edit.xml │ │ └── template-resources.json │ └── workspace.py ├── console.py ├── constants.py ├── data │ ├── __init__.py │ ├── control.py │ ├── data.py │ ├── errors.py │ ├── manipulation.py │ └── test │ │ ├── __init__.py │ │ ├── test_data.py │ │ └── test_variables.py ├── events.py ├── image │ ├── __init__.py │ ├── blobmaker.py │ ├── data.py │ ├── functions.py │ ├── machine.py │ ├── provider.py │ ├── source.py │ └── tracker.py ├── machine │ ├── __init__.py │ ├── interface.py │ ├── machine.py │ └── test │ │ ├── __init__.py │ │ └── test_property.py ├── manufacturer │ ├── dummy.py │ ├── harvard.py │ ├── heidolph.py │ ├── kern.py │ ├── knauer.py │ ├── mt.py │ ├── omega.py │ ├── phidgets.py │ ├── startech.py │ ├── thalesnano.py │ └── vici.py ├── notifier │ ├── __init__.py │ └── sms.py ├── protocol │ ├── __init__.py │ ├── basic.py │ └── gsioc.py ├── queue.py ├── sequence │ ├── __init__.py │ ├── control.py │ ├── error.py │ ├── experiment.py │ ├── runtime.py │ ├── sequence.py │ ├── shortcuts.py │ ├── test │ │ ├── __init__.py │ │ ├── test_control.py │ │ └── test_sequence.py │ └── util.py ├── test │ ├── __init__.py │ └── test_sequence_if.py ├── transport │ ├── __init__.py │ ├── basic.py │ ├── gsioc.py │ └── phidgets.py └── util.py ├── package.json ├── requirements.txt ├── rollup.config.js ├── setup.py ├── tools ├── build.py ├── camera_server │ └── server.py ├── iCIR_server │ └── irserver.js └── knauer_mock │ └── knauer_mock.js └── yarn.lock /.devcontainer/devcontainer.json: -------------------------------------------------------------------------------- 1 | // For format details, see https://aka.ms/vscode-remote/devcontainer.json or the definition README at 2 | // https://github.com/microsoft/vscode-dev-containers/tree/master/containers/python-3 3 | { 4 | "name": "Octopus (Python 3)", 5 | "dockerComposeFile": "../docker-compose.yml", 6 | "service": "web", 7 | "workspaceFolder": "/app", 8 | "shutdownAction": "stopCompose", 9 | "extensions": [ 10 | "ms-python.python", 11 | "ms-python.vscode-pylance", 12 | ], 13 | "settings": { 14 | "python.pythonPath": "/usr/local/bin/python", 15 | "python.linting.pylintEnabled": true, 16 | "python.linting.pylintPath": "/usr/local/bin/pylint", 17 | "python.linting.enabled": true 18 | }, 19 | } 20 | -------------------------------------------------------------------------------- /.devcontainer/noop.txt: -------------------------------------------------------------------------------- 1 | This file is copied into the container along with requirements.txt* from the 2 | parent folder. This is done to prevent the Dockerfile COPY instruction from 3 | failing if no requirements.txt is found. -------------------------------------------------------------------------------- /.dockerignore: -------------------------------------------------------------------------------- 1 | .git/ 2 | .pytest_cache/ 3 | __pycache__/ 4 | node_modules/ 5 | 6 | .coverage 7 | .dockerignore 8 | .env* 9 | !.env.example 10 | docker-compose.override.yml 11 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | * text=auto 2 | *.sh text eol=lf 3 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.py[cod] 2 | 3 | # C extensions 4 | *.so 5 | 6 | # Packages 7 | *.egg 8 | *.egg-info 9 | dist 10 | build 11 | eggs 12 | parts 13 | var 14 | sdist 15 | develop-eggs 16 | .installed.cfg 17 | lib 18 | lib64 19 | __pycache__ 20 | 21 | # Installer logs 22 | pip-log.txt 23 | 24 | # Unit test / coverage reports 25 | .coverage 26 | .tox 27 | nosetests.xml 28 | 29 | # Translations 30 | *.mo 31 | 32 | # Mr Developer 33 | .mr.developer.cfg 34 | .project 35 | .pydevproject 36 | 37 | # Twisted trial 38 | _trial_temp/ 39 | 40 | # Twisted plugins 41 | twisted/plugins/dropin.cache 42 | 43 | # Octopus plugins 44 | plugins/* 45 | octopus-plugins.txt 46 | 47 | # Dependencies 48 | node_modules/* 49 | 50 | # npm 51 | package-lock.json 52 | 53 | # Build 54 | octopus/blocktopus/resources/blockly/pack/* 55 | octopus/blocktopus/resources/blockly/msg/js/*.js 56 | octopus/blocktopus/resources/cache 57 | 58 | # Stored data 59 | data/* 60 | 61 | # Twisted log / PID 62 | child.log 63 | twistd.log 64 | twistd.pid 65 | 66 | # Eclipse IDE 67 | .metadata/ 68 | 69 | .DS_Store 70 | -------------------------------------------------------------------------------- /.pylintrc: -------------------------------------------------------------------------------- 1 | [TYPECHECK] 2 | ignored-classes=twisted.internet.reactor -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:14.15.5-buster-slim AS webpack 2 | 3 | WORKDIR /app 4 | 5 | RUN apt-get update \ 6 | && apt-get install -y build-essential --no-install-recommends \ 7 | && rm -rf /var/lib/apt/lists/* /usr/share/doc /usr/share/man \ 8 | && apt-get clean \ 9 | && chown node:node -R /app 10 | 11 | USER node 12 | 13 | COPY --chown=node:node package.json *yarn* rollup.config.js ./ 14 | 15 | RUN yarn install 16 | 17 | ARG NODE_ENV="production" 18 | ENV NODE_ENV="${NODE_ENV}" \ 19 | USER="node" 20 | 21 | RUN mkdir -p octopus/blocktopus/resources/blockly/pack/ && mkdir -p octopus/blocktopus/blockly/ && chown node:node -R octopus 22 | COPY --chown=node:node octopus/blocktopus/blockly octopus/blocktopus/blockly 23 | 24 | RUN if [ "${NODE_ENV}" != "development" ]; then \ 25 | yarn run build; fi 26 | 27 | CMD ["bash"] 28 | 29 | # 30 | # App 31 | # 32 | 33 | FROM python:3.9.5-slim-buster AS app 34 | 35 | WORKDIR /app 36 | 37 | RUN apt-get update \ 38 | && apt-get install -y build-essential curl libpq-dev git --no-install-recommends \ 39 | && apt-get install -y libatlas-base-dev libffi-dev libglib2.0-0 libgl1 usbutils dos2unix --no-install-recommends \ 40 | && rm -rf /var/lib/apt/lists/* /usr/share/doc /usr/share/man \ 41 | && apt-get clean \ 42 | && useradd --create-home python \ 43 | && chown python:python -R /app 44 | 45 | USER python 46 | 47 | # Install requirements and plugins 48 | COPY --chown=python:python requirements*.txt octopus-plugins.txt ./ 49 | COPY --chown=python:python bin/ ./bin 50 | 51 | RUN chmod 0755 bin/* && dos2unix bin/pip3-install && bin/pip3-install 52 | 53 | RUN if [ -f octopus-plugins.txt ]; then pip install -r octopus-plugins.txt; fi 54 | 55 | # Set environment variables 56 | ARG OCTOPUS_ENV="production" 57 | ENV OCTOPUS_ENV="${OCTOPUS_ENV}" \ 58 | OCTOPUS_PLUGINS_DIR="/app/plugins" \ 59 | PYTHONUNBUFFERED="true" \ 60 | PYTHONPATH="." \ 61 | PATH="${PATH}:/home/python/.local/bin" \ 62 | USER="python" 63 | 64 | # Download the JS/CSS/etc resources if in prodution env 65 | RUN mkdir -p octopus/blocktopus/resources/cache/ && mkdir -p octopus/blocktopus/templates/ && chown python:python -R octopus 66 | RUN mkdir tools && chown python:python tools 67 | COPY --chown=python:python octopus/blocktopus/templates/template-resources.json octopus/blocktopus/templates/template-resources.json 68 | COPY --chown=python:python tools/build.py tools 69 | RUN if [ "$OCTOPUS_ENV" == "production" ]; then \ 70 | python tools/build.py ; fi 71 | 72 | # Copy packed javascript from the webpack worker 73 | COPY --chown=python:python --from=webpack /app/octopus/blocktopus/resources/blockly/pack /pack/ 74 | 75 | # Copy rest of source 76 | COPY --chown=python:python . ./ 77 | 78 | CMD ["python", "-m", "octopus.blocktopus.server"] 79 | 80 | 81 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2021 Richard Ingham 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of 6 | this software and associated documentation files (the "Software"), to deal in 7 | the Software without restriction, including without limitation the rights to 8 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 9 | the Software, and to permit persons to whom the Software is furnished to do so, 10 | subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 17 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 18 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 19 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 20 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include octopus/blocktopus/resources/* 2 | include octopus/blocktopus/templates/* 3 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | Octopus provides ability to create control programmes for laboratory automation 3 | using [Python 3.9](http://www.python.org) and [Twisted](http://www.twistedmatrix.com), 4 | and real-time remote monitoring over http/websockets for running protocols. 5 | 6 | It can provide a command-line interface for interacting with machines using 7 | virtual instrument interfaces, or for creating scripted protocols to be run. 8 | 9 | It is primarily designed for flow chemistry equipment; it is designed to work by 10 | defining parameters to set for each virtual instrument interface, rather than by 11 | defining methods to be called. 12 | 13 | Blocktopus is a web-based user interface for definind octopus experiments based on 14 | the Google Blockly block-based programming environment. 15 | 16 | 17 | # Blocktopus Installation 18 | 19 | ## Download Repository 20 | 21 | ### Download by cloning the repository: 22 | 23 | ``` 24 | git clone https://github.com/richardingham/octopus.git 25 | cd octopus 26 | mkdir data 27 | ``` 28 | 29 | ### (Optional) - add plugins: 30 | 31 | - Either, rename `octopus-plugins.txt.example` to `octopus-plugins.txt` and add any plugins you want to use. 32 | - Or, create a `plugins` directory and pull any plugins into that directory. 33 | 34 | 35 | ## Option 1 - Run in Docker 36 | 37 | Build and run docker container: 38 | 39 | ``` 40 | docker build -t "octopus:latest" . 41 | docker run -it -p 8001:8001 -p 9000:9000 -v /app/data:/app/data octopus:latest 42 | ``` 43 | 44 | Access the interface: 45 | 46 | ``` 47 | http://127.0.0.1:8001 48 | ``` 49 | 50 | 51 | ## Option 2 - Run without Docker 52 | 53 | ### Install requirements and build 54 | 55 | ``` 56 | pyenv local 3.9.5 57 | pip install -r requirements.txt 58 | pip install -r octopus-plugins.txt 59 | yarn install 60 | yarn run build 61 | ``` 62 | 63 | ### Start the application 64 | 65 | ``` 66 | python octopus/blocktopus/server/server.py --plugins-dir=plugins 67 | ``` 68 | 69 | Use `--help` for all options. 70 | 71 | ### Access the interface: 72 | 73 | ``` 74 | http://127.0.0.1:8001 75 | ``` 76 | 77 | 78 | # Octopus only installation 79 | 80 | ``` 81 | pyenv local 3.9.5 82 | pip install git+https://github.com/richardingham/octopus.git 83 | ``` 84 | 85 | ## Octopus - command line 86 | 87 | ``` 88 | $ python -m octopus.console 89 | ``` 90 | 91 | ```python 92 | >>> reactor = vapourtec.R2R4(serial("/dev/ttyUSB0", baudrate = 19200)) 93 | >>> reactor.power.value 94 | off 95 | 96 | >>> reactor.pump1.target.set(1000) 97 | >>> reactor.power.set("on") 98 | >>> reactor.power.value 99 | on 100 | 101 | >>> reactor.pump1.pressure.value 102 | 1866 103 | ``` 104 | 105 | * [Read the full documentation](doc/Octopus Documentation.md) 106 | 107 | I recommend the use of [GNU screen](https://www.gnu.org/software/screen/) when 108 | running long experiments on a remote computer, to avoid having a network 109 | disconnection terminate the experiment. For a good introduction to screen, 110 | visit [aperiodic.net](http://aperiodic.net/screen/start). 111 | 112 | 113 | # Plugins 114 | 115 | Instrument interfaces are provided via plugins. 116 | 117 | A plugin is any python package that provides instruments. These should be installed using pip. 118 | For instruments to be auto-discovered by Blocktopus there should be an entry point to `blocktopus_blocks` 119 | defined for each block in the setup.py. 120 | 121 | See [octopus_wpi](https://github.com/richardingham/octopus_wpi) for an example. 122 | 123 | 124 | # Available Machine Interfaces 125 | 126 | * [Gilson](doc/Manufacturer%20-%20Gilson.md) - 506C Control Module, 127 | 233XL Sample Injector, 402 Syringe Pump, 151 UV/Vis. 128 | * [Heidolph](doc/Manufacturer%20-%20Heidolph.md) - Hei-End Hotplate. 129 | * [Kern](doc/Manufacturer%20-%20Kern.md) - PCB Balance 130 | * [Knauer](doc/Manufacturer%20-%20Knauer.md) - K-120, S-100 HPLC pump. 131 | * [Mettler Toledo](doc/Manufacturer%20-%20Mettler Toledo.md) - Balance, iC IR connector. 132 | * [ThalesNano](doc/Manufacturer%20-%20ThalesNano.md) - H-Cube. 133 | * [Vapourtec](doc/Manufacturer%20-%20Vapourtec.md) - R2+/R4. (*Contact author for access.*) 134 | * [VICI](doc/Manufacturer%20-%20Vici.md) - Multi-Position Valve. 135 | * [World Precision Instruments](https://github.com/richardingham/octopus_wpi) - Aladdin Syringe Pump. 136 | 137 | ## Using Phidgets 138 | 139 | Drivers for some [Phidgets devices](http://www.phidgets.com) are available. 140 | Before use, the DLL and API must be installed. 141 | 142 | * [Phidgets](doc/Manufacturer - Phidgets.md). 143 | 144 | -------------------------------------------------------------------------------- /bin/docker-entrypoint-web: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -e 4 | 5 | # This will clean up old md5 digested files since they are volume persisted. 6 | # If you want to persist older versions of any of these files to avoid breaking 7 | # external links outside of your domain then feel free remove this line. 8 | rm -rf octopus/blocktopus/resources/pack 9 | 10 | # Always keep this here as it ensures the built and digested assets get copied 11 | # into the correct location. This avoids them getting clobbered by any volumes. 12 | cp -a /pack /app/octopus/blocktopus/resources/pack 13 | 14 | # Ensure any local plugins are available on the python path 15 | if [ -d /app/plugins ]; then 16 | for PLUGINDIR in /app/plugins/*; do 17 | if [ -d $PLUGINDIR ]; then 18 | export PYTHONPATH="${PYTHONPATH}:{$PLUGINDIR}"; 19 | fi 20 | done 21 | fi 22 | 23 | exec "$@" -------------------------------------------------------------------------------- /bin/pip3-install: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -e 4 | 5 | pip3 install --no-warn-script-location --user -r requirements.txt 6 | 7 | # If requirements.txt is newer than the lock file or the lock file doesn't exist. 8 | if [ requirements.txt -nt requirements-lock.txt ]; then 9 | pip3 freeze --user > requirements-lock.txt 10 | fi 11 | 12 | pip3 install --no-warn-script-location --user \ 13 | -r requirements.txt -c requirements-lock.txt -------------------------------------------------------------------------------- /doc/Installation - Phidget Driver.md: -------------------------------------------------------------------------------- 1 | Installing Phidgets 2 | =================== 3 | 4 | To use [phidgets devices](http://www.phidgets.com/) you will first 5 | need to compile the drivers and install the Python interface. 6 | 7 | ``` 8 | $ sudo apt-get install libusb-1.0-0-dev 9 | $ wget http://www.phidgets.com/downloads/libraries/libphidget.tar.gz 10 | $ tar -xzvf libphidget.tar.gz 11 | $ cd libphidget-... 12 | $ ./configure 13 | $ make 14 | ``` 15 | 16 | [ This will take a while ] 17 | 18 | ``` 19 | $ sudo make install 20 | $ sudo cp udev/99-phidgets.rules /etc/udev/rules.d 21 | ``` 22 | 23 | If hotplug is installed: 24 | ``` 25 | $ sudo cp hotplug/* /etc/hotplug/usb 26 | $ sudo chmod 755 /etc/hotplug/usb/phidgets 27 | ``` 28 | 29 | [ You might want to save libphidget-... somewhere to save 30 | having to compile again on a raspberry pi ] 31 | 32 | ``` 33 | $ rm libphidget.tar.gz 34 | $ rm -r libphidget-... 35 | ``` 36 | 37 | ``` 38 | $ wget http://www.phidgets.com/downloads/libraries/PhidgetsPython.zip 39 | $ unzip PhidgetsPython.zip 40 | $ cd PhidgetsPython 41 | $ sudo python setup.py install 42 | $ rm -r PhidgetsPython 43 | $ rm PhidgetsPython.zip 44 | ``` 45 | 46 | Check that installation was successful: 47 | 48 | ```python 49 | >>> import Phidgets 50 | ``` 51 | -------------------------------------------------------------------------------- /doc/Manufacturer - Heidolph.md: -------------------------------------------------------------------------------- 1 | Heidolph Hei-End Stirrer Hotplate 2 | ================================= 3 | 4 | ```python 5 | heidolph.HeiEnd(connection) 6 | ``` 7 | 8 | Connect using proprietary serial cable. 9 | 10 | Baud rate 9600. 7 data bits. Even Parity. One Stop Bit. 11 | 12 | 13 | Properties 14 | ---------- 15 | 16 | * `heater` 17 | 18 | * `power` (rw, str) 19 | 20 | Values: "on", "off" 21 | 22 | * `safetydelta` (rw, int) 23 | 24 | Safety delta value (degree C) 25 | 26 | * `mediumtemp` (r, float) 27 | 28 | Current temperature as measured by external Pt-1000 thermocouple. 29 | 30 | * `mediumsafetytemp` (r, float) 31 | 32 | Reading from second external thermocouple (the Pt-1000 has two). 33 | 34 | * `hotplatetemp` (r, float) 35 | 36 | Current temperature as measured by internal thermocouple. 37 | 38 | * `hotplatesafetytemp` (r, float) 39 | 40 | Reading from second internal thermocouple. 41 | 42 | * `stirrer` 43 | 44 | * `power` (rw, str) 45 | 46 | Values: "on", "off" 47 | 48 | * `target` (rw, int) 49 | 50 | Target stirrer speed in rpm. 51 | 52 | * `speed` (r, int) 53 | 54 | Current speed in rpm. 55 | 56 | 57 | Methods 58 | ------- 59 | 60 | None 61 | 62 | -------------------------------------------------------------------------------- /doc/Manufacturer - Kern.md: -------------------------------------------------------------------------------- 1 | Kern PCB Balance 2 | ================ 3 | 4 | ```python 5 | kern.PCB(connection) 6 | ``` 7 | 8 | Connect using serial cable with one male and one female end. NB neets to be a DTS-DTE cable, not a straight-through. 9 | 10 | Baud rate 19200 (can be changed). 8N1. 11 | Make sure that the "ignore small changes" setting is switched off. 12 | 13 | Properties 14 | ---------- 15 | 16 | * `weight` (r, float) 17 | 18 | The current weight in grams. 19 | 20 | 21 | Methods 22 | ------- 23 | 24 | * `getStableWeight()` 25 | 26 | Returns a Deferred which calls back with a weight as soon as it is stable. 27 | 28 | * `tare()` 29 | 30 | Tares the balance. Returns a Deferred that calls back after 5s. 31 | -------------------------------------------------------------------------------- /doc/Manufacturer - Knauer.md: -------------------------------------------------------------------------------- 1 | Knauer K120 2 | =========== 3 | 4 | ```python 5 | knauer.K120(connection) 6 | ``` 7 | 8 | Serial Parameters: Baud rate 9600. 8N1. Requires a crossover cable. 9 | 10 | Properties 11 | ---------- 12 | 13 | * `status` (r, str) 14 | 15 | Current status. 16 | 17 | Values: "ok", "motor-blocked", "manual-stop" 18 | Can't figure out how to acheive "manual-stop" 19 | 20 | * `power` (rw, str) 21 | 22 | System Power. 23 | 24 | Values: "on", "off". 25 | Default: "off". 26 | 27 | * `target` (rw, int) 28 | 29 | Target flow rate in uL/min. 30 | 31 | * `rate` (r, int) 32 | 33 | Current flow rate in uL/min. 34 | 35 | 36 | Methods 37 | ------- 38 | 39 | * `allowKeypad(allow)` 40 | 41 | if `allow` is `False`, locks the keypad. `True` to unlock. 42 | 43 | 44 | Knauer S100 45 | =========== 46 | 47 | ```python 48 | knauer.S100(connection) 49 | ``` 50 | 51 | Serial Parameters: Baud rate 9600. 8N1. Requires a crossover cable. 52 | 53 | Properties 54 | ---------- 55 | 56 | * `status` (r, str) 57 | 58 | Current status. 59 | 60 | Values: "idle", "running", "overpressure", "underpressure", "overcurrent", "undercurrent" 61 | 62 | * `power` (rw, str) 63 | 64 | System Power. 65 | 66 | Values: "on", "off". 67 | Default: "off". 68 | 69 | * `target` (rw, int) 70 | 71 | Target flow rate in uL/min. 72 | 73 | * `rate` (r, int) 74 | 75 | Current flow rate in uL/min. 76 | 77 | * `pressure` (r, int) 78 | 79 | Current pressure in mbar . 80 | 81 | Methods 82 | ------- 83 | 84 | * `allowKeypad(allow)` 85 | 86 | if `allow` is `False`, locks the keypad. `True` to unlock. 87 | 88 | -------------------------------------------------------------------------------- /doc/Manufacturer - Mettler Toledo.md: -------------------------------------------------------------------------------- 1 | SICS Balance 2 | ============ 3 | 4 | ```python 5 | mt.SICSBalance(connection) 6 | ``` 7 | 8 | Connect using serial cable. 9 | 10 | Properties 11 | ---------- 12 | 13 | * `weight` (r, float) 14 | 15 | The current weight in grams. 16 | 17 | 18 | Methods 19 | ------- 20 | 21 | * `getStableWeight()` 22 | 23 | Returns a Deferred which calls back with a weight as soon as it is stable. 24 | 25 | * `tare()` 26 | 27 | Tares the balance. Returns a Deferred that calls back after 5s. 28 | 29 | 30 | ICIR Connection 31 | =============== 32 | 33 | iC IR is running on a computer that is accessible via LAN. 34 | Connector Server is running (see tools/iCIR_server in octopus distribution) 35 | 36 | Connect via TCP on port 8124. 37 | 38 | ```python 39 | mt.ICIR(connection, stream_names) 40 | ``` 41 | 42 | `stream_names` is a list of named peaks as configured within iC IR. It is recommended to give the peaks simple names 43 | such as "starting_material" and "product". 44 | 45 | * A parameter is created for each stream, with units of mAU. (r, float) 46 | 47 | The name of this parameter is modified from the name passed in, to make it a valid attribute name: 48 | 49 | Characters other than [a-zA-Z0-9_] will be removed. 50 | If the first character of the name is not [a-zA-Z] the name will be prefixed with "stream_" 51 | 52 | * The machine object also has an attribute, `streams`, which is a list of all streams with their original names as passed in. 53 | -------------------------------------------------------------------------------- /doc/Manufacturer - Phidgets.md: -------------------------------------------------------------------------------- 1 | Phidgets Devices 2 | ================ 3 | 4 | Phidgets devices connect by USB (some connect via an InterfaceKit which is 5 | itself connected by USB). 6 | 7 | The Phidgets Drivers need to be installed - see doc/Installation - Phidgets 8 | 9 | Use the protocols.phidgets.Phidget connection, which accepts the phidget 10 | device ID as a parameter. N.B. You can use the Phidgets Control Panel software 11 | (for windows, downloadable from phidgets.com) to determine the ID for each 12 | device. 13 | 14 | ```python 15 | connection = Phidget(1005) 16 | ``` 17 | 18 | For devices that connect via an InterfaceKit, use the `InterfaceKit.input()`, 19 | `InterfaceKit.output()` and `InterfaceKit.sensor()` methods to get connection 20 | objects. 21 | 22 | ```python 23 | ifk = phidgets.InterfaceKit(Phidget(1005)) 24 | device = phidgets.PHSensor(ifk.sensor(2)) 25 | ``` 26 | 27 | InterfaceKit 28 | ============ 29 | 30 | ```python 31 | phidgets.InterfaceKit(Phidget(id)) 32 | ``` 33 | 34 | Methods 35 | ------- 36 | 37 | * `input(id)` 38 | 39 | Access a connection to digital input `id`. The connection object has a 40 | method, `state()` that returns the input state. 41 | 42 | * `output(id)` 43 | 44 | Access a connection to digital output `id`. The connection object has 45 | two methods: `state` returns the output state, and `set` which changes 46 | it. 47 | 48 | * `sensor(positions)` 49 | 50 | Access a connection to analogue sensor `id`. The connection object has a 51 | method, `value()` that returns the current voltage detected by the sensor. 52 | 53 | 54 | TemperatureSensor 55 | ================= 56 | 57 | Tested with the four-thermocouple device. 58 | 59 | ```python 60 | phidgets.TemperatureSensor(connection, inputs) 61 | ``` 62 | 63 | `inputs` is a list of the connected thermocouples: 64 | 65 | ```python 66 | inputs = [{ 67 | "index": 0, # Which position the thermocouple is connected to (0-3). 68 | "type": type, # What sort of thermocouple is attached. 69 | "min_change": 0.5, # Minimum temperature change to record (default 0.5 deg). 70 | }] 71 | ``` 72 | 73 | `type` can be a member of `phidgets.ThermocoupleType`: 74 | 75 | * `phidgets.ThermocoupleType.E` (E-type) 76 | * `phidgets.ThermocoupleType.J` (J-type) 77 | * `phidgets.ThermocoupleType.K` (K-type) 78 | * `phidgets.ThermocoupleType.T` (T-type) 79 | 80 | TemperatureSensor is created with an list of attached thermocouples: 81 | 82 | `TemperatureSensor.thermocouples[...]` 83 | 84 | Each themocouple object has a `temperature` property (r, float) with units 85 | degree C. 86 | 87 | 88 | PHSensor 89 | ======== 90 | 91 | Only tested with USB model. 92 | 93 | ```python 94 | phidgets.PHSensor(connection, min_change) 95 | ``` 96 | 97 | `min_change` is the minimum pH change that will be registered. Default: 0.5. 98 | 99 | Properties 100 | ---------- 101 | 102 | * `temperature` (rw, float) 103 | 104 | Current temperature at probe (degree C). 105 | 106 | * `ph` (r, float) 107 | 108 | The current measured pH value. 109 | -------------------------------------------------------------------------------- /doc/Manufacturer - ThalesNano.md: -------------------------------------------------------------------------------- 1 | ThalesNano H-Cube 2 | ================= 3 | 4 | ```python 5 | thalesnano.HCube(connection) 6 | ``` 7 | 8 | Baud rate 9600 (can be changed). 8N1. 9 | 10 | Properties 11 | ---------- 12 | 13 | * `state` (r, str) 14 | 15 | Current state . 16 | 17 | * `message` (r, str) 18 | 19 | Current status message. 20 | 21 | * `system_pressure_target` (rw, int) 22 | 23 | Current setting for system pressure. Hydrogen must be off to change the pressure. 24 | Takes values from 10-100 in steps of 10, in bar. 25 | 26 | * `system_pressure` (r, int) 27 | 28 | Current pressure reading (bar). 29 | 30 | * `inlet_pressure` (r, int) 31 | 32 | Pressure at the inlet, in bar. 33 | 34 | * `hydrogen_pressure` (r, int) 35 | 36 | Should be the H2 pressure. Doesn't work. 37 | 38 | * `column_temperature_target` (rw, int) 39 | 40 | Current setting for column temperature. 41 | Takes values from 10-100 in steps of 10, in degrees C. 42 | 43 | Changes incrementally if the hydrogen is on. 44 | Completes only when the target has changed all the way. 45 | 46 | * `column_temperature` (r, int) 47 | 48 | Current temperature reading (degree C). 49 | 50 | * `hydrogen_mode` (rw, str) 51 | 52 | Takes values "full", "controlled", "off". If mode is set to "full", "system_pressure_target" will be set to 0. 53 | Can't change when the hydrogen is on. 54 | 55 | * `gas_liquid_ratio` (r, int) 56 | 57 | Allegedly some kind of bubble counter. 58 | 59 | Methods 60 | ------- 61 | 62 | * `start_hydrogenation()` 63 | 64 | Turn on hydrogen. Completes once pressure has built up. 65 | 66 | * `stop_keep_hydrogen()` 67 | 68 | Release pressure, keep hydrogen. 69 | Completes after valves have closed. 70 | 71 | * `stop_release_hydrogen()` 72 | 73 | Release pressure, don't keep hydrogen. 74 | Completes after pressure has released. 75 | 76 | * `shutdown()` 77 | 78 | Shut down H-Cube. 79 | -------------------------------------------------------------------------------- /doc/Manufacturer - Vapourtec.md: -------------------------------------------------------------------------------- 1 | Vapourtec R2+/R4 2 | ================ 3 | 4 | NB. This control file is currently not included in the distribution, because 5 | Vapourtec have requested that we do not publish it. Documentation for the 6 | serial command API is available from [Vapourtec](http://www.vapourtec.co.uk), 7 | using which a virtual instrument can be created. 8 | 9 | 10 | ```python 11 | vapourtec.R2R4(connection) 12 | ``` 13 | 14 | Serial Parameters: Baud rate 19200. 8N1. 15 | 16 | If only the R2 is connected, the heaters will show up as "off". 17 | 18 | Connecting a second R1/R2 is not supported. Second unit will be ignored. 19 | 20 | Results with an R1 unit are unknown. 21 | 22 | Properties 23 | ---------- 24 | 25 | * `status` (r, str) 26 | 27 | Current status. 28 | 29 | Values: "off", "running", 30 | "system overpressure", "pump 1 overpressure", "pump 2 overpressure", 31 | "system underpressure", "pump 1 underpressure", "pump 2 underpressure" 32 | 33 | * `power` (rw, str) 34 | 35 | System Power. 36 | 37 | Values: "on", "off". 38 | 39 | Default: "off". 40 | 41 | * `pressure` (r, int) 42 | 43 | System pressure in mbar. 44 | 45 | * `pressure_limit` (rw, int) 46 | 47 | System pressure limit in mbar. 48 | 49 | Default: 15000. 50 | 51 | * `output` (rw, str) 52 | 53 | Position of output valve. 54 | 55 | Values: "waste", "collect". 56 | 57 | Default: "waste". 58 | 59 | * `loop1`, `loop2` (rw, str) 60 | 61 | Position of loop valves. 62 | 63 | Values: "load", "inject". 64 | 65 | Default: "load". 66 | 67 | * `pump1` ... `pump4` - Pumps 68 | 69 | * `pump1.target` (rw, int) 70 | 71 | Target flow rate in uL/min. 72 | 73 | * `pump1.input` (rw, str) 74 | 75 | Pump input valve position. 76 | 77 | Values "solvent", "reagent". 78 | 79 | Default "solvent". 80 | 81 | * `pump1.rate` (r, int) 82 | 83 | Current flow rate in uL/min. 84 | 85 | * `pump1.pressure` (r, int) 86 | 87 | Current pressure in mbar. 88 | 89 | * `pump1.airlock` (r, int) 90 | 91 | Current airlock value. 92 | 93 | Unknown unit. > ~ 10000 is bad. 94 | 95 | * `heater1` ... `heater4` - Heaters 96 | 97 | * `heater1.target` (rw, int) 98 | 99 | Target Temperature. Allowed values depend on connected heater/cooler. 100 | 101 | Default -1000 (off). 102 | 103 | * `heater1.mode` (r, str) 104 | 105 | Current heating mode. 106 | 107 | Values: "off", "cooling", "heating", "stable unheated", "stable heated". 108 | 109 | * `heater1.power` (r, int) 110 | 111 | Current power draw in W. 112 | 113 | * `heater1.temp` (r, int) 114 | 115 | Current temperature in degrees C. 116 | 117 | 118 | Methods 119 | ------- 120 | 121 | * `gsioc(id)` 122 | 123 | Builds a connection object to refer to an instrument connected on the R2's GSIOC port. 124 | 125 | `id` is the GSIOC id of the desired connection. (You have to create a new connection for each device) 126 | 127 | ```python 128 | r = vapourtec.R2R4(connection) 129 | g = gilson.UVVis151(r.gsioc(15)) 130 | ``` 131 | 132 | -------------------------------------------------------------------------------- /doc/Manufacturer - Vici.md: -------------------------------------------------------------------------------- 1 | Valco Vici Multiposition Valve 2 | ============================== 3 | 4 | ```python 5 | vici.MultiValve(connection) 6 | ``` 7 | 8 | Connect using proprietary serial cable, or make your own according to the specification. The manual controller does not need to be connected. 9 | 10 | Baud rate 9600 (can be changed). 8N1. 11 | 12 | Properties 13 | ---------- 14 | 15 | * `position` (rw, int) 16 | 17 | The current position. Default 0. 18 | 19 | 20 | Methods 21 | ------- 22 | 23 | * `move(position, direction)` 24 | 25 | Move to `position`. Direction can be `c|cw|clockwise`, `a|cc|counterclockwise` or `f|fastest`. 26 | 27 | * `advance(positions)` 28 | 29 | Increment `position` by `positions`. 30 | 31 | -------------------------------------------------------------------------------- /doc/Manufacturer - WPI.md: -------------------------------------------------------------------------------- 1 | World Precision Instruments Aladdin-100 Syringe Pump 2 | ==================================================== 3 | 4 | ```python 5 | wpi.Aladdin(connection, syringe_diameter) 6 | ``` 7 | 8 | Connect using proprietary serial cable, or make your own according to the specification. 9 | 10 | Baud rate set in pump configuration. 8N1. 11 | 12 | `syringe_size` is the diameter of the currently installed syringe, in mm. 13 | 14 | This driver works by programming a simple pumping program with no volume limit and default 15 | zero rate. The pump speed is adjusted by altering the rate, and switching on and off the 16 | pump. 17 | 18 | NB the communications protocol is relatively complex but currently works. 19 | 20 | Properties 21 | ---------- 22 | 23 | * `status` (r, str) 24 | 25 | The current state. 26 | 27 | Values: "infusing", "withdrawing", "program-stopped", 28 | "program-paused", "pause-phase", "trigger-wait", "alarm" 29 | 30 | * `rate` (rw, int) 31 | 32 | Current rate in uL/min. 33 | 34 | * `direction` (rw, str) 35 | 36 | Values: "infuse", "withdraw" 37 | 38 | * `dispensed` (r, float) 39 | 40 | Current dispensed volume, mL. 41 | 42 | * `withdrawn` (r, float) 43 | 44 | Current withdrawn volume, mL. 45 | 46 | 47 | Methods 48 | ------- 49 | 50 | None 51 | 52 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | services: 2 | web: 3 | build: 4 | context: . 5 | tty: true 6 | ports: 7 | - "8001:8001" 8 | - "9000:9000" 9 | volumes: 10 | - ./data:/app/data 11 | - ./plugins:/app/plugins 12 | -------------------------------------------------------------------------------- /examples/sequence.py: -------------------------------------------------------------------------------- 1 | import twisted.python.log 2 | import sys 3 | twisted.python.log.startLogging(sys.stdout) 4 | 5 | from twisted.internet import defer 6 | defer.Deferred.debug = True 7 | 8 | from octopus.sequence.runtime import * 9 | 10 | s = sequence( 11 | log("one"), 12 | sequence( 13 | log("two"), 14 | log("three"), 15 | ), 16 | wait("3s"), 17 | log("four"), 18 | ) 19 | 20 | run(s) 21 | 22 | 23 | -------------------------------------------------------------------------------- /examples/test_abort.py: -------------------------------------------------------------------------------- 1 | from twisted.internet import reactor 2 | from twisted.python import log 3 | from octopus.sequence.shortcuts import * 4 | from octopus.sequence.experiment import Experiment 5 | 6 | s = wait(2) 7 | e = Experiment(s) 8 | reactor.callLater(0.5, e.pause) 9 | reactor.callLater(1, e.resume) 10 | reactor.callLater(1.5, e.stop) 11 | reactor.callLater(0, e.run) 12 | 13 | s1 = wait(2) 14 | e1 = Experiment(s1) 15 | reactor.callLater(2.5, s1.abort) 16 | reactor.callLater(2, e1.run) 17 | 18 | s2 = wait(2) 19 | e2 = Experiment(s2) 20 | reactor.callLater(3.5, s2.cancel) 21 | reactor.callLater(3, e2.run) 22 | 23 | reactor.callLater(4, reactor.stop) 24 | reactor.run() 25 | -------------------------------------------------------------------------------- /examples/test_bind.py: -------------------------------------------------------------------------------- 1 | from octopus.sequence.runtime import * 2 | from octopus.sequence.control import Bind 3 | 4 | from twisted.internet import defer 5 | defer.Deferred.debug = True 6 | 7 | v = variable(0, "i", "i") 8 | d = variable(False, "b", "b") 9 | 10 | d_ctrl = Bind(d, v, lambda x: x > 5) 11 | 12 | s = sequence( 13 | log("Running"), 14 | loop_while(v < 10, [ 15 | increment(v), 16 | log("v = " + v + "; d = " + d), 17 | wait(1), 18 | ]), 19 | log("Stopping experiment") 20 | ) 21 | s.dependents.add(d_ctrl) 22 | 23 | run(s) 24 | 25 | 26 | -------------------------------------------------------------------------------- /examples/test_call.py: -------------------------------------------------------------------------------- 1 | from octopus.sequence.runtime import * 2 | 3 | def fn (): 4 | return sequence( 5 | log("fn called") 6 | ) 7 | 8 | run(sequence( 9 | call(fn) 10 | )) 11 | 12 | -------------------------------------------------------------------------------- /examples/test_dep.py: -------------------------------------------------------------------------------- 1 | from twisted.internet import reactor 2 | 3 | from octopus.sequence.runtime import * 4 | from octopus.sequence import runtime as r 5 | from octopus.sequence.util import Runnable, Pausable, Cancellable, Dependent 6 | 7 | 8 | class MyD (Dependent): 9 | 10 | def __init__ (self, i): 11 | Dependent.__init__(self) 12 | self.i = i 13 | 14 | def _run (self): 15 | print ("Dep %d Run" % self.i) 16 | 17 | def _pause (self): 18 | print ("Dep %d Pause" % self.i) 19 | 20 | def _resume (self): 21 | print ("Dep %d Resume" % self.i) 22 | 23 | def _cancel (self, abort = False): 24 | print ("Dep %d Cancel" % self.i) 25 | 26 | def _reset (self): 27 | print ("Dep %d Reset" % self.i) 28 | 29 | e = r._experiment 30 | d1 = MyD(1) 31 | d2 = MyD(2) 32 | 33 | s = sequence( 34 | log("Starting experiment"), 35 | wait("5m"), 36 | log("Stopping experiment") 37 | ) 38 | s.dependents.add(d1) 39 | s.dependents.add(d2) 40 | 41 | reactor.callLater(2, e.pause) 42 | reactor.callLater(4, e.resume) 43 | reactor.callLater(6, s[1].delay, 7) 44 | reactor.callLater(7, d2.cancel) 45 | reactor.callLater(8, s[1].cancel) 46 | 47 | run(s) 48 | 49 | 50 | -------------------------------------------------------------------------------- /examples/test_on.py: -------------------------------------------------------------------------------- 1 | from octopus.sequence.runtime import * 2 | from octopus.sequence.util import Trigger 3 | from twisted.internet import reactor 4 | 5 | 6 | def fn (): 7 | print ("fn called") 8 | return sequence( 9 | log("fn called"), 10 | set(v, False) 11 | ) 12 | 13 | v = variable(False, "v", "v") 14 | v2 = variable(False, "v", "v") 15 | 16 | o1 = Trigger(v == True, fn) 17 | o2 = Trigger(v2 == True, log("o2 triggered"), max_calls = 1) 18 | 19 | s = sequence( 20 | log("Loading o"), 21 | wait("8s"), 22 | set(v2, True), 23 | wait("1s") 24 | ) 25 | 26 | s.dependents.add(o1) 27 | s.dependents.add(o2) 28 | 29 | reactor.callLater(2, v.set, True) 30 | reactor.callLater(4, v.set, True) 31 | reactor.callLater(6, v.set, True) 32 | 33 | run(s) 34 | 35 | -------------------------------------------------------------------------------- /examples/test_statemonitor.py: -------------------------------------------------------------------------------- 1 | from octopus.sequence.runtime import * 2 | from octopus.sequence.control import StateMonitor 3 | 4 | v = variable(0, "i", "i") 5 | test = (((v >= 4) & (v <= 6)) | ((v >= 9) & (v <= 14))) == False 6 | 7 | d = StateMonitor() 8 | d.add(test) 9 | d.trigger_step = sequence( 10 | log("Triggered"), 11 | wait(4), 12 | log("Still Triggered"), 13 | ) 14 | d.reset_step = sequence( 15 | log("Reset"), 16 | wait(4), 17 | log("... not triggered again") 18 | ) 19 | 20 | s = sequence( 21 | log("Running"), 22 | loop_while(v < 20, [ 23 | increment(v), 24 | wait(1), 25 | log("v = " + v) 26 | ]), 27 | log("Stopping experiment") 28 | ) 29 | s.dependents.add(d) 30 | 31 | run(s) 32 | 33 | 34 | -------------------------------------------------------------------------------- /examples/test_tick.py: -------------------------------------------------------------------------------- 1 | from twisted.internet import reactor 2 | 3 | from octopus.sequence.runtime import * 4 | from octopus.sequence import runtime as r 5 | from octopus.sequence.util import Tick 6 | 7 | def fn1 (): 8 | print ("d3 tick.") 9 | 10 | def fn2 (): 11 | return log("d4 tick...") 12 | 13 | e = r._experiment 14 | d1 = Tick(sequence( 15 | log("d1 tick...") 16 | ), interval = 2) 17 | 18 | d2 = Tick(log("d2 tick"), interval = 3) 19 | d3 = Tick(fn1, interval = 1) 20 | d4 = Tick(fn2, interval = 1) 21 | 22 | w1 = wait("10s") 23 | w2 = wait("3s") 24 | w3 = wait("3s") 25 | s = sequence( 26 | log("Starting experiment"), 27 | w1, 28 | w2, 29 | cancel(d1), 30 | w3, 31 | log("Stopping experiment") 32 | ) 33 | s.dependents.add(d1) 34 | w1.dependents.add(d2) 35 | w2.dependents.add(d3) 36 | w3.dependents.add(d4) 37 | 38 | run(s) 39 | 40 | 41 | -------------------------------------------------------------------------------- /examples/test_while.py: -------------------------------------------------------------------------------- 1 | from octopus.sequence.runtime import * 2 | 3 | v = variable(0, "v", "v") 4 | 5 | s = sequence( 6 | loop_while(v < 5, sequence( 7 | log("v = " + v), 8 | set(v, v + 1) 9 | )), 10 | set(v, 0), 11 | loop_while(v < 2, sequence( 12 | log("v = " + v), 13 | set(v, v + 1) 14 | ), min_calls = 5), 15 | log("Done") 16 | ) 17 | 18 | run(s) 19 | 20 | -------------------------------------------------------------------------------- /examples/variables1.py: -------------------------------------------------------------------------------- 1 | 2 | from octopus.sequence.runtime import * 3 | 4 | v = variable(0, "variable", "variable") 5 | 6 | run( 7 | 8 | sequence( #23 9 | once(v > 4, log("v > 4")), #2 #1 10 | wait("2s"), #3 11 | loop_while(v < 2, #7 12 | sequence( #6 13 | increment(v), #4 14 | wait("1 s"), #5 15 | ) 16 | ), 17 | parallel( #15 18 | loop_until(v > 5, #11 19 | sequence( #10 20 | increment(v), #8 21 | wait("2 sec"), #9 22 | ) 23 | ), 24 | sequence( #14 25 | wait_until(v == 4), #12 26 | log("V is now 5"), #13 27 | ), 28 | ), 29 | do_if(v > 10, log("v > 10"), log("v < 10")), #18 #16 #17 30 | do_if(v > 1, log("v > 1"), log("v < 1")), #21 #19 #20 31 | log("Done!"), #22 32 | ) 33 | 34 | ) 35 | -------------------------------------------------------------------------------- /octopus-plugins.txt.example: -------------------------------------------------------------------------------- 1 | # Git repositories need 'git' to be installed 2 | #git+https://github.com/richardingham/octopus_wpi.git 3 | -------------------------------------------------------------------------------- /octopus/blocks/.keep: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /octopus/blocktopus/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/richardingham/octopus/64c97ec75f6d89f247c01257fa4013d5e83ad693/octopus/blocktopus/__init__.py -------------------------------------------------------------------------------- /octopus/blocktopus/block_registry.py: -------------------------------------------------------------------------------- 1 | 2 | from typing import Type 3 | from types import ModuleType 4 | from octopus.blocktopus.workspace import Block 5 | 6 | from twisted.logger import Logger 7 | log = Logger() 8 | 9 | 10 | block_types = {} 11 | exclude = set([Block]) 12 | processed = set() 13 | 14 | 15 | def register_builtin_blocks(): 16 | from octopus.blocktopus import blocks 17 | import importlib 18 | 19 | for mod_name in blocks.__all__: 20 | mod = importlib.import_module(f".{mod_name}", blocks.__name__) 21 | register_module_blocks(mod) 22 | 23 | 24 | def register_module_blocks(mod: ModuleType): 25 | log.debug("Registering blocks from module {module}", module=mod) 26 | 27 | mod_dict = { name: cls for name, cls in mod.__dict__.items() if isinstance(cls, type) and issubclass(cls, Block) and cls not in processed } 28 | 29 | if "__all__" in mod.__dict__: 30 | exclude.update([cls for name, cls in mod_dict.items() if name not in mod.__all__]) 31 | mod_dict = { name: cls for name, cls in mod_dict.items() if name in mod.__all__ } 32 | 33 | if "__exclude_blocks__" in mod.__dict__: 34 | exclude.update([cls for name, cls in mod_dict.items() if name in mod.__exclude_blocks__]) 35 | 36 | for name, cls in mod_dict.items(): 37 | if isinstance(cls, type) and issubclass(cls, Block) and cls not in exclude: 38 | register_block(name, cls) 39 | 40 | processed.update(mod_dict.values()) 41 | 42 | 43 | def register_block(name: str, block: Type[Block]): 44 | if name in block_types: 45 | raise ValueError(f"Block type {name} is already registered ({block_types[name]}).") 46 | 47 | log.debug("Registering block {block_name} as {block_cls}", block_name=name, block_cls=block) 48 | block_types[name] = block 49 | 50 | 51 | def get_block_class(name: str) -> Type[Block]: 52 | return block_types[name] 53 | -------------------------------------------------------------------------------- /octopus/blocktopus/blockly/blocks/colour.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @license 3 | * Visual Blocks Editor 4 | * 5 | * Copyright 2012 Google Inc. 6 | * https://developers.google.com/blockly/ 7 | * 8 | * Licensed under the Apache License, Version 2.0 (the "License"); 9 | * you may not use this file except in compliance with the License. 10 | * You may obtain a copy of the License at 11 | * 12 | * http://www.apache.org/licenses/LICENSE-2.0 13 | * 14 | * Unless required by applicable law or agreed to in writing, software 15 | * distributed under the License is distributed on an "AS IS" BASIS, 16 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 17 | * See the License for the specific language governing permissions and 18 | * limitations under the License. 19 | */ 20 | 21 | /** 22 | * @fileoverview Colour blocks for Blockly. 23 | * @author fraser@google.com (Neil Fraser) 24 | */ 25 | 'use strict'; 26 | 27 | import Blockly from '../core/blockly'; 28 | import Blocks from '../core/blocks'; 29 | import Msg from '../core/msg'; 30 | import FieldColour from '../core/field_colour'; 31 | import {MATH_CATEGORY_HUE} from '../colourscheme'; 32 | 33 | Blocks['colour_picker'] = { 34 | /** 35 | * Block for colour picker. 36 | * @this Block 37 | */ 38 | init: function() { 39 | this.setHelpUrl(Msg.COLOUR_PICKER_HELPURL); 40 | this.setColour(MATH_CATEGORY_HUE); 41 | this.appendDummyInput() 42 | .appendField(new FieldColour('#ff0000'), 'COLOUR'); 43 | this.setOutput(true, 'Colour'); 44 | this.setTooltip(Msg.COLOUR_PICKER_TOOLTIP); 45 | } 46 | }; 47 | -------------------------------------------------------------------------------- /octopus/blocktopus/blockly/blocks/timing.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @license 3 | * Visual Blocks Editor 4 | * 5 | * Copyright 2012 Google Inc. 6 | * https://github.com/google/blockly 7 | * 8 | * Licensed under the Apache License, Version 2.0 (the "License"); 9 | * you may not use this file except in compliance with the License. 10 | * You may obtain a copy of the License at 11 | * 12 | * http://www.apache.org/licenses/LICENSE-2.0 13 | * 14 | * Unless required by applicable law or agreed to in writing, software 15 | * distributed under the License is distributed on an "AS IS" BASIS, 16 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 17 | * See the License for the specific language governing permissions and 18 | * limitations under the License. 19 | */ 20 | 21 | /** 22 | * @fileoverview Timing blocks for Blockly. 23 | * @author mail@richardingham.net (Richard Ingham) 24 | */ 25 | 'use strict'; 26 | 27 | import Blockly from '../core/blockly'; 28 | import Blocks from '../core/blocks'; 29 | import Block from '../core/block'; 30 | import Msg from '../core/msg'; 31 | import FieldTextInput from '../core/field_textinput'; 32 | import {CONTROL_CATEGORY_HUE} from '../colourscheme'; 33 | import {numberValidator} from '../core/validators'; 34 | 35 | Blocks['controls_wait'] = { 36 | /** 37 | * Block for wait (time) statement 38 | * @this Block 39 | */ 40 | init: function() { 41 | //this.setHelpUrl(Msg.CONTROLS_WAIT_HELPURL); 42 | this.setColour(CONTROL_CATEGORY_HUE); 43 | this.appendValueInput('TIME') 44 | .setCheck('Number') 45 | .appendField('wait for'); //Msg.CONTROLS_IF_MSG_IF); 46 | this.setPreviousStatement(true); 47 | this.setNextStatement(true); 48 | this.setTooltip('Pause the sequence for the specified time (in seconds)'); //Msg.CONTROLS_WAIT_TOOLTIP); 49 | } 50 | }; 51 | 52 | Blocks['controls_wait_until'] = { 53 | /** 54 | * Block for wait_until statement 55 | * @this Block 56 | */ 57 | init: function() { 58 | //this.setHelpUrl(Msg.CONTROLS_WAIT_HELPURL); 59 | this.setColour(CONTROL_CATEGORY_HUE); 60 | this.appendValueInput('CONDITION') 61 | .setCheck('Boolean') 62 | .appendField('wait until'); //Msg.CONTROLS_IF_MSG_IF); 63 | this.setPreviousStatement(true); 64 | this.setNextStatement(true); 65 | this.setTooltip('Pause the sequence until the passed condition is met'); //Msg.CONTROLS_WAIT_TOOLTIP); 66 | } 67 | }; 68 | 69 | Blocks['controls_maketime'] = { 70 | /** 71 | * Block for time value. 72 | * @this Block 73 | */ 74 | init: function() { 75 | this.fieldHour_ = new FieldTextInput('0', numberValidator); 76 | this.fieldMinute_ = new FieldTextInput('0', numberValidator); 77 | this.fieldSecond_ = new FieldTextInput('0', numberValidator); 78 | //this.setHelpUrl(Msg.MATH_NUMBER_HELPURL); 79 | this.setColour(CONTROL_CATEGORY_HUE); 80 | this.appendDummyInput() 81 | .appendField(this.fieldHour_, 'HOUR') 82 | .appendField('h') 83 | .appendField(this.fieldMinute_, 'MINUTE') 84 | .appendField('m') 85 | .appendField(this.fieldSecond_, 'SECOND') 86 | .appendField('s'); 87 | this.setOutput(true, 'Number'); 88 | this.setTooltip('Calculates a time in seconds'); // Msg.CONTROLS_TIME_TOOLTIP); 89 | } 90 | }; 91 | -------------------------------------------------------------------------------- /octopus/blocktopus/blockly/colourscheme.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | export const PROCEDURE_CATEGORY_HUE = 290; 4 | export const CONTROL_CATEGORY_HUE = 120; 5 | export const LOGIC_CATEGORY_HUE = 210; 6 | export const MATH_CATEGORY_HUE = { r: 63, g: 113, b: 181 }; //230; 7 | export const TEXT_CATEGORY_HUE = { r: 179, g: 45, b: 94 }; //160; 8 | export const VARIABLES_CATEGORY_HUE = { r: 208, g: 95, b: 45 }; // 330; 9 | export const MACHINES_CATEGORY_HUE = 0; 10 | export const CONNECTIONS_CATEGORY_HUE = 60; 11 | -------------------------------------------------------------------------------- /octopus/blocktopus/blockly/constants.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | export const VARIABLES_NAME_TYPE = "VARIABLES"; 4 | export const PROCEDURES_NAME_TYPE = "PROCEDURES"; 5 | 6 | export const MACHINE_NAME_PREFIX = "machine"; // Todo - extract to lang 7 | //Blockly.showPrefixToUser = true; 8 | export const GLOBAL_NAME_PREFIX = "global"; // For names introduced by global variable declarations 9 | //Blockly.procedureParameterPrefix = "input"; // For names introduced by procedure/function declarations 10 | //Blockly.handlerParameterPrefix = "input"; // For names introduced by event handlers 11 | //Blockly.localNamePrefix = "local"; // For names introduced by local variable declarations 12 | //Blockly.loopParameterPrefix = "item"; // For names introduced by for loops 13 | //Blockly.loopRangeParameterPrefix = "counter"; // For names introduced by for range loops 14 | -------------------------------------------------------------------------------- /octopus/blocktopus/blockly/core/blocks.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @license 3 | * Visual Blocks Editor 4 | * 5 | * Copyright 2013 Google Inc. 6 | * https://github.com/google/blockly 7 | * 8 | * Licensed under the Apache License, Version 2.0 (the "License"); 9 | * you may not use this file except in compliance with the License. 10 | * You may obtain a copy of the License at 11 | * 12 | * http://www.apache.org/licenses/LICENSE-2.0 13 | * 14 | * Unless required by applicable law or agreed to in writing, software 15 | * distributed under the License is distributed on an "AS IS" BASIS, 16 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 17 | * See the License for the specific language governing permissions and 18 | * limitations under the License. 19 | */ 20 | 21 | /** 22 | * @fileoverview Flexible templating system for defining blocks. 23 | * @author spertus@google.com (Ellen Spertus) 24 | */ 25 | 'use strict'; 26 | 27 | /** 28 | * Name space for the Blocks singleton. 29 | * Blocks gets populated in the blocks files, possibly through calls to 30 | * Blocks.addTemplate(). 31 | */ 32 | var Blocks = {}; 33 | export default Blocks; 34 | -------------------------------------------------------------------------------- /octopus/blocktopus/blockly/core/contextmenu.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @license 3 | * Visual Blocks Editor 4 | * 5 | * Copyright 2011 Google Inc. 6 | * https://github.com/google/blockly 7 | * 8 | * Licensed under the Apache License, Version 2.0 (the "License"); 9 | * you may not use this file except in compliance with the License. 10 | * You may obtain a copy of the License at 11 | * 12 | * http://www.apache.org/licenses/LICENSE-2.0 13 | * 14 | * Unless required by applicable law or agreed to in writing, software 15 | * distributed under the License is distributed on an "AS IS" BASIS, 16 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 17 | * See the License for the specific language governing permissions and 18 | * limitations under the License. 19 | */ 20 | 21 | /** 22 | * @fileoverview Functionality for the right-click context menus. 23 | * @author fraser@google.com (Neil Fraser) 24 | */ 25 | 'use strict'; 26 | 27 | import Blockly from './blockly'; 28 | import Xml from './xml'; 29 | import WidgetDiv from './widgetdiv'; 30 | 31 | var Menu = {}; 32 | export default Menu; 33 | 34 | /** 35 | * Which block is the context menu attached to? 36 | * @type {Blockly.Block} 37 | */ 38 | Menu.currentBlock = null; 39 | 40 | /** 41 | * Construct the menu based on the list of options and show the menu. 42 | * @param {!Event} e Mouse event. 43 | * @param {!Array.} options Array of menu options. 44 | */ 45 | Menu.show = function(e, options) { 46 | WidgetDiv.show(Menu, function () { 47 | if (this.menu) { 48 | this.menu.closemenu(); 49 | } 50 | }.bind(this)); 51 | 52 | if (!options.length) { 53 | Menu.hide(); 54 | return; 55 | } 56 | 57 | this.menu = new ContextMenu(options); 58 | this.menu.showAtEvent(e); 59 | 60 | /* Here's what one option object looks like: 61 | {text: 'Make It So', 62 | enabled: true, 63 | callback: Blockly.MakeItSo} 64 | */ 65 | 66 | /* ??? 67 | menu.setAllowAutoFocus(true); 68 | // 1ms delay is required for focusing on context menus because some other 69 | // mouse event is still waiting in the queue and clears focus. 70 | setTimeout(function() {menuDom.focus();}, 1);*/ 71 | 72 | Menu.currentBlock = null; // May be set by Blockly.Block. 73 | 74 | }; 75 | 76 | /** 77 | * Hide the context menu. 78 | */ 79 | Menu.hide = function() { 80 | if (this.menu) { 81 | this.menu.closemenu(); 82 | } 83 | this.menu = null; 84 | WidgetDiv.hideIfOwner(Menu); 85 | Menu.currentBlock = null; 86 | }; 87 | 88 | /** 89 | * Create a callback function that creates and configures a block, 90 | * then places the new block next to the original. 91 | * @param {!Blockly.Block} block Original block. 92 | * @param {!Element} xml XML representation of new block. 93 | * @return {!Function} Function that creates a block. 94 | */ 95 | Menu.callbackFactory = function(block, xml) { 96 | return function() { 97 | var newBlock = Xml.domToBlock(block.workspace, xml); 98 | // Move the new block next to the old block. 99 | var xy = block.getRelativeToSurfaceXY(); 100 | if (Blockly.RTL) { 101 | xy.x -= Blockly.SNAP_RADIUS; 102 | } else { 103 | xy.x += Blockly.SNAP_RADIUS; 104 | } 105 | xy.y += Blockly.SNAP_RADIUS * 2; 106 | newBlock.moveBy(xy.x, xy.y); 107 | newBlock.select(); 108 | }; 109 | }; 110 | -------------------------------------------------------------------------------- /octopus/blocktopus/blockly/core/field_checkbox.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @license 3 | * Visual Blocks Editor 4 | * 5 | * Copyright 2012 Google Inc. 6 | * https://github.com/google/blockly 7 | * 8 | * Licensed under the Apache License, Version 2.0 (the "License"); 9 | * you may not use this file except in compliance with the License. 10 | * You may obtain a copy of the License at 11 | * 12 | * http://www.apache.org/licenses/LICENSE-2.0 13 | * 14 | * Unless required by applicable law or agreed to in writing, software 15 | * distributed under the License is distributed on an "AS IS" BASIS, 16 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 17 | * See the License for the specific language governing permissions and 18 | * limitations under the License. 19 | */ 20 | 21 | /** 22 | * @fileoverview Checkbox field. Checked or not checked. 23 | * @author fraser@google.com (Neil Fraser) 24 | */ 25 | 'use strict'; 26 | 27 | import Blockly from './blockly'; 28 | import Field from './field'; 29 | import {createSvgElement} from './utils'; 30 | import {inherits} from './utils'; 31 | 32 | /** 33 | * Class for a checkbox field. 34 | * @param {string} state The initial state of the field ('TRUE' or 'FALSE'). 35 | * @param {Function} opt_changeHandler A function that is executed when a new 36 | * option is selected. Its sole argument is the new checkbox state. If 37 | * it returns a value, this becomes the new checkbox state, unless the 38 | * value is null, in which case the change is aborted. 39 | * @extends {Field} 40 | * @constructor 41 | */ 42 | var FieldCheckbox = function(state, opt_changeHandler) { 43 | FieldCheckbox.super_.call(this, ''); 44 | 45 | this.changeHandler_ = opt_changeHandler; 46 | // The checkbox doesn't use the inherited text element. 47 | // Instead it uses a custom checkmark element that is either visible or not. 48 | this.checkElement_ = createSvgElement('text', 49 | {'class': 'blocklyText', 'x': -3}, this.fieldGroup_); 50 | var textNode = document.createTextNode('\u2713'); 51 | this.checkElement_.appendChild(textNode); 52 | // Set the initial state. 53 | this.setValue(state); 54 | }; 55 | inherits(FieldCheckbox, Field); 56 | export default Field; 57 | 58 | /** 59 | * Clone this FieldCheckbox. 60 | * @return {!FieldCheckbox} The result of calling the constructor again 61 | * with the current values of the arguments used during construction. 62 | */ 63 | FieldCheckbox.prototype.clone = function() { 64 | return new FieldCheckbox(this.getValue(), this.changeHandler_); 65 | }; 66 | 67 | /** 68 | * Mouse cursor style when over the hotspot that initiates editability. 69 | */ 70 | FieldCheckbox.prototype.CURSOR = 'default'; 71 | 72 | /** 73 | * Return 'TRUE' if the checkbox is checked, 'FALSE' otherwise. 74 | * @return {string} Current state. 75 | */ 76 | FieldCheckbox.prototype.getValue = function() { 77 | return String(this.state_).toUpperCase(); 78 | }; 79 | 80 | /** 81 | * Set the checkbox to be checked if strBool is 'TRUE', unchecks otherwise. 82 | * @param {string} strBool New state. 83 | */ 84 | FieldCheckbox.prototype.setValue = function(strBool) { 85 | var newState = (strBool == 'TRUE'); 86 | if (this.state_ !== newState) { 87 | this.state_ = newState; 88 | this.checkElement_.style.display = newState ? 'block' : 'none'; 89 | if (this.sourceBlock_ && this.sourceBlock_.rendered) { 90 | this.sourceBlock_.workspace.fireChangeEvent(); 91 | } 92 | } 93 | thisField.emit("changed", newState); 94 | }; 95 | 96 | /** 97 | * Toggle the state of the checkbox. 98 | * @private 99 | */ 100 | FieldCheckbox.prototype.showEditor_ = function() { 101 | var newState = !this.state_; 102 | if (this.changeHandler_) { 103 | // Call any change handler, and allow it to override. 104 | var override = this.changeHandler_(newState); 105 | if (override !== undefined) { 106 | newState = override; 107 | } 108 | } 109 | if (newState !== null) { 110 | this.setValue(String(newState).toUpperCase()); 111 | } 112 | }; 113 | -------------------------------------------------------------------------------- /octopus/blocktopus/blockly/core/field_global_flydown.js: -------------------------------------------------------------------------------- 1 | // -*- mode: java; c-basic-offset: 2; -*- 2 | // Copyright 2013-2014 MIT, All rights reserved 3 | // Released under the MIT License https://raw.github.com/mit-cml/app-inventor/master/mitlicense.txt 4 | /** 5 | * @license 6 | * @fileoverview Clickable field with flydown menu of global getter and setter blocks. 7 | * @author fturbak@wellesley.edu (Lyn Turbak) 8 | */ 9 | 10 | 'use strict'; 11 | 12 | import Blockly from './blockly'; 13 | import FieldFlydown from './field_flydown'; 14 | import {inherits} from './utils'; 15 | import {GLOBAL_NAME_PREFIX} from '../constants'; 16 | 17 | /** 18 | * Class for a clickable global variable declaration field. 19 | * @param {string} text The initial parameter name in the field. 20 | * @extends {Field} 21 | * @constructor 22 | */ 23 | var FieldGlobalFlydown = function(name, isEditable, displayLocation, changeHandler) { 24 | FieldGlobalFlydown.super_.call(this, name, isEditable, displayLocation, changeHandler); 25 | }; 26 | inherits(FieldGlobalFlydown, FieldFlydown); 27 | export default FieldGlobalFlydown; 28 | 29 | FieldGlobalFlydown.prototype.fieldCSSClassName = 'blocklyFieldParameter'; 30 | 31 | FieldGlobalFlydown.prototype.flyoutCSSClassName = 'blocklyFieldParameterFlydown'; 32 | 33 | /** 34 | * Block creation menu for global variables 35 | * Returns a list of two XML elements: a getter block for name and a setter block for this parameter field. 36 | * @return {!Array.} List of two XML elements. 37 | **/ 38 | FieldGlobalFlydown.prototype.flydownBlocksXML_ = function() { 39 | var name, v = this.sourceBlock_.variable_; 40 | if (v) { 41 | name = v.getDisplay() + '@@' + v.getName(); 42 | } else { 43 | name = GLOBAL_NAME_PREFIX + " " + this.getText(); // global name for this parameter field. 44 | } 45 | var getterSetterXML = 46 | '' + 47 | '' + 48 | '' + 49 | name + 50 | '' + 51 | '' + 52 | '' + 53 | '' + 54 | name + 55 | '' + 56 | '' + 57 | ''; 58 | return getterSetterXML; 59 | }; 60 | -------------------------------------------------------------------------------- /octopus/blocktopus/blockly/core/field_label.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @license 3 | * Visual Blocks Editor 4 | * 5 | * Copyright 2012 Google Inc. 6 | * https://github.com/google/blockly 7 | * 8 | * Licensed under the Apache License, Version 2.0 (the "License"); 9 | * you may not use this file except in compliance with the License. 10 | * You may obtain a copy of the License at 11 | * 12 | * http://www.apache.org/licenses/LICENSE-2.0 13 | * 14 | * Unless required by applicable law or agreed to in writing, software 15 | * distributed under the License is distributed on an "AS IS" BASIS, 16 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 17 | * See the License for the specific language governing permissions and 18 | * limitations under the License. 19 | */ 20 | 21 | /** 22 | * @fileoverview Non-editable text field. Used for titles, labels, etc. 23 | * @author fraser@google.com (Neil Fraser) 24 | */ 25 | 'use strict'; 26 | 27 | import Blockly from './blockly'; 28 | import BlockSvg from './block_svg'; 29 | import Field from './field'; 30 | import Tooltip from './tooltip'; 31 | import {createSvgElement} from './utils'; 32 | import {inherits} from './utils'; 33 | 34 | /** 35 | * Class for a non-editable field. 36 | * @param {string} text The initial content of the field. 37 | * @param {string|Array} extraClass Any additional classes to add to the field. 38 | * @extends {Field} 39 | * @constructor 40 | */ 41 | var FieldLabel = function(text, extraClass) { 42 | extraClass = extraClass || ''; 43 | if (Array.isArray(extraClass)) { 44 | extraClass = extraClass.join(' '); 45 | } 46 | 47 | this.sourceBlock_ = null; 48 | // Build the DOM. 49 | this.textElement_ = createSvgElement('text', 50 | {'class': 'blocklyText ' + extraClass}, null); 51 | this.size_ = {height: 25, width: 0}; 52 | this.setText(text); 53 | }; 54 | inherits(FieldLabel, Field); 55 | export default FieldLabel; 56 | 57 | /** 58 | * Clone this FieldLabel. 59 | * @return {!FieldLabel} The result of calling the constructor again 60 | * with the current values of the arguments used during construction. 61 | */ 62 | FieldLabel.prototype.clone = function() { 63 | return new FieldLabel(this.getText()); 64 | }; 65 | 66 | /** 67 | * Editable fields are saved by the XML renderer, non-editable fields are not. 68 | */ 69 | FieldLabel.prototype.EDITABLE = false; 70 | 71 | /** 72 | * Install this text on a block. 73 | * @param {!Block} block The block containing this text. 74 | */ 75 | FieldLabel.prototype.init = function(block) { 76 | if (this.sourceBlock_) { 77 | throw 'Text has already been initialized once.'; 78 | } 79 | this.sourceBlock_ = block; 80 | block.getSvgRoot().appendChild(this.textElement_); 81 | 82 | // Configure the field to be transparent with respect to tooltips. 83 | this.textElement_.tooltip = this.sourceBlock_; 84 | Tooltip.bindMouseEvents(this.textElement_); 85 | }; 86 | 87 | /** 88 | * Dispose of all DOM objects belonging to this text. 89 | */ 90 | FieldLabel.prototype.dispose = function() { 91 | var node = this.textElement_ 92 | if (node && node.parentNode) node.parentNode.removeChild(node); 93 | this.textElement_ = null; 94 | }; 95 | 96 | /** 97 | * Gets the group element for this field. 98 | * Used for measuring the size and for positioning. 99 | * @return {!Element} The group element. 100 | */ 101 | FieldLabel.prototype.getRootElement = function() { 102 | return /** @type {!Element} */ (this.textElement_); 103 | }; 104 | 105 | /** 106 | * Change the tooltip text for this field. 107 | * @param {string|!Element} newTip Text for tooltip or a parent element to 108 | * link to for its tooltip. 109 | */ 110 | FieldLabel.prototype.setTooltip = function(newTip) { 111 | this.textElement_.tooltip = newTip; 112 | }; 113 | -------------------------------------------------------------------------------- /octopus/blocktopus/blockly/core/field_lexical_parameter_flydown.js: -------------------------------------------------------------------------------- 1 | // -*- mode: java; c-basic-offset: 2; -*- 2 | /** 3 | * @license 4 | * @fileoverview Clickable field with flydown menu of local parameter getter blocks. 5 | * @author fturbak@wellesley.edu (Lyn Turbak) 6 | * @author mail@richardingham.net (Richard Ingham) 7 | */ 8 | 9 | 'use strict'; 10 | 11 | import Blockly from './blockly'; 12 | import FieldFlydown from './field_flydown'; 13 | import {inherits} from './utils'; 14 | 15 | /** 16 | * Class for a clickable global variable declaration field. 17 | * @param {string} text The initial parameter name in the field. 18 | * @extends {FieldFlydown} 19 | * @constructor 20 | */ 21 | export default function FieldLexicalParameterFlydown (name, isEditable, displayLocation, changeHandler) { 22 | FieldLexicalParameterFlydown.super_.call(this, name, isEditable, displayLocation, 23 | // rename all references to this global variable 24 | changeHandler) 25 | }; 26 | inherits(FieldLexicalParameterFlydown, FieldFlydown); 27 | 28 | FieldLexicalParameterFlydown.prototype.fieldCSSClassName = 'blocklyFieldParameter'; 29 | 30 | FieldLexicalParameterFlydown.prototype.flyoutCSSClassName = 'blocklyFieldParameterFlydown'; 31 | 32 | /** 33 | * Block creation menu for global variables 34 | * Returns a list of two XML elements: a getter block for name and a setter block for this parameter field. 35 | * @return {!Array.} List of two XML elements. 36 | **/ 37 | FieldLexicalParameterFlydown.prototype.flydownBlocksXML_ = function() { 38 | var name, v = this.sourceBlock_.variable_; 39 | if (v) { 40 | name = v.getDisplay() + '@@' + v.getName(); 41 | } else { 42 | name = this.getText(); 43 | } 44 | var getterSetterXML = 45 | '' + 46 | '' + 47 | '' + 48 | name + 49 | '' + 50 | '' + 51 | ''; 52 | return getterSetterXML; 53 | }; 54 | -------------------------------------------------------------------------------- /octopus/blocktopus/blockly/core/field_machine_flydown.js: -------------------------------------------------------------------------------- 1 | // -*- mode: java; c-basic-offset: 2; -*- 2 | /** 3 | * @license 4 | * @fileoverview Clickable field with flydown menu of machine getter blocks. 5 | * @author fturbak@wellesley.edu (Lyn Turbak) 6 | * @author mail@richardingham.net (Richard Ingham) 7 | */ 8 | 9 | 'use strict'; 10 | 11 | import Blockly from './blockly'; 12 | import FieldFlydown from './field_flydown'; 13 | import {inherits} from './utils'; 14 | import {MACHINE_NAME_PREFIX} from '../constants'; 15 | 16 | /** 17 | * Class for a clickable global variable declaration field. 18 | * @param {string} text The initial parameter name in the field. 19 | * @extends {FieldFlydown} 20 | * @constructor 21 | */ 22 | export default function FieldMachineFlydown (name, isEditable, displayLocation, changeHandler) { 23 | FieldMachineFlydown.super_.call(this, name, isEditable, displayLocation, 24 | // rename all references to this global variable 25 | changeHandler) 26 | }; 27 | inherits(FieldMachineFlydown, FieldFlydown); 28 | 29 | FieldMachineFlydown.prototype.fieldCSSClassName = 'blocklyFieldParameter'; 30 | 31 | FieldMachineFlydown.prototype.flyoutCSSClassName = 'blocklyFieldParameterFlydown'; 32 | 33 | /** 34 | * Block creation menu for global variables 35 | * Returns a list of two XML elements: a getter block for name and a setter block for this parameter field. 36 | * @return {!Array.} List of two XML elements. 37 | **/ 38 | FieldMachineFlydown.prototype.flydownBlocksXML_ = function() { 39 | var name, v = this.sourceBlock_.variable_; 40 | if (v) { 41 | name = v.getDisplay() + '@@' + v.getName(); 42 | } else { 43 | name = MACHINE_NAME_PREFIX + " " + this.getText(); // global name for this parameter field. 44 | } 45 | var getterSetterXML = 46 | '' + 47 | '' + 48 | '' + 49 | name + 50 | '' + 51 | '' + 52 | ''; 53 | return getterSetterXML; 54 | }; 55 | -------------------------------------------------------------------------------- /octopus/blocktopus/blockly/core/msg.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @license 3 | * Visual Blocks Editor 4 | * 5 | * Copyright 2013 Google Inc. 6 | * https://github.com/google/blockly 7 | * 8 | * Licensed under the Apache License, Version 2.0 (the "License"); 9 | * you may not use this file except in compliance with the License. 10 | * You may obtain a copy of the License at 11 | * 12 | * http://www.apache.org/licenses/LICENSE-2.0 13 | * 14 | * Unless required by applicable law or agreed to in writing, software 15 | * distributed under the License is distributed on an "AS IS" BASIS, 16 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 17 | * See the License for the specific language governing permissions and 18 | * limitations under the License. 19 | */ 20 | 21 | /** 22 | * @fileoverview Core JavaScript library for Blockly. 23 | * @author scr@google.com (Sheridan Rawlins) 24 | */ 25 | 'use strict'; 26 | 27 | var Msg = {}; 28 | 29 | export default Msg; 30 | -------------------------------------------------------------------------------- /octopus/blocktopus/blockly/core/toolbox.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @license 3 | * Visual Blocks Editor 4 | * 5 | * Copyright 2011 Google Inc. 6 | * https://github.com/google/blockly 7 | * 8 | * Licensed under the Apache License, Version 2.0 (the "License"); 9 | * you may not use this file except in compliance with the License. 10 | * You may obtain a copy of the License at 11 | * 12 | * http://www.apache.org/licenses/LICENSE-2.0 13 | * 14 | * Unless required by applicable law or agreed to in writing, software 15 | * distributed under the License is distributed on an "AS IS" BASIS, 16 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 17 | * See the License for the specific language governing permissions and 18 | * limitations under the License. 19 | */ 20 | 21 | /** 22 | * @fileoverview Toolbox from whence to create blocks. 23 | * @author fraser@google.com (Neil Fraser) 24 | */ 25 | 'use strict'; 26 | 27 | import Blockly from './blockly'; 28 | import Flyout from './flyout'; 29 | 30 | var Toolbox = {}; 31 | export default Toolbox; 32 | 33 | /** 34 | * Width of the toolbox. 35 | * @type {number} 36 | */ 37 | Toolbox.width = 0; 38 | 39 | /** 40 | * Creates the toolbox's DOM. Only needs to be called once. 41 | * @param {!Element} svg The top-level SVG element. 42 | * @param {!Element} container The SVG's HTML parent element. 43 | */ 44 | Toolbox.createDom = function(svg, container) { 45 | /** 46 | * @type {!Flyout} 47 | * @private 48 | */ 49 | Toolbox.flyout_ = new Flyout(); 50 | svg.appendChild(Toolbox.flyout_.createDom()); 51 | }; 52 | 53 | /** 54 | * Initializes the toolbox. 55 | */ 56 | Toolbox.init = function() { 57 | Toolbox.flyout_.init(Blockly.mainWorkspace); 58 | Toolbox.populate_(); 59 | Toolbox.width = -1; 60 | }; 61 | 62 | Toolbox.populate_ = function () {}; 63 | -------------------------------------------------------------------------------- /octopus/blocktopus/blockly/core/validators.js: -------------------------------------------------------------------------------- 1 | 2 | 'use strict'; 3 | 4 | export function numberValidator(text, integer, nonnegative) { 5 | // TODO: Handle cases like 'ten', '1.203,14', etc. 6 | // 'O' is sometimes mistaken for '0' by inexperienced users. 7 | text = text.replace(/O/ig, '0'); 8 | // Strip out thousands separators. 9 | text = text.replace(/,/g, ''); 10 | 11 | var n = parseFloat(text); 12 | if (isNaN(n)) return null; 13 | 14 | if (nonnegative) { 15 | n = Math.max(0, n); 16 | } 17 | 18 | if (integer) { 19 | return String(Math.floor(n)); 20 | } else { 21 | var s = String(n); 22 | if (s.indexOf('.') === -1 && text.indexOf('.') !== -1 && parseInt(text || 0) === n) { 23 | return s + '.0'; 24 | } 25 | return s; 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /octopus/blocktopus/blockly/core/widgetdiv.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @license 3 | * Visual Blocks Editor 4 | * 5 | * Copyright 2013 Google Inc. 6 | * https://github.com/google/blockly 7 | * 8 | * Licensed under the Apache License, Version 2.0 (the "License"); 9 | * you may not use this file except in compliance with the License. 10 | * You may obtain a copy of the License at 11 | * 12 | * http://www.apache.org/licenses/LICENSE-2.0 13 | * 14 | * Unless required by applicable law or agreed to in writing, software 15 | * distributed under the License is distributed on an "AS IS" BASIS, 16 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 17 | * See the License for the specific language governing permissions and 18 | * limitations under the License. 19 | */ 20 | 21 | /** 22 | * @fileoverview A div that floats on top of Blockly. This singleton contains 23 | * temporary HTML UI widgets that the user is currently interacting with. 24 | * E.g. text input areas, colour pickers, context menus. 25 | * @author fraser@google.com (Neil Fraser) 26 | */ 27 | 'use strict'; 28 | 29 | import Blockly from './blockly'; 30 | 31 | var WidgetDiv = {}; 32 | export default WidgetDiv; 33 | 34 | /** 35 | * The HTML container. Set once by inject.js's Blockly.createDom_. 36 | * @type Element 37 | */ 38 | WidgetDiv.DIV = null; 39 | 40 | /** 41 | * The object currently using this container. 42 | * @private 43 | * @type Object 44 | */ 45 | WidgetDiv.owner_ = null; 46 | 47 | /** 48 | * Optional cleanup function set by whichever object uses the widget. 49 | * @private 50 | * @type Function 51 | */ 52 | WidgetDiv.dispose_ = null; 53 | 54 | /** 55 | * Initialize and display the widget div. Close the old one if needed. 56 | * @param {!Object} newOwner The object that will be using this container. 57 | * @param {Function} dispose Optional cleanup function to be run when the widget 58 | * is closed. 59 | */ 60 | WidgetDiv.show = function(newOwner, dispose) { 61 | WidgetDiv.hide(); 62 | WidgetDiv.owner_ = newOwner; 63 | WidgetDiv.dispose_ = dispose; 64 | WidgetDiv.DIV.style.display = 'block'; 65 | }; 66 | 67 | /** 68 | * Destroy the widget and hide the div. 69 | */ 70 | WidgetDiv.hide = function() { 71 | if (WidgetDiv.owner_) { 72 | WidgetDiv.DIV.style.display = 'none'; 73 | WidgetDiv.dispose_ && WidgetDiv.dispose_(); 74 | WidgetDiv.owner_ = null; 75 | WidgetDiv.dispose_ = null; 76 | 77 | var child, node = WidgetDiv.DIV; 78 | while ((child = node.firstChild)) { 79 | node.removeChild(child); 80 | } 81 | } 82 | }; 83 | 84 | /** 85 | * Is the container visible? 86 | * @return {boolean} True if visible. 87 | */ 88 | WidgetDiv.isVisible = function() { 89 | return !!WidgetDiv.owner_; 90 | }; 91 | 92 | /** 93 | * Destroy the widget and hide the div if it is being used by the specified 94 | * object. 95 | * @param {!Object} oldOwner The object that was using this container. 96 | */ 97 | WidgetDiv.hideIfOwner = function(oldOwner) { 98 | if (WidgetDiv.owner_ == oldOwner) { 99 | WidgetDiv.hide(); 100 | } 101 | }; 102 | 103 | /** 104 | * Position the widget at a given location. Prevent the widget from going 105 | * offscreen top or left (right in RTL). 106 | * @param {number} anchorX Horizontal location (window coorditates, not body). 107 | * @param {number} anchorY Vertical location (window coorditates, not body). 108 | * @param {!goog.math.Size} windowSize Height/width of window. 109 | * @param {!goog.math.Coordinate} scrollOffset X/y of window scrollbars. 110 | */ 111 | WidgetDiv.position = function(anchorX, anchorY, windowSize, 112 | scrollOffset) { 113 | // Don't let the widget go above the top edge of the window. 114 | if (anchorY < scrollOffset.y) { 115 | anchorY = scrollOffset.y; 116 | } 117 | if (Blockly.RTL) { 118 | // Don't let the menu go right of the right edge of the window. 119 | if (anchorX > windowSize.width + scrollOffset.x) { 120 | anchorX = windowSize.width + scrollOffset.x; 121 | } 122 | } else { 123 | // Don't let the widget go left of the left edge of the window. 124 | if (anchorX < scrollOffset.x) { 125 | anchorX = scrollOffset.x; 126 | } 127 | } 128 | WidgetDiv.DIV.style.left = anchorX + 'px'; 129 | WidgetDiv.DIV.style.top = anchorY + 'px'; 130 | }; 131 | -------------------------------------------------------------------------------- /octopus/blocktopus/blockly/generators/python-octo-blocks.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var Blocks = {}; 4 | 5 | export default Blocks; 6 | -------------------------------------------------------------------------------- /octopus/blocktopus/blockly/generators/python-octo-constants.js: -------------------------------------------------------------------------------- 1 | 2 | export const GENERATOR_NAME = 'Octopus Python'; 3 | 4 | /** 5 | * Order of operation ENUMs. 6 | * http://docs.python.org/reference/expressions.html#summary 7 | */ 8 | export const ORDER = { 9 | ATOMIC: 0, // 0 "" ... 10 | COLLECTION: 1, // tuples, lists, dictionaries 11 | STRING_CONVERSION: 1, // `expression...` 12 | MEMBER: 2, // . [] 13 | FUNCTION_CALL: 2, // () 14 | EXPONENTIATION: 3, // ** 15 | UNARY_SIGN: 4, // + - 16 | BITWISE_NOT: 4, // ~ 17 | MULTIPLICATIVE: 5, // * / // % 18 | ADDITIVE: 6, // + - 19 | BITWISE_SHIFT: 7, // << >> 20 | BITWISE_AND: 8, // & 21 | BITWISE_XOR: 9, // ^ 22 | BITWISE_OR: 10, // | 23 | RELATIONAL: 11, // in, not in, is, is not, 24 | // <, <=, >, >=, <>, !=, == 25 | LOGICAL_NOT: 12, // not 26 | LOGICAL_AND: 13, // and 27 | LOGICAL_OR: 14, // or 28 | CONDITIONAL: 15, // if else 29 | LAMBDA: 16, // lambda 30 | NONE: 99, // (...) 31 | }; 32 | 33 | /** 34 | * The method of indenting. Defaults to two spaces, but language generators 35 | * may override this to increase indent or change to tabs. 36 | */ 37 | export const INDENT = ' '; 38 | 39 | /** 40 | * This is used as a placeholder in functions defined using 41 | * Generator.provideFunction_. It must not be legal code that could 42 | * legitimately appear in a function definition (or comment), and it must 43 | * not confuse the regular expression parser. 44 | * @private 45 | */ 46 | export const FUNCTION_NAME_PLACEHOLDER_ = '{leCUI8hutHZI4480Dc}'; 47 | export const FUNCTION_NAME_PLACEHOLDER_REGEXP_ = new RegExp(FUNCTION_NAME_PLACEHOLDER_, 'g'); 48 | 49 | /** 50 | * Category to separate generated function names from variables and procedures. 51 | */ 52 | export const NAME_TYPE = 'generated_function'; 53 | 54 | /** 55 | * Arbitrary code to inject into locations that risk causing infinite loops. 56 | * Any instances of '%1' will be replaced by the block ID that failed. 57 | * E.g. ' checkTimeout(%1);\n' 58 | * @type ?string 59 | */ 60 | export const INFINITE_LOOP_TRAP = null; 61 | 62 | /** 63 | * Arbitrary code to inject before every statement. 64 | * Any instances of '%1' will be replaced by the block ID of the statement. 65 | * E.g. 'highlight(%1);\n' 66 | * @type ?string 67 | */ 68 | export const STATEMENT_PREFIX = null; 69 | -------------------------------------------------------------------------------- /octopus/blocktopus/blockly/generators/python-octo.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @license 3 | * MIT 4 | */ 5 | 6 | 'use strict'; 7 | 8 | import Names from '../core/names'; 9 | import {addReservedWords, initDefinitions, getDefinitions, blockToCode, makeSequence, scrubNakedValue} from './python-octo-methods'; 10 | 11 | // import keyword 12 | // print ','.join(keyword.kwlist) 13 | addReservedWords( 14 | // http://docs.python.org/reference/lexical_analysis.html#keywords 15 | 'and,as,assert,break,class,continue,def,del,elif,else,except,exec,finally,for,from,global,if,import,in,is,lambda,not,or,pass,print,raise,return,try,while,with,yield,' + 16 | //http://docs.python.org/library/constants.html 17 | 'True,False,None,NotImplemented,Ellipsis,__debug__,quit,exit,copyright,license,credits,' + 18 | // http://docs.python.org/library/functions.html 19 | 'abs,divmod,input,open,staticmethod,all,enumerate,int,ord,str,any,eval,isinstance,pow,sum,basestring,execfile,issubclass,print,super,bin,file,iter,property,tuple,bool,filter,len,range,type,bytearray,float,list,raw_input,unichr,callable,format,locals,reduce,unicode,chr,frozenset,long,reload,vars,classmethod,getattr,map,repr,xrange,cmp,globals,max,reversed,zip,compile,hasattr,memoryview,round,__import__,complex,hash,min,set,apply,delattr,help,next,setattr,buffer,dict,hex,object,slice,coerce,dir,id,oct,sorted,intern' 20 | ); 21 | 22 | /** 23 | * Prepend the generated code with the variable definitions. 24 | * @param {string} code Generated code. 25 | * @return {string} Completed code. 26 | */ 27 | function finish (code) { 28 | // Convert the definitions dictionary into a list. 29 | var imports = []; 30 | var contextDefinitions = getDefinitions(); 31 | var definitions = []; 32 | //var machines = []; 33 | for (var name in contextDefinitions) { 34 | var def = contextDefinitions[name]; 35 | if (def.match && def.match(/^(from\s+\S+\s+)?import\s+\S+/)) { 36 | imports.push(def); 37 | } else { 38 | if (def.join) { 39 | def = def.join("\n"); 40 | } 41 | definitions.push(def); 42 | } 43 | } 44 | var allDefs = imports.join('\n') + '\n\n' + definitions.join('\n\n'); 45 | return allDefs.replace(/\n\n+/g, '\n\n').replace(/\n*$/, '\n\n\n') + code; 46 | }; 47 | 48 | /** 49 | * Generate code for all blocks in the workspace to the specified language. 50 | * @return {string} Generated code. 51 | */ 52 | export function workspaceToCode (blocks) { 53 | var code = []; 54 | initDefinitions(); 55 | for (var x = 0, block; block = blocks[x]; x++) { 56 | var line = blockToCode(block); 57 | if (Array.isArray(line) && line.length === 2 && typeof line[1] === "number") { 58 | // Value blocks return tuples of code and operator order. 59 | // Top-level blocks don't care about operator order. 60 | line = line[0]; 61 | } 62 | if (line) { 63 | line = makeSequence(line); 64 | if (block.outputConnection && scrubNakedValue) { 65 | // This block is a naked value. Ask the language's code generator if 66 | // it wants to append a semicolon, or something. 67 | line = scrubNakedValue(line); 68 | } 69 | code.push(line); 70 | } 71 | } 72 | code = code.join('\n\n'); // Blank line between each section. 73 | code = finish(code); 74 | // Final scrubbing of whitespace. 75 | code = code.replace(/^\s+\n/, ''); 76 | code = code.replace(/\n\s+$/, '\n'); 77 | code = code.replace(/[ \t]+\n/g, '\n'); 78 | return code; 79 | }; 80 | -------------------------------------------------------------------------------- /octopus/blocktopus/blockly/generators/python-octo/colour.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @license 3 | * Visual Blocks Language 4 | * 5 | * Copyright 2012 Google Inc. 6 | * https://developers.google.com/blockly/ 7 | * 8 | * Licensed under the Apache License, Version 2.0 (the "License"); 9 | * you may not use this file except in compliance with the License. 10 | * You may obtain a copy of the License at 11 | * 12 | * http://www.apache.org/licenses/LICENSE-2.0 13 | * 14 | * Unless required by applicable law or agreed to in writing, software 15 | * distributed under the License is distributed on an "AS IS" BASIS, 16 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 17 | * See the License for the specific language governing permissions and 18 | * limitations under the License. 19 | */ 20 | 21 | /** 22 | * @fileoverview Generating Python for colour blocks. 23 | * @author fraser@google.com (Neil Fraser) 24 | */ 25 | 'use strict'; 26 | 27 | import {ORDER} from '../python-octo-constants'; 28 | import PythonOcto from '../python-octo-blocks'; 29 | 30 | PythonOcto['colour_picker'] = function(block) { 31 | // Colour picker. 32 | var code = '\'' + block.getFieldValue('COLOUR') + '\''; 33 | return [code, ORDER.ATOMIC]; 34 | }; 35 | -------------------------------------------------------------------------------- /octopus/blocktopus/blockly/generators/python-octo/connections.js: -------------------------------------------------------------------------------- 1 | import {ORDER} from '../python-octo-constants'; 2 | import {getVariableName, addDefinition, quote} from '../python-octo-methods'; 3 | import PythonOcto from '../python-octo-blocks'; 4 | 5 | PythonOcto['connection_tcp'] = function (block) { 6 | var host = block.getFieldValue('HOST'); 7 | var port = parseInt(block.getFieldValue('PORT')); 8 | var code = 'tcp(' + quote(host) + ', ' + port + ')'; 9 | return [code, ORDER.FUNCTION_CALL]; 10 | }; 11 | 12 | PythonOcto['connection_serial'] = function(block) { 13 | var port = block.getFieldValue('PORT'); 14 | var baud = parseInt(block.getFieldValue('BAUD')); 15 | var code = 'serial(' + quote(port) + ', baudrate = ' + baud + ')'; 16 | return [code, ORDER.FUNCTION_CALL]; 17 | }; 18 | 19 | PythonOcto['connection_phidget'] = function(block) { 20 | addDefinition('import_transport_basic_phidget', 'from octopus.transport.basic import Phidget'); 21 | var id = parseInt(block.getFieldValue('ID')); 22 | var code = 'Phidget(' + id + ')'; 23 | return [code, ORDER.FUNCTION_CALL]; 24 | }; 25 | 26 | PythonOcto['connection_cvcamera'] = function(block) { 27 | addDefinition('import_image_source', 'from octopus.image.source import cv_webcam'); 28 | var id = parseInt(block.getFieldValue('ID')); 29 | var code = 'cv_webcam(' + id + ')'; 30 | return [code, ORDER.FUNCTION_CALL]; 31 | }; 32 | 33 | PythonOcto['connection_gsioc'] = function(block) { 34 | var name = getVariableName(block.getVariable()); 35 | var id = parseInt(block.getFieldValue('ID')); 36 | var code = name + '.gsioc(' + id + ')'; 37 | return [code, ORDER.FUNCTION_CALL]; 38 | }; 39 | -------------------------------------------------------------------------------- /octopus/blocktopus/blockly/generators/python-octo/images.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @license 3 | * Visual Blocks Language 4 | * 5 | * Copyright 2012 Google Inc. 6 | * https://github.com/google/blockly 7 | * 8 | * Licensed under the Apache License, Version 2.0 (the "License"); 9 | * you may not use this file except in compliance with the License. 10 | * You may obtain a copy of the License at 11 | * 12 | * http://www.apache.org/licenses/LICENSE-2.0 13 | * 14 | * Unless required by applicable law or agreed to in writing, software 15 | * distributed under the License is distributed on an "AS IS" BASIS, 16 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 17 | * See the License for the specific language governing permissions and 18 | * limitations under the License. 19 | */ 20 | 21 | /** 22 | * @fileoverview Generating Python for image blocks. 23 | * @author mail@richardingham.net (Richard Ingham) 24 | */ 25 | 'use strict'; 26 | 27 | import {ORDER} from '../python-octo-constants'; 28 | import {valueToCode} from '../python-octo-methods'; 29 | import PythonOcto from '../python-octo-blocks'; 30 | 31 | PythonOcto['image_findcolour'] = function(block) { 32 | var input = valueToCode(block, 'INPUT', ORDER.NONE) || 'None'; 33 | var code = 'image.select(' + input + ', "' + block.getFieldValue('OP').toLowerCase() + '")'; 34 | return [code, ORDER.FUNCTION_CALL]; 35 | }; 36 | 37 | PythonOcto['image_threshold'] = function(block) { 38 | var input = valueToCode(block, 'INPUT', ORDER.NONE) || 'None'; 39 | var code = 'image.threshold(' + input + ', ' + parseInt(block.getFieldValue('THRESHOLD')) + ')'; 40 | return [code, ORDER.FUNCTION_CALL]; 41 | }; 42 | 43 | PythonOcto['image_erode'] = function(block) { 44 | var input = valueToCode(block, 'INPUT', ORDER.NONE) || 'None'; 45 | var code = 'image.erode(' + input + ')'; 46 | return [code, ORDER.FUNCTION_CALL]; 47 | }; 48 | 49 | PythonOcto['image_invert'] = function(block) { 50 | var input = valueToCode(block, 'INPUT', ORDER.NONE) || 'None'; 51 | var code = 'image.invert(' + input + ')'; 52 | return [code, ORDER.FUNCTION_CALL]; 53 | }; 54 | 55 | PythonOcto['image_colourdistance'] = function(block) { 56 | var input = valueToCode(block, 'INPUT', ORDER.NONE) || 'None'; 57 | var colour = valueToCode(block, 'COLOUR', ORDER.NONE) || '\'#000000\''; 58 | var code = 'image.colorDistance(' + [input, colour].join(', ') + ')'; 59 | return [code, ORDER.FUNCTION_CALL]; 60 | }; 61 | 62 | PythonOcto['image_huedistance'] = function(block) { 63 | var input = valueToCode(block, 'INPUT', ORDER.NONE) || 'None'; 64 | var colour = valueToCode(block, 'COLOUR', ORDER.NONE) || '\'#000000\''; 65 | var code = 'image.hueDistance(' + [input, colour].join(', ') + ')'; 66 | return [code, ORDER.FUNCTION_CALL]; 67 | }; 68 | 69 | PythonOcto['image_intensityfn'] = function(block) { 70 | var input = valueToCode(block, 'INPUT', ORDER.NONE) || 'None'; 71 | var code = 'image.intensity(' + input + ', \'' + block.getFieldValue('OP') + '\')'; 72 | return [code, ORDER.FUNCTION_CALL]; 73 | }; 74 | 75 | PythonOcto['image_crop'] = function(block) { 76 | var input = valueToCode(block, 'INPUT', ORDER.NONE) || 'None'; 77 | var x = parseInt(block.getFieldValue('X')); 78 | var y = parseInt(block.getFieldValue('Y')); 79 | var w = parseInt(block.getFieldValue('W')); 80 | var h = parseInt(block.getFieldValue('H')); 81 | var code = 'image.crop(' + [input, x, y, w, h].join(', ') + ')'; 82 | return [code, ORDER.FUNCTION_CALL]; 83 | }; 84 | 85 | PythonOcto['image_tonumber'] = function(block) { 86 | var input = valueToCode(block, 'INPUT', ORDER.NONE) || 'None'; 87 | var code = 'image.calculate(' + input + ', "' + block.getFieldValue('OP') + '")'; 88 | return [code, ORDER.FUNCTION_CALL]; 89 | }; 90 | -------------------------------------------------------------------------------- /octopus/blocktopus/blockly/generators/python-octo/logic.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @license 3 | * Visual Blocks Language 4 | * 5 | * Copyright 2012 Google Inc. 6 | * https://github.com/google/blockly 7 | * 8 | * Licensed under the Apache License, Version 2.0 (the "License"); 9 | * you may not use this file except in compliance with the License. 10 | * You may obtain a copy of the License at 11 | * 12 | * http://www.apache.org/licenses/LICENSE-2.0 13 | * 14 | * Unless required by applicable law or agreed to in writing, software 15 | * distributed under the License is distributed on an "AS IS" BASIS, 16 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 17 | * See the License for the specific language governing permissions and 18 | * limitations under the License. 19 | */ 20 | 21 | /** 22 | * @fileoverview Generating Python-Octo for logic blocks. 23 | * @author q.neutron@gmail.com (Quynh Neutron) 24 | * @author mail@richardingham.net (Richard Ingham) 25 | */ 26 | 'use strict'; 27 | 28 | import {ORDER} from '../python-octo-constants'; 29 | import {valueToCode} from '../python-octo-methods'; 30 | import PythonOcto from '../python-octo-blocks'; 31 | 32 | PythonOcto['logic_compare'] = function(block) { 33 | // Comparison operator. 34 | var OPERATORS = { 35 | 'EQ': '==', 36 | 'NEQ': '!=', 37 | 'LT': '<', 38 | 'LTE': '<=', 39 | 'GT': '>', 40 | 'GTE': '>=' 41 | }; 42 | var operator = OPERATORS[block.getFieldValue('OP')]; 43 | var order = ORDER.RELATIONAL; 44 | var argument0 = valueToCode(block, 'A', order) || '0'; 45 | var argument1 = valueToCode(block, 'B', order) || '0'; 46 | var code = argument0 + ' ' + operator + ' ' + argument1; 47 | return [code, order]; 48 | }; 49 | 50 | PythonOcto['logic_operation'] = function(block) { 51 | // Operations 'and', 'or'. 52 | var operator = (block.getFieldValue('OP') == 'AND') ? 'and' : 'or'; 53 | var order = (operator == 'and') ? ORDER.LOGICAL_AND : ORDER.LOGICAL_OR; 54 | var argument0 = valueToCode(block, 'A', order); 55 | var argument1 = valueToCode(block, 'B', order); 56 | if (!argument0 && !argument1) { 57 | // If there are no arguments, then the return value is false. 58 | argument0 = 'False'; 59 | argument1 = 'False'; 60 | } else { 61 | // Single missing arguments have no effect on the return value. 62 | var defaultArgument = (operator == 'and') ? 'True' : 'False'; 63 | if (!argument0) { 64 | argument0 = defaultArgument; 65 | } 66 | if (!argument1) { 67 | argument1 = defaultArgument; 68 | } 69 | } 70 | var code = argument0 + ' ' + operator + ' ' + argument1; 71 | return [code, order]; 72 | }; 73 | 74 | PythonOcto['logic_negate'] = function(block) { 75 | // Negation. 76 | var argument0 = valueToCode(block, 'BOOL', ORDER.LOGICAL_NOT) || 'True'; 77 | var code = 'False == ' + argument0; 78 | return [code, ORDER.RELATIONAL]; 79 | }; 80 | 81 | PythonOcto['logic_boolean'] = function(block) { 82 | // Boolean values true and false. 83 | var code = (block.getFieldValue('BOOL') == 'TRUE') ? 'True' : 'False'; 84 | return [code, ORDER.ATOMIC]; 85 | }; 86 | 87 | PythonOcto['logic_null'] = function(block) { 88 | // Null data type. 89 | return ['None', ORDER.ATOMIC]; 90 | }; 91 | 92 | PythonOcto['logic_ternary'] = function(block) { 93 | // Ternary operator. 94 | var value_if = valueToCode(block, 'IF', ORDER.CONDITIONAL) || 'False'; 95 | var value_then = valueToCode(block, 'THEN', ORDER.CONDITIONAL) || 'None'; 96 | var value_else = valueToCode(block, 'ELSE', ORDER.CONDITIONAL) || 'None'; 97 | var code = value_then + ' if ' + value_if + ' else ' + value_else; 98 | return [code, ORDER.CONDITIONAL]; 99 | }; 100 | -------------------------------------------------------------------------------- /octopus/blocktopus/blockly/generators/python-octo/machines.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | import {ORDER} from '../python-octo-constants'; 4 | import {addDefinition, getVariableName, valueToCode, quote} from '../python-octo-methods'; 5 | import PythonOcto from '../python-octo-blocks'; 6 | 7 | function machineBlockGenerator (smod, mod, cls) { 8 | return function (block) { 9 | var blockVariable = block.getVariable(); 10 | var name = getVariableName(blockVariable); 11 | var alias = (blockVariable ? blockVariable.getVarName() : "_"); 12 | var conn = valueToCode(block, 'CONNECTION', ORDER.NONE) || 'dummy()'; 13 | addDefinition('import_' + smod + '_' + mod, 'from ' + smod + ' import ' + mod); 14 | 15 | var attributes = [] 16 | if (block.mutation) { 17 | var opt, options = block.machineOptions || []; 18 | for (var i = 0, max = options.length; i < max; i++) { 19 | opt = options[i]; 20 | if (opt.multi) { 21 | attributes.push(opt.name + ' = ' + JSON.stringify(block.mutation[opt.name] || [])); 22 | } else { 23 | attributes.push(opt.name + ' = ' + (block.mutation[opt.name] || (opt.type === "Number" ? 0 : '""'))); 24 | } 25 | } 26 | } 27 | 28 | return [ 29 | name, ' = ', mod, '.', cls, '(', conn, 30 | attributes.length ? ', ' : '', attributes.join(', '), 31 | ', alias = ', quote(alias), ')' 32 | ].join(''); 33 | }; 34 | }; 35 | 36 | PythonOcto['machine_vapourtec_R2R4'] = machineBlockGenerator('octopus.manufacturer', 'vapourtec', 'R2R4'); 37 | PythonOcto['machine_knauer_K120'] = machineBlockGenerator('octopus.manufacturer', 'knauer', 'K120'); 38 | PythonOcto['machine_knauer_S100'] = machineBlockGenerator('octopus.manufacturer', 'knauer', 'S100'); 39 | PythonOcto['machine_vici_multivalve'] = machineBlockGenerator('octopus.manufacturer', 'vici', 'MultiValve'); 40 | PythonOcto['machine_mt_icir'] = machineBlockGenerator('octopus.manufacturer', 'mt', 'iCIR'); 41 | PythonOcto['machine_wpi_aladdin'] = machineBlockGenerator('octopus.manufacturer', 'wpi', 'Aladdin'); 42 | PythonOcto['machine_phidgets_phsensor'] = machineBlockGenerator('octopus.manufacturer', 'phidgets', 'PHSensor'); 43 | PythonOcto['machine_singletracker'] = machineBlockGenerator('octopus.image', 'tracker', 'SingleBlobTracker'); 44 | PythonOcto['machine_multitracker'] = machineBlockGenerator('octopus.image', 'tracker', 'MultiBlobTracker'); 45 | PythonOcto['machine_imageprovider'] = machineBlockGenerator('octopus.image', 'provider', 'ImageProvider'); 46 | PythonOcto['machine_omega_hh306a'] = machineBlockGenerator('octopus.manufacturer', 'omega', 'HH306A'); 47 | PythonOcto['machine_harvard_phd2000'] = machineBlockGenerator('octopus.manufacturer', 'harvard', 'PHD2000'); 48 | PythonOcto['machine_mt_sics_balance'] = machineBlockGenerator('octopus.manufacturer', 'mt', 'SICSBalance'); 49 | PythonOcto['machine_startech_powerremotecontrol'] = machineBlockGenerator('octopus.manufacturer', 'startech', 'PowerRemoveControl'); 50 | PythonOcto['machine_gilson_FractionCollector203B'] = machineBlockGenerator('octopus.manufacturer', 'gilson', 'FractionCollector203B'); 51 | -------------------------------------------------------------------------------- /octopus/blocktopus/blockly/generators/python-octo/variables.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @license 3 | * Visual Blocks Language 4 | * 5 | * Copyright 2012 Google Inc. 6 | * https://github.com/google/blockly 7 | * 8 | * Licensed under the Apache License, Version 2.0 (the "License"); 9 | * you may not use this file except in compliance with the License. 10 | * You may obtain a copy of the License at 11 | * 12 | * http://www.apache.org/licenses/LICENSE-2.0 13 | * 14 | * Unless required by applicable law or agreed to in writing, software 15 | * distributed under the License is distributed on an "AS IS" BASIS, 16 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 17 | * See the License for the specific language governing permissions and 18 | * limitations under the License. 19 | */ 20 | 21 | /** 22 | * @fileoverview Generating Python-Octo for variable blocks. 23 | * @author q.neutron@gmail.com (Quynh Neutron) 24 | * @author mail@richardingham.net (Richard Ingham) 25 | */ 26 | 'use strict'; 27 | 28 | import {ORDER} from '../python-octo-constants'; 29 | import {getVariableName, valueToCode} from '../python-octo-methods'; 30 | import PythonOcto from '../python-octo-blocks'; 31 | 32 | PythonOcto['lexical_variable_get'] = function(block) { 33 | // Variable getter. 34 | var name = getVariableName(block.getVariable()); 35 | return [name, ORDER.ATOMIC]; 36 | }; 37 | 38 | PythonOcto['lexical_variable_set'] = function(block) { 39 | // Variable setter. 40 | var argument0 = valueToCode(block, 'VALUE', ORDER.NONE) || '0'; 41 | var name = getVariableName(block.getVariable()); 42 | return 'set(' + name + ', ' + argument0 + ')'; 43 | }; 44 | 45 | PythonOcto['lexical_variable_set_to'] = function(block) { 46 | // Variable setter. 47 | var variable = block.getVariable(); 48 | var name = getVariableName(variable); 49 | var type = variable && variable.getType(); 50 | var value = block.getFieldValue('VALUE'); 51 | var defaultValue; 52 | 53 | if (type == 'Number') { 54 | if (!value) { 55 | value = 0; 56 | } 57 | } else if (type == 'Boolean') { 58 | value = value ? 'true' : 'false'; 59 | } else if (!value) { 60 | value = '\'\''; 61 | } else { 62 | value = '\'' + value + '\''; 63 | } 64 | 65 | return 'set(' + name + ', ' + value + ')'; 66 | }; 67 | 68 | PythonOcto['global_declaration'] = function(block) { 69 | var argument0 = valueToCode(block, 'VALUE', ORDER.NONE) || '0'; 70 | var name = getVariableName(block.variable_); 71 | var targetBlock = block.getInputTargetBlock('VALUE'); 72 | var vars = targetBlock && targetBlock.getVars(); 73 | var conn = targetBlock && targetBlock.outputConnection; 74 | 75 | if (vars && vars.length) { 76 | return name + ' = ' + argument0; 77 | } else if (conn) { 78 | var type = "str"; 79 | if (conn && conn.check_) { 80 | if (conn.check_.indexOf("Number") !== -1) { 81 | type = argument0.indexOf('.') > -1 ? "float" : "int"; 82 | } 83 | if (conn.check_.indexOf("Boolean") !== -1) { 84 | type = "bool"; 85 | } 86 | } 87 | return name + ' = variable(' + type + ', ' + argument0 + ')'; 88 | } else { 89 | return name + ' = variable()'; 90 | } 91 | }; 92 | -------------------------------------------------------------------------------- /octopus/blocktopus/blockly/msg/json/synonyms.json: -------------------------------------------------------------------------------- 1 | {"PROCEDURES_DEFRETURN_TITLE": "PROCEDURES_DEFNORETURN_TITLE", "LISTS_GET_SUBLIST_INPUT_IN_LIST": "LISTS_INLIST", "LISTS_SET_INDEX_INPUT_IN_LIST": "LISTS_INLIST", "PROCEDURES_DEFRETURN_PROCEDURE": "PROCEDURES_DEFNORETURN_PROCEDURE", "VARIABLES_SET_ITEM": "VARIABLES_DEFAULT_NAME", "LISTS_CREATE_WITH_ITEM_TITLE": "VARIABLES_DEFAULT_NAME", "CONTROLS_IF_ELSE_TITLE_ELSE": "CONTROLS_IF_MSG_ELSE", "VARIABLES_GET_ITEM": "VARIABLES_DEFAULT_NAME", "PROCEDURES_DEFRETURN_DO": "PROCEDURES_DEFNORETURN_DO", "LISTS_GET_INDEX_HELPURL": "LISTS_INDEX_OF_HELPURL", "TEXT_CREATE_JOIN_ITEM_TITLE_ITEM": "VARIABLES_DEFAULT_NAME", "CONTROLS_IF_MSG_THEN": "CONTROLS_REPEAT_INPUT_DO", "LISTS_INDEX_OF_INPUT_IN_LIST": "LISTS_INLIST", "PROCEDURES_CALLRETURN_CALL": "PROCEDURES_CALLNORETURN_CALL", "LISTS_GET_INDEX_INPUT_IN_LIST": "LISTS_INLIST", "CONTROLS_IF_ELSEIF_TITLE_ELSEIF": "CONTROLS_IF_MSG_ELSEIF", "CONTROLS_FOREACH_INPUT_DO": "CONTROLS_REPEAT_INPUT_DO", "CONTROLS_IF_IF_TITLE_IF": "CONTROLS_IF_MSG_IF", "CONTROLS_WHILEUNTIL_INPUT_DO": "CONTROLS_REPEAT_INPUT_DO", "CONTROLS_FOR_INPUT_DO": "CONTROLS_REPEAT_INPUT_DO", "TEXT_APPEND_VARIABLE": "VARIABLES_DEFAULT_NAME", "MATH_CHANGE_TITLE_ITEM": "VARIABLES_DEFAULT_NAME"} -------------------------------------------------------------------------------- /octopus/blocktopus/blockly/overrides.js: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /octopus/blocktopus/blocks/__init__.py: -------------------------------------------------------------------------------- 1 | __all__ = [ 2 | "mathematics", 3 | "text", 4 | "logic", 5 | "controls", 6 | "variables", 7 | "machines", 8 | "dependents", 9 | "images", 10 | "colour", 11 | ] 12 | -------------------------------------------------------------------------------- /octopus/blocktopus/blocks/colour.py: -------------------------------------------------------------------------------- 1 | # Package Imports 2 | from ..workspace import Block 3 | 4 | # Twisted Imports 5 | from twisted.internet import defer 6 | from twisted.python import log 7 | 8 | 9 | class colour_picker (Block): 10 | def eval (self): 11 | colour = self.fields['COLOUR'] 12 | 13 | if len(colour) != 7: 14 | colour = '#000000' 15 | 16 | return defer.succeed(( 17 | int(colour[1:3], 16), 18 | int(colour[3:5], 16), 19 | int(colour[5:7], 16), 20 | )) 21 | -------------------------------------------------------------------------------- /octopus/blocktopus/blocks/logic.py: -------------------------------------------------------------------------------- 1 | from ..workspace import Block 2 | from twisted.internet import defer 3 | from .variables import lexical_variable 4 | 5 | import operator 6 | 7 | 8 | class logic_null (Block): 9 | def eval (self): 10 | return defer.succeed(None) 11 | 12 | 13 | class logic_boolean (Block): 14 | def eval (self): 15 | return defer.succeed(self.fields['BOOL'] == 'TRUE') 16 | 17 | 18 | class logic_negate (Block): 19 | outputType = bool 20 | 21 | def eval (self): 22 | def negate (result): 23 | if result is None: 24 | return None 25 | 26 | return result == False 27 | 28 | self._complete = self.getInputValue('BOOL').addCallback(negate) 29 | return self._complete 30 | 31 | 32 | _operators_map = { 33 | "EQ": operator.eq, 34 | "NEQ": operator.ne, 35 | "LT": operator.lt, 36 | "LTE": operator.le, 37 | "GT": operator.gt, 38 | "GTE": operator.ge 39 | } 40 | 41 | def _compare (lhs, rhs, op_id): 42 | if lhs is None or rhs is None: 43 | return None 44 | 45 | op = _operators_map[op_id] 46 | return op(lhs, rhs) 47 | 48 | # Emit a warning if bad op given 49 | 50 | class logic_compare (Block): 51 | outputType = bool 52 | 53 | def eval (self): 54 | lhs = self.getInputValue('A') 55 | rhs = self.getInputValue('B') 56 | op_id = self.fields['OP'] 57 | 58 | def _eval (results): 59 | lhs, rhs = results 60 | return _compare(lhs, rhs, op_id) 61 | 62 | self._complete = defer.gatherResults([lhs, rhs]).addCallback(_eval) 63 | return self._complete 64 | 65 | 66 | class lexical_variable_compare (lexical_variable): 67 | outputType = bool 68 | 69 | def eval (self): 70 | variable = self._getVariable() 71 | 72 | if variable is None: 73 | self.emitLogMessage( 74 | "Unknown variable: " + str(self.getFieldValue('VAR')), 75 | "error" 76 | ) 77 | 78 | return defer.succeed(None) 79 | 80 | value = self.getFieldValue('VALUE') 81 | op_id = self.getFieldValue('OP') 82 | 83 | unit = self.getFieldValue('UNIT', None) 84 | 85 | if isinstance(unit, (int, float)): 86 | value *= unit 87 | 88 | return defer.succeed(_compare(variable.value, value, op_id)) 89 | 90 | 91 | class logic_operation (Block): 92 | outputType = bool 93 | 94 | def eval (self): 95 | @defer.inlineCallbacks 96 | def _run (): 97 | op = self.fields['OP'] 98 | lhs = yield self.getInputValue('A') 99 | 100 | if lhs is None: 101 | return 102 | 103 | if op == "AND": 104 | if bool(lhs): 105 | rhs = yield self.getInputValue('B') 106 | 107 | if rhs is None: 108 | return 109 | 110 | defer.returnValue(bool(rhs)) 111 | else: 112 | defer.returnValue(False) 113 | elif op == "OR": 114 | if bool(lhs): 115 | defer.returnValue(True) 116 | else: 117 | rhs = yield self.getInputValue('B') 118 | 119 | if rhs is None: 120 | return 121 | 122 | defer.returnValue(bool(rhs)) 123 | 124 | # Emit a warning 125 | return 126 | 127 | self._complete = _run() 128 | return self._complete 129 | 130 | 131 | class logic_ternary (Block): 132 | # TODO: outputType of then and else should be the same. 133 | # this is then the outputType of the logic_ternary block. 134 | 135 | def eval (self): 136 | @defer.inlineCallbacks 137 | def _run (): 138 | test = yield self.getInputValue('IF') 139 | 140 | if test is None: 141 | return 142 | 143 | if bool(test): 144 | result = yield self.getInputValue('THEN') 145 | defer.returnValue(result) 146 | else: 147 | result = yield self.getInputValue('ELSE') 148 | defer.returnValue(result) 149 | 150 | self._complete = _run() 151 | return self._complete 152 | -------------------------------------------------------------------------------- /octopus/blocktopus/blocks/test/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/richardingham/octopus/64c97ec75f6d89f247c01257fa4013d5e83ad693/octopus/blocktopus/blocks/test/__init__.py -------------------------------------------------------------------------------- /octopus/blocktopus/blocks/test/test_logic.py: -------------------------------------------------------------------------------- 1 | 2 | import random 3 | 4 | from twisted.trial import unittest 5 | 6 | from .. import logic 7 | from .. import mathematics 8 | from ...workspace import Workspace, Block 9 | from .... import data 10 | 11 | 12 | 13 | class CompareBlockTestCase (unittest.TestCase): 14 | def setUp(self): 15 | self.workspace = Workspace() 16 | self.block: Block = logic.logic_compare(self.workspace, 1) 17 | 18 | self.inputA = mathematics.math_number(self.workspace, 2) 19 | self.inputA.setFieldValue('NUM', 10) 20 | 21 | self.inputB = mathematics.math_number(self.workspace, 2) 22 | self.inputB.setFieldValue('NUM', 20) 23 | 24 | self.block.connectInput('A', self.inputA, "value") 25 | self.block.connectInput('B', self.inputB, "value") 26 | 27 | def test_compare_gt(self): 28 | self.block.setFieldValue('OP', 'GT') 29 | result = self.block.eval() 30 | self.assertEqual(self.successResultOf(result), False) 31 | return result 32 | 33 | def test_compare_lt(self): 34 | self.block.setFieldValue('OP', 'LT') 35 | result = self.block.eval() 36 | self.assertEqual(self.successResultOf(result), True) 37 | return result 38 | 39 | def test_compare_eq(self): 40 | self.block.setFieldValue('OP', 'EQ') 41 | result = self.block.eval() 42 | self.assertEqual(self.successResultOf(result), False) 43 | return result 44 | 45 | def test_compare_neq(self): 46 | self.block.setFieldValue('OP', 'NEQ') 47 | result = self.block.eval() 48 | self.assertEqual(self.successResultOf(result), True) 49 | return result 50 | 51 | 52 | class LexicalVariableCompareBlockTestCase (unittest.TestCase): 53 | def setUp(self): 54 | self.variable = data.Variable(int) 55 | self.workspace = Workspace() 56 | self.workspace.variables.add('global.global::test_var', self.variable) 57 | 58 | self.block: Block = logic.lexical_variable_compare(self.workspace, 1) 59 | self.block.setFieldValue('VAR', 'global.global::test_var') 60 | 61 | def test_variable(self): 62 | self.assertIs(self.block._getVariable(), self.variable) 63 | 64 | def test_compare_eq(self): 65 | self.variable.set(11) 66 | self.block.setFieldValue('VALUE', 10) 67 | self.block.setFieldValue('OP', 'EQ') 68 | 69 | result = self.block.eval() 70 | self.assertEqual(self.successResultOf(result), False) 71 | return result 72 | 73 | def test_compare_unit(self): 74 | self.variable.set(11) 75 | self.block.setFieldValue('VALUE', 10) 76 | self.block.setFieldValue('UNIT', 100) 77 | self.block.setFieldValue('OP', 'GT') 78 | 79 | result = self.block.eval() 80 | self.assertEqual(self.successResultOf(result), False) 81 | return result -------------------------------------------------------------------------------- /octopus/blocktopus/blocks/text.py: -------------------------------------------------------------------------------- 1 | from ..workspace import Block 2 | from twisted.internet import defer 3 | 4 | 5 | class text (Block): 6 | def eval (self): 7 | return defer.succeed(self.getFieldValue('TEXT')) 8 | 9 | 10 | class text_join (Block): 11 | def eval (self): 12 | def concatenate (results): 13 | return "".join(map(str, results)) 14 | 15 | d = [] 16 | i = 0 17 | 18 | while True: 19 | if 'ADD' + str(i) in self.inputs: 20 | d.append(self.getInputValue('ADD' + str(i))) 21 | i += 1 22 | else: 23 | break 24 | 25 | self._complete = defer.gatherResults(d).addCallback(concatenate) 26 | return self._complete 27 | -------------------------------------------------------------------------------- /octopus/blocktopus/database/createdb.py: -------------------------------------------------------------------------------- 1 | import sqlite3 2 | from pathlib import Path 3 | from twisted.logger import Logger 4 | 5 | log = Logger() 6 | 7 | def createdb(dir: Path): 8 | log.info("Creating database: {file}", file=(dir / 'octopus.db')) 9 | 10 | # Create Database 11 | conn = sqlite3.connect(dir / 'octopus.db') 12 | 13 | # Create tables 14 | conn.execute('''CREATE TABLE sketches ( 15 | guid text, 16 | title text, 17 | user_id integer, 18 | created_date integer, 19 | modified_date integer, 20 | deleted integer DEFAULT 0 21 | )''') 22 | 23 | conn.execute('''CREATE TABLE experiments ( 24 | guid text, 25 | sketch_guid text, 26 | title text, 27 | user_id integer, 28 | started_date integer, 29 | finished_date integer DEFAULT 0, 30 | deleted integer DEFAULT 0 31 | )''') 32 | 33 | # By default, create in the ../data directory. 34 | if __name__ == "__main__": 35 | from os.path import join, dirname 36 | createdb(join(dirname(dirname(__file__)), 'data')) 37 | -------------------------------------------------------------------------------- /octopus/blocktopus/database/upgradedb-1.py: -------------------------------------------------------------------------------- 1 | import sqlite3 2 | from os.path import join, dirname 3 | 4 | def updatedb (dir): 5 | print ("Upgrading database " + join(dir, 'octopus.db')) 6 | 7 | # Create Database 8 | conn = sqlite3.connect(join(dir, 'octopus.db')) 9 | 10 | # Create tables 11 | conn.execute("ALTER TABLE sketches ADD COLUMN deleted integer DEFAULT 0") 12 | 13 | # By default, create in the ../data directory. 14 | if __name__ == "__main__": 15 | updatedb(join(dirname(dirname(__file__)), 'data')) 16 | -------------------------------------------------------------------------------- /octopus/blocktopus/database/upgradedb-2.py: -------------------------------------------------------------------------------- 1 | import sqlite3 2 | from os.path import join, dirname 3 | 4 | def updatedb (dir): 5 | print ("Upgrading database " + join(dir, 'octopus.db')) 6 | 7 | # Create Database 8 | conn = sqlite3.connect(join(dir, 'octopus.db')) 9 | 10 | # Modify table 11 | conn.execute("ALTER TABLE experiments ADD COLUMN title text") 12 | conn.execute("UPDATE experiments SET title = (SELECT title FROM sketches WHERE sketches.guid = experiments.sketch_guid)") 13 | conn.commit() 14 | 15 | # By default, create in the ../data directory. 16 | if __name__ == "__main__": 17 | updatedb(join(dirname(dirname(__file__)), 'data')) 18 | -------------------------------------------------------------------------------- /octopus/blocktopus/database/upgradedb-3.py: -------------------------------------------------------------------------------- 1 | import sqlite3 2 | from os.path import join, dirname 3 | 4 | def updatedb (dir): 5 | print ("Upgrading database " + join(dir, 'octopus.db')) 6 | 7 | # Create Database 8 | conn = sqlite3.connect(join(dir, 'octopus.db')) 9 | 10 | # Modify table 11 | conn.execute("ALTER TABLE experiments ADD COLUMN finished_date integer DEFAULT 0") 12 | conn.execute("ALTER TABLE experiments ADD COLUMN deleted integer DEFAULT 0") 13 | conn.execute("UPDATE experiments SET finished_date = started_date") 14 | conn.commit() 15 | 16 | # By default, create in the ../data directory. 17 | if __name__ == "__main__": 18 | updatedb(join(dirname(dirname(__file__)), 'data')) 19 | -------------------------------------------------------------------------------- /octopus/blocktopus/resources/blockly/blockly-ext.css: -------------------------------------------------------------------------------- 1 | .blocklyText { 2 | cursor: default; 3 | font-family: sans-serif; 4 | font-size: 11pt; 5 | fill: #fff; 6 | } 7 | .blocklyNonEditableText>text { 8 | pointer-events: none; 9 | } 10 | .blocklyNonEditableText>rect, 11 | .blocklyEditableText>rect { 12 | fill: #fff; 13 | fill-opacity: .6; 14 | } 15 | .blocklyNonEditableText>text, 16 | .blocklyEditableText>text { 17 | fill: #000; 18 | } 19 | .blocklyEditableText:hover>rect { 20 | stroke-width: 2; 21 | stroke: #fff; 22 | } 23 | /* 24 | * [lyn, 10/08/13] Control parameter fields with flydown getter/setter blocks. 25 | * Brightening factors for variable color rgb(208,95,45): 26 | * 10%: rgb(212, 111, 66) 27 | * 20%: rgb(217, 127, 87) 28 | * 30%: rgb(222, 143, 108) 29 | * 40%: rgb(226, 159, 129) 30 | * 50%: rgb(231, 175, 150) 31 | * 60%: rgb(236, 191, 171) 32 | * 70%: rgb(240, 207, 192) 33 | * 80%: rgb(245, 223, 213) 34 | * 90%: rgb(250, 239, 234) 35 | */ 36 | .blocklyFieldParameter>rect { 37 | /* fill: rgb(231,175,150);*/ /* This looks too much like getter/setter var */ 38 | fill: rgb(222, 143, 108); 39 | fill-opacity: 1.0; 40 | stroke-width: 2; 41 | stroke: rgb(231, 175, 150); 42 | } 43 | .blocklyFieldParameter>text { 44 | /* fill: #000; */ /* Use white rather than black on dark orange */ 45 | stroke-width: 1; 46 | fill: #000; 47 | } 48 | .blocklyFieldParameter:hover>rect { 49 | stroke-width: 2; 50 | stroke: rgb(231,175,150); 51 | fill: rgb(231,175,150); 52 | fill-opacity: 1.0; 53 | } 54 | /* 55 | * [lyn, 10/08/13] Control flydown with the getter/setter blocks. 56 | */ 57 | .blocklyFieldParameterFlydown { 58 | fill: rgb(231,175,150); 59 | fill-opacity: 0.8; 60 | } 61 | /* 62 | * [lyn, 10/08/13] Control parameter fields with flydown procedure caller block. 63 | */ 64 | .blocklyFieldProcedure>rect { 65 | /* rgb(231,175,150) is procedure color rgb(124,83,133) brightened by 70% */ 66 | fill: rgb(215,203,218); 67 | fill-opacity: 1.0; 68 | stroke-width: 0; 69 | stroke: #000; 70 | } 71 | .blocklyFieldProcedure>text { 72 | fill: #000; 73 | } 74 | .blocklyFieldProcedure:hover>rect { 75 | stroke-width: 2; 76 | stroke: #fff; 77 | fill: rgb(215,203,218); 78 | fill-opacity: 1.0; 79 | } 80 | /* 81 | * [lyn, 10/08/13] Control flydown with the procedure caller block. 82 | */ 83 | .blocklyFieldProcedureFlydown { 84 | fill: rgb(215,203,218); 85 | fill-opacity: 0.8; 86 | } 87 | /* 88 | * Don't allow users to select text. It gets annoying when trying to 89 | * drag a block and selected text moves instead. 90 | */ 91 | .blocklyBubbleText { 92 | fill: #000; 93 | } 94 | /* 95 | * [lyn, 09/10/14] Limit size of runtime error dialog window 96 | */ 97 | .blocklyRuntimeErrorDialog { 98 | max-width: 75%; 99 | max-height: 75%; 100 | overflow: auto 101 | } 102 | .blocklySvg text { 103 | -moz-user-select: none; 104 | -webkit-user-select: none; 105 | user-select: none; 106 | cursor: inherit; 107 | } 108 | /* 109 | * Selecting text for Errors and Warnings is allowed though. 110 | */ 111 | .blocklySvg text.blocklyErrorWarningText { 112 | -moz-user-select: text; 113 | -webkit-user-select: text; 114 | user-select: text; 115 | } 116 | .blocklyHidden { 117 | display: none; 118 | } 119 | .blocklyFieldDropdown:not(.blocklyHidden) { 120 | display: block; 121 | } 122 | /* 123 | * Hide scrollbar track in flyout 124 | */ 125 | path.blocklyFlyoutBackground rect.blocklyScrollbarKnob { 126 | color: rgb(182, 182, 182); 127 | } 128 | path.blocklyFlyoutBackground rect.blocklyScrollbarBackground { 129 | fill: none; 130 | stroke: none; 131 | } 132 | 133 | .blocklyText.fa { 134 | font-family: FontAwesome; 135 | } 136 | .blocklyText.fa.quote { 137 | font-size: 80%; 138 | fill: rgba(255, 255, 255, 0.9); 139 | } 140 | -------------------------------------------------------------------------------- /octopus/blocktopus/resources/blockly/media/1x1.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/richardingham/octopus/64c97ec75f6d89f247c01257fa4013d5e83ad693/octopus/blocktopus/resources/blockly/media/1x1.gif -------------------------------------------------------------------------------- /octopus/blocktopus/resources/blockly/media/click.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/richardingham/octopus/64c97ec75f6d89f247c01257fa4013d5e83ad693/octopus/blocktopus/resources/blockly/media/click.mp3 -------------------------------------------------------------------------------- /octopus/blocktopus/resources/blockly/media/click.ogg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/richardingham/octopus/64c97ec75f6d89f247c01257fa4013d5e83ad693/octopus/blocktopus/resources/blockly/media/click.ogg -------------------------------------------------------------------------------- /octopus/blocktopus/resources/blockly/media/click.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/richardingham/octopus/64c97ec75f6d89f247c01257fa4013d5e83ad693/octopus/blocktopus/resources/blockly/media/click.wav -------------------------------------------------------------------------------- /octopus/blocktopus/resources/blockly/media/delete.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/richardingham/octopus/64c97ec75f6d89f247c01257fa4013d5e83ad693/octopus/blocktopus/resources/blockly/media/delete.mp3 -------------------------------------------------------------------------------- /octopus/blocktopus/resources/blockly/media/delete.ogg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/richardingham/octopus/64c97ec75f6d89f247c01257fa4013d5e83ad693/octopus/blocktopus/resources/blockly/media/delete.ogg -------------------------------------------------------------------------------- /octopus/blocktopus/resources/blockly/media/delete.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/richardingham/octopus/64c97ec75f6d89f247c01257fa4013d5e83ad693/octopus/blocktopus/resources/blockly/media/delete.wav -------------------------------------------------------------------------------- /octopus/blocktopus/resources/blockly/media/handclosed.cur: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/richardingham/octopus/64c97ec75f6d89f247c01257fa4013d5e83ad693/octopus/blocktopus/resources/blockly/media/handclosed.cur -------------------------------------------------------------------------------- /octopus/blocktopus/resources/blockly/media/handopen.cur: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/richardingham/octopus/64c97ec75f6d89f247c01257fa4013d5e83ad693/octopus/blocktopus/resources/blockly/media/handopen.cur -------------------------------------------------------------------------------- /octopus/blocktopus/resources/blockly/media/sprites.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/richardingham/octopus/64c97ec75f6d89f247c01257fa4013d5e83ad693/octopus/blocktopus/resources/blockly/media/sprites.png -------------------------------------------------------------------------------- /octopus/blocktopus/resources/download.js: -------------------------------------------------------------------------------- 1 | 2 | jQuery(function ($) { 3 | var checkboxes = $('
').hide().appendTo($('form')); 4 | $('#variables-list tbody input[type=checkbox]').appendTo(checkboxes); 5 | 6 | $('[data-toggle="tooltip"]').each(function () { 7 | this._tooltip = new Tooltip(this, { placement: 'top' }); 8 | }); 9 | 10 | $('#variables-list').DataTable({ 11 | order: [[1, 'asc']], 12 | paging: false, 13 | autoWidth: false, 14 | columns: [ 15 | { visible: false, orderable: false }, 16 | null, 17 | null, 18 | null 19 | ] 20 | }); 21 | 22 | var dt = $('#variables-list').DataTable(); 23 | $('') 24 | .appendTo('#variables-list_filter') 25 | .on('click', function () { 26 | dt.search(''); 27 | dt.draw(); 28 | return false; 29 | }); 30 | $('') 31 | .appendTo('#variables-list_filter') 32 | .on('click', generateToggleAllStatesFunction(true)); 33 | $('') 34 | .appendTo('#variables-list_filter') 35 | .on('click', generateToggleAllStatesFunction(false)); 36 | 37 | $('#variables-list tbody').on('click', 'tr', function () { 38 | toggleSelectedState(this); 39 | setSubmitButtonState(); 40 | }); 41 | 42 | function generateToggleAllStatesFunction (state) { 43 | return function () { 44 | $('#variables-list tbody tr').each(function () { 45 | toggleSelectedState(this, state); 46 | }); 47 | setSubmitButtonState(); 48 | return false; 49 | }; 50 | } 51 | 52 | function toggleSelectedState (tr, state) { 53 | $(tr).toggleClass('active', state); 54 | $('input[value="' + $(tr).data('key') + '"]', checkboxes) 55 | .attr('checked', $(tr).is('.active')); 56 | } 57 | 58 | // Disable submit button initially. 59 | function setSubmitButtonState () { 60 | var selectedVars = $('input[checked]', checkboxes).length; 61 | var button = $('form .buttons button[type="submit"]'); 62 | var tooltipElement = button.parent('.tooltip-wrapper'); 63 | 64 | button.attr('disabled', selectedVars === 0); 65 | 66 | if (selectedVars === 0) { 67 | tooltipElement.data('tooltip', new Tooltip(tooltipElement, { 68 | title: 'Select some data variables to continue', 69 | placement: 'right' 70 | })); 71 | } else { 72 | tooltipElement.data('tooltip').dispose(); 73 | } 74 | } 75 | 76 | setSubmitButtonState(); 77 | }); 78 | -------------------------------------------------------------------------------- /octopus/blocktopus/resources/experiment-popup.css: -------------------------------------------------------------------------------- 1 | 2 | .bubble-dropdown { 3 | top: 0; 4 | left: 0; 5 | } 6 | .bubble-dropdown:before { 7 | content: ''; 8 | display: inline-block; 9 | border-left: 7px solid transparent; 10 | border-right: 7px solid transparent; 11 | border-bottom: 7px solid #ccc; 12 | border-top: 0; 13 | border-bottom-color: rgba(0, 0, 0, 0.2); 14 | position: absolute; 15 | } 16 | .bubble-dropdown:after { 17 | content: ''; 18 | display: inline-block; 19 | border-left: 6px solid transparent; 20 | border-right: 6px solid transparent; 21 | border-bottom: 6px solid #fff; 22 | border-top: 0; 23 | position: absolute; 24 | } 25 | .bubble-dropdown.bubble-orient-left:before { 26 | left: 6px; 27 | } 28 | .bubble-dropdown.bubble-orient-left:after { 29 | left: 7px; 30 | } 31 | .bubble-dropdown.bubble-orient-right:before { 32 | right: 6px; 33 | } 34 | .bubble-dropdown.bubble-orient-right:after { 35 | right: 7px; 36 | } 37 | .bubble-dropdown.bubble-orient-top:before { 38 | top: -7px; 39 | } 40 | .bubble-dropdown.bubble-orient-top:after { 41 | top: -6px; 42 | } 43 | .bubble-dropdown.bubble-orient-bottom:before { 44 | bottom: -7px; 45 | border-bottom: 0; 46 | border-top: 7px solid #999; 47 | } 48 | .bubble-dropdown.bubble-orient-bottom:after { 49 | bottom: -6px; 50 | border-bottom: 0; 51 | border-top: 6px solid #fff; 52 | } 53 | 54 | .bubble-dropdown .property-editor .form-item { 55 | padding: 2px 8px; 56 | } 57 | 58 | .bubble-dropdown .graph-options { 59 | padding: 5px 0; 60 | } 61 | 62 | .bubble-dropdown .graph-options > div.variable, 63 | .bubble-dropdown .graph-options > div.message { 64 | padding: 3px 20px; 65 | line-height: 1.4; 66 | color: #333; 67 | white-space: nowrap; 68 | } 69 | .bubble-dropdown .graph-options > div.message { 70 | font-style: italic; 71 | } 72 | 73 | .bubble-dropdown .graph-options > div.variable > i { 74 | padding-right: 5px; 75 | } 76 | .bubble-dropdown .graph-options > div.variable > i.fa-times-circle { 77 | color: #dd0000; 78 | } 79 | .bubble-dropdown .graph-options > div.variable > i.fa-plus-circle { 80 | color: #00dd00; 81 | } 82 | .bubble-dropdown .graph-options > div.variable:hover, 83 | .bubble-dropdown .graph-options > div.variable:hover > i { 84 | background-color: #23527c; 85 | color: #ffffff; 86 | cursor: default; 87 | } 88 | 89 | .bubble-dropdown .graph-options .form-item { 90 | padding: 3px 20px; 91 | } 92 | -------------------------------------------------------------------------------- /octopus/blocktopus/resources/experiment-result.css: -------------------------------------------------------------------------------- 1 | html, body { 2 | margin: 0; 3 | padding: 0; 4 | height: 100vh; 5 | } 6 | 7 | body { 8 | font-family: 'Segoe UI', 'Helvetica Neue', Helvetica, Arial, sans-serif; 9 | color: #333; 10 | -webkit-user-select: none; 11 | -moz-user-select: none; 12 | -ms-user-select: none; 13 | user-select: none; 14 | -webkit-tap-highlight-color: rgba(0,0,0,0); 15 | -webkit-touch-callout: none; 16 | } 17 | 18 | #toolbar { 19 | background-color: #526E9C; 20 | border-radius: 0; 21 | color: #FFF; 22 | height: 60px; 23 | margin-bottom: 0; 24 | padding-top: 10px; 25 | } 26 | 27 | #toolbar span { 28 | color: #fff; 29 | padding-bottom: 3px; 30 | font-size: 24px; 31 | margin-left: 15px; 32 | } 33 | 34 | #toolbar .btn { 35 | font-size: 18px; 36 | background-color: transparent; 37 | border-color: rgba(255, 255, 255, 0.6); 38 | color: #fff; 39 | margin-top: 0; 40 | } 41 | 42 | #viewer > * { 43 | top: 60px; 44 | } 45 | 46 | #variables { 47 | padding: 10px 0; 48 | width: 180px; 49 | bottom: 0; 50 | margin-bottom: 0; 51 | position: fixed; 52 | list-style-type: none; 53 | overflow-y: scroll; 54 | } 55 | 56 | #variables li { 57 | padding: 2px 15px; 58 | cursor: default; 59 | font-size: 18px; 60 | } 61 | 62 | #variables li:hover { 63 | background: #526E9C; 64 | } 65 | 66 | #data { 67 | position: fixed; 68 | left: 180px; 69 | right: 0; 70 | bottom: 0; 71 | overflow-y: scroll; 72 | } 73 | 74 | #data .grid-stack-item-handle i { 75 | float: right; 76 | padding: 3px; 77 | cursor: pointer; 78 | } 79 | 80 | #data .grid-stack-item-handle i:hover { 81 | color: #fff; 82 | } 83 | 84 | #data .chart-container div.grid-stack-item-content { 85 | background: #FFF; 86 | border: 1px solid #000; 87 | border-radius: 2px; 88 | overflow: hidden; 89 | } 90 | 91 | #data .chart-container div.grid-stack-item-handle { 92 | background: rgb(171, 178, 209); 93 | height: 20px; 94 | float: left; 95 | width: 100%; 96 | } 97 | 98 | #data .chart-container .chart { 99 | height: 100%; 100 | padding-top: 20px; 101 | } 102 | 103 | #data .property-container div.grid-stack-item-content { 104 | background: #FFF; 105 | border: 1px solid #000; 106 | border-radius: 2px; 107 | overflow: hidden; 108 | } 109 | 110 | #data .property-container div.grid-stack-item-handle { 111 | background: rgb(171, 178, 209); 112 | height: 20px; 113 | float: left; 114 | padding-left: 5px; 115 | width: 100%; 116 | } 117 | 118 | #data .property-container .property { 119 | height: 100%; 120 | padding-top: 20px; 121 | } 122 | 123 | #data .property-container .value { 124 | padding: 5px; 125 | font-size: 18px; 126 | } 127 | 128 | svg .line { 129 | fill: none; 130 | stroke: #000; 131 | stroke-width: 1.5px; 132 | } 133 | 134 | svg { 135 | font: 10px sans-serif; 136 | } 137 | 138 | svg .axis path, svg .axis line { 139 | fill: none; 140 | stroke: #000; 141 | shape-rendering: crispEdges; 142 | } 143 | 144 | svg .x.axis line { 145 | shape-rendering: auto; 146 | } 147 | 148 | svg .legend rect { 149 | fill: white; 150 | stroke: black; 151 | opacity: 0.8; 152 | } 153 | -------------------------------------------------------------------------------- /octopus/blocktopus/resources/experiment-result.js: -------------------------------------------------------------------------------- 1 | jQuery(function ($) { 2 | 3 | var timezero = parseInt($('#viewer').data('timezero')) * 1000; 4 | var timeend = parseInt($('#viewer').data('timeend')) * 1000; 5 | var variables = $('#viewer').data('variables'); 6 | var dataurl = $('#viewer').data('url'); 7 | 8 | var graphs = []; 9 | 10 | $.each(variables, function (i, variable) { 11 | $('#variables').append($('
  • ', { 12 | 'data-id': variable.key, 13 | 'text': variable.name 14 | })); 15 | }); 16 | 17 | var grid = $('
    ') 18 | .addClass('grid-stack') 19 | .appendTo('#data') 20 | .gridstack({ 21 | cell_height: 50, 22 | width: 12, 23 | float: true, 24 | handle: '.grid-stack-item-handle' 25 | }) 26 | .data('gridstack'); 27 | 28 | function requestData (streams, start, end) { 29 | var graph = this; 30 | 31 | var data = { 32 | var: streams 33 | }; 34 | 35 | if (start) { 36 | data.start = +start / 1000; 37 | } 38 | if (end) { 39 | data.end = +end / 1000; 40 | } 41 | 42 | $.get( 43 | dataurl, 44 | data, 45 | function (payload) { 46 | var i, j, m, n, data, point, zero = timezero; 47 | for (i = 0, m = payload.length; i < m; i++) { 48 | data = payload[i].data; 49 | for (j = 0, n = data.length; j < n; j++) { 50 | point = data[j]; 51 | point[0] = new Date(point[0] * 1000 + zero); 52 | } 53 | } 54 | 55 | graph.addData(payload); 56 | }, 57 | 'json' 58 | ); 59 | } 60 | 61 | function addGraph (variable) { 62 | var container = $('
    '); 63 | var content = $('
    ').appendTo(container); 64 | var handle = $('
    ').appendTo(content); 65 | 66 | $('').appendTo(handle); 67 | $('').appendTo(handle); 68 | $('').appendTo(handle); 69 | 70 | var graphEl = $('
    ').appendTo(content); 71 | grid.add_widget(container, 0, 0, 6, 5); 72 | 73 | var graph = new Graph(graphEl[0], { 74 | timezero: timezero, 75 | static: true, 76 | requestData: requestData 77 | }); 78 | graph.addStream(variable); 79 | graphs.push(graph); 80 | container.data('graph', graph); 81 | 82 | return true; 83 | } 84 | 85 | $('.grid-stack').on('resizestop', _.debounce(function (event) { 86 | //var grid = this; 87 | var element = $(event.target); 88 | if (element.is('.chart-container')) { 89 | element.data('graph').setSize(); 90 | } 91 | }, 500)); 92 | 93 | // Chart toolbar options 94 | $('#data').on('click', '.chart-container i.remove', function () { 95 | var container = $(this).closest('.chart-container'); 96 | var graph = container.data('graph'); 97 | container.remove(); 98 | 99 | for (i = 0, m = graphs.length; i < m; i++) { 100 | if (graphs[i] == graph) { 101 | graphs.splice(i, 1); 102 | } 103 | } 104 | }); 105 | 106 | $('#data').on('click', '.chart-container i.options', function () { 107 | var container = $(this).closest('.chart-container'); 108 | var graph = container.data('graph'); 109 | var editor = new GraphOptions(container, graph, { 110 | axisTime: true 111 | }); 112 | editor.show(); 113 | }); 114 | 115 | $('#data').on('click', '.chart-container i.add', function () { 116 | var container = $(this).closest('.chart-container'); 117 | var graph = container.data('graph'); 118 | var editor = new GraphAddStream(container, graph, variables, { 119 | addStream: function (variable) { 120 | return graph.addStream(variable); 121 | } 122 | }); 123 | editor.show(); 124 | }); 125 | 126 | // Add a chart by clicking on a variables entry 127 | $('#variables').on('click', 'li', function () { 128 | var id = $(this).data('id'); 129 | 130 | if (id) { 131 | // Find id in variables. 132 | for (var i = 0, m = variables.length; i < m; i++) { 133 | if (variables[i].key === id) { 134 | return addGraph(variables[i]); 135 | } 136 | } 137 | } 138 | }); 139 | }); 140 | -------------------------------------------------------------------------------- /octopus/blocktopus/resources/prettify/prettify.css: -------------------------------------------------------------------------------- 1 | .pln{color:#000}@media screen{.str{color:#080}.kwd{color:#008}.com{color:#800}.typ{color:#606}.lit{color:#066}.pun,.opn,.clo{color:#660}.tag{color:#008}.atn{color:#606}.atv{color:#080}.dec,.var{color:#606}.fun{color:red}}@media print,projection{.str{color:#060}.kwd{color:#006;font-weight:bold}.com{color:#600;font-style:italic}.typ{color:#404;font-weight:bold}.lit{color:#044}.pun,.opn,.clo{color:#440}.tag{color:#006;font-weight:bold}.atn{color:#404}.atv{color:#060}}pre.prettyprint{padding:2px;border:1px solid #888}ol.linenums{margin-top:0;margin-bottom:0}li.L0,li.L1,li.L2,li.L3,li.L5,li.L6,li.L7,li.L8{list-style-type:none}li.L1,li.L3,li.L5,li.L7,li.L9{background:#eee} -------------------------------------------------------------------------------- /octopus/blocktopus/resources/root.css: -------------------------------------------------------------------------------- 1 | 2 | body { 3 | font-family: 'Segoe UI', 'Helvetica Neue', Helvetica, Arial, sans-serif; 4 | color: #333; 5 | -webkit-tap-highlight-color: rgba(0,0,0,0); 6 | -webkit-touch-callout: none; 7 | } 8 | 9 | #header { 10 | background-color: #337ab7; 11 | color: #FFF; 12 | padding: 10px 0 35px; 13 | } 14 | #header h1 { 15 | font-size: 60px; 16 | } 17 | 18 | #action { 19 | margin: 40px 0; 20 | } 21 | 22 | #create form { 23 | text-align: center; 24 | } 25 | 26 | #create .btn-lg { 27 | padding: 20px 36px; 28 | font-size: 30px; 29 | border-radius: 8px; 30 | } 31 | 32 | #running h2 { 33 | font-size: 20px; 34 | } 35 | 36 | #stored { 37 | background-color: rgb(247, 247, 252) 38 | } 39 | 40 | #stored table tr td, 41 | #past table tr td { 42 | white-space: nowrap; 43 | vertical-align: middle; 44 | } 45 | #stored table tr td:first-child, 46 | #past table tr td:first-child { 47 | width: 75%; 48 | white-space: normal; 49 | } 50 | #stored th, #stored td, 51 | #past th, #past td { 52 | padding: 8px 20px; 53 | } 54 | #stored th:first-child, 55 | #stored td:first-child, 56 | #past th:first-child, 57 | #past td:first-child { 58 | padding: 8px; 59 | } 60 | 61 | #past { 62 | margin: 20px 0; 63 | } 64 | #past a[data-toggle=collapse] { 65 | margin: 20px 20px 0 0; 66 | } 67 | #past div.panel { 68 | margin: 40px 20px 20px; 69 | } 70 | 71 | #stored form, 72 | #past form { 73 | display: inline; 74 | } 75 | 76 | #stored tr.deleted, 77 | #stored tr.deleted a, 78 | #past tr.deleted, 79 | #past tr.deleted a { 80 | color: #999; 81 | background-color: #eee; 82 | } 83 | -------------------------------------------------------------------------------- /octopus/blocktopus/server/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/richardingham/octopus/64c97ec75f6d89f247c01257fa4013d5e83ad693/octopus/blocktopus/server/__init__.py -------------------------------------------------------------------------------- /octopus/blocktopus/server/__main__.py: -------------------------------------------------------------------------------- 1 | from . import server 2 | 3 | server.run_server_with_env_args() 4 | -------------------------------------------------------------------------------- /octopus/blocktopus/server/protocol/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/richardingham/octopus/64c97ec75f6d89f247c01257fa4013d5e83ad693/octopus/blocktopus/server/protocol/__init__.py -------------------------------------------------------------------------------- /octopus/blocktopus/server/protocol/block.py: -------------------------------------------------------------------------------- 1 | from ...workspace import Workspace, Event, UnknownEventError 2 | 3 | class BlockProtocol (object): 4 | def __init__ (self, transport): 5 | self.transport = transport 6 | 7 | def send (self, topic, payload, context): 8 | self.transport.send('block', topic, payload, context) 9 | 10 | def receive (self, topic, payload, sketch, context): 11 | try: 12 | if topic == 'transaction': 13 | for event in payload['events']: 14 | self.receive(event['event'], event['data'], sketch, context) 15 | return 16 | 17 | if topic == 'cancel': 18 | return sketch.runtimeCancelBlock(payload['id']) 19 | 20 | # Block commands 21 | try: 22 | event = Event.fromPayload(topic, payload) 23 | return sketch.processEvent(event, context) 24 | 25 | except UnknownEventError: 26 | pass 27 | 28 | except Error as e: 29 | return self.send('error', e, context) 30 | 31 | 32 | class Error (Exception): 33 | pass 34 | -------------------------------------------------------------------------------- /octopus/blocktopus/server/protocol/runtime.py: -------------------------------------------------------------------------------- 1 | 2 | class RuntimeProtocol (object): 3 | def __init__ (self, transport): 4 | self.transport = transport 5 | 6 | def send (self, topic, payload, context): 7 | self.transport.send('runtime', topic, payload, context) 8 | 9 | def receive (self, topic, payload, context): 10 | if topic == 'getruntime': return self.getRuntime(payload, context) 11 | if topic == 'packet': return self.receivePacket(payload, context) 12 | 13 | def getRuntime (self, payload, context): 14 | try: 15 | name = self.transport.options["type"] 16 | except KeyError: 17 | name = "octopus" 18 | 19 | try: 20 | capabilities = self.transport.options["capabilities"] 21 | except KeyError: 22 | capabilities = [] 23 | 24 | self.send('runtime', { 25 | "type": name, 26 | "version": self.transport.version, 27 | "capabilities": capabilities 28 | }, context) 29 | 30 | def receivePacket (self, payload, context): 31 | self.send('error', Error('Packets not supported yet'), context) 32 | 33 | class Error (Exception): 34 | pass 35 | -------------------------------------------------------------------------------- /octopus/blocktopus/server/protocol/sketch.py: -------------------------------------------------------------------------------- 1 | from ...sketch import Sketch 2 | 3 | from twisted.internet import reactor 4 | 5 | from octopus.constants import State 6 | 7 | class SketchProtocol (object): 8 | def __init__ (self, transport): 9 | self.transport = transport 10 | 11 | def send (self, topic, payload, context): 12 | self.transport.send('sketch', topic, payload, context) 13 | 14 | def receive (self, topic, payload, sketch, context): 15 | try: 16 | if topic == 'load': 17 | return self.loadSketch(payload, context) 18 | 19 | if sketch is None: 20 | raise Error("[%s:%s] No Sketch specified" % ('sketch', topic)) 21 | 22 | if topic == 'rename': 23 | return sketch.renameSketch({ 'title': payload['title'] }, context) 24 | 25 | except Error as e: 26 | return self.send('error', e, context) 27 | 28 | def loadSketch (self, payload, context): 29 | if "sketch" not in payload: 30 | raise Error('No sketch ID provided') 31 | 32 | id = payload["sketch"] 33 | 34 | def _onEvent (protocol, topic, payload): 35 | # is id already in the data? 36 | payload['sketch'] = id 37 | self.transport.send(protocol, topic, payload, context) 38 | 39 | def _sendData (sketch): 40 | blockStates = { 41 | block.id: block.state.name.lower() 42 | for block in sketch.workspace.allBlocks.values() 43 | if block.state is not State.READY 44 | } 45 | 46 | sketchData = { 47 | "sketch": sketch.id, 48 | "title": sketch.title, 49 | "events": sketch.workspace.toEvents(), 50 | "state": sketch.workspace.state.name.lower(), 51 | "block-states": blockStates 52 | } 53 | 54 | if sketch.experiment is not None: 55 | sketchData['experiment'] = sketch.experiment.id 56 | sketchData['log-messages'] = sketch.experiment.logMessages 57 | 58 | self.send('load', sketchData, context) 59 | 60 | try: 61 | sketch = self.transport.sketches[id] 62 | except KeyError: 63 | pass 64 | else: 65 | sketch.subscribe(context, _onEvent) 66 | return _sendData(sketch) 67 | 68 | def _done (data): 69 | self.transport.sketches[id] = sketch 70 | sketch.subscribe(context, _onEvent) 71 | return _sendData(sketch) 72 | 73 | def _error (failure): 74 | self.send('error', str(failure), context) 75 | 76 | sketch = Sketch(id) 77 | 78 | @sketch.on("closed") 79 | def onSketchClosed (data): 80 | # This must be performed later to avoid an exception 81 | # in sketches.items() in disconnected() 82 | def _del (): 83 | del self.transport.sketches[id] 84 | 85 | reactor.callLater(0, _del) 86 | 87 | return sketch.load().addCallbacks(_done, _error) 88 | 89 | 90 | class Error (Exception): 91 | pass 92 | -------------------------------------------------------------------------------- /octopus/blocktopus/server/transport/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/richardingham/octopus/64c97ec75f6d89f247c01257fa4013d5e83ad693/octopus/blocktopus/server/transport/__init__.py -------------------------------------------------------------------------------- /octopus/blocktopus/server/transport/base.py: -------------------------------------------------------------------------------- 1 | from ..protocol.sketch import SketchProtocol 2 | from ..protocol.experiment import ExperimentProtocol 3 | from ..protocol.block import BlockProtocol 4 | from ..protocol.runtime import RuntimeProtocol 5 | 6 | # This is the class all runtime implementations can extend to easily wrap 7 | # into any transport protocol. 8 | class BaseTransport (object): 9 | def __init__ (self, options = None): 10 | self.options = options or {} 11 | self.version = '0.1' 12 | self.runtimeProtocol = RuntimeProtocol(self) 13 | self.sketchProtocol = SketchProtocol(self) 14 | self.experimentProtocol = ExperimentProtocol(self) 15 | self.blockProtocol = BlockProtocol(self) 16 | 17 | self.sketches = {} 18 | 19 | def send (self, protocol, topic, payload, context): 20 | """Send a message back to the user via the transport protocol. 21 | Each transport implementation should provide their own implementation 22 | of this method. 23 | The context is usually the context originally received from the 24 | transport with the request. For example, a specific socket connection. 25 | @param [str] Name of the protocol 26 | @param [str] Topic of the message 27 | @param [dict] Message payload 28 | @param [Object] Message context, dependent on the transport 29 | """ 30 | 31 | raise NotImplementedError 32 | 33 | def receive (self, protocol, topic, payload, context): 34 | """Handle incoming message 35 | This is the entry-point to actual protocol handlers. When receiving 36 | a message, the runtime should call this to make the requested actions 37 | happen 38 | The context is originally received from the transport. For example, 39 | a specific socket connection. The context will be utilized when 40 | sending messages back to the requester. 41 | @param [str] Name of the protocol 42 | @param [str] Topic of the message 43 | @param [dict] Message payload 44 | @param [Object] Message context, dependent on the transport 45 | """ 46 | 47 | # Find locally stored sketch by ID 48 | try: 49 | sketch = self.sketches[payload['sketch']] 50 | except KeyError: 51 | sketch = None 52 | 53 | # Block actions 54 | if protocol == 'block': 55 | return self.blockProtocol.receive(topic, payload, sketch, context) 56 | 57 | # Experiment actions 58 | if protocol == 'experiment': 59 | try: 60 | if sketch.experiment.id == payload['experiment']: 61 | experiment = sketch.experiment 62 | else: 63 | experiment = None 64 | except (AttributeError, KeyError): 65 | experiment = None 66 | 67 | return self.experimentProtocol.receive(topic, payload, sketch, experiment, context) 68 | 69 | # Sketch actions 70 | if protocol == 'sketch': 71 | return self.sketchProtocol.receive(topic, payload, sketch, context) 72 | 73 | def disconnected (self, context): 74 | """Handle client disconnection 75 | @param [Object] Message context, dependent on the transport 76 | """ 77 | 78 | for id, sketch in self.sketches.items(): 79 | sketch.unsubscribe(context) 80 | -------------------------------------------------------------------------------- /octopus/blocktopus/server/websocket.py: -------------------------------------------------------------------------------- 1 | from autobahn.twisted.websocket import WebSocketServerProtocol, WebSocketServerFactory 2 | from autobahn.websocket.compress import PerMessageDeflateOffer, PerMessageDeflateOfferAccept 3 | from twisted.python import log 4 | from twisted.internet import reactor 5 | 6 | import json 7 | 8 | from .transport.base import BaseTransport 9 | 10 | 11 | class WebSocketRuntime (BaseTransport): 12 | def __init__ (self): 13 | BaseTransport.__init__(self, options = { 14 | "capabilities": [ 15 | 'protocol:sketch', 16 | 'protocol:block', 17 | 'protocol:experiment', 18 | ] 19 | }) 20 | 21 | def send (self, protocol, topic, payload, context): 22 | if isinstance(payload, Exception): 23 | payload = { 24 | "type": payload.__class__.__name__, 25 | "message": payload.message 26 | } 27 | 28 | response = { 29 | "protocol": protocol, 30 | "command": topic, 31 | "payload": payload, 32 | } 33 | 34 | if topic == "error": 35 | log.err("Response Error: " + str(payload)) 36 | # log.msg("Response", response) 37 | 38 | context.sendMessage(json.dumps(response).encode('utf-8')) 39 | 40 | 41 | class OctopusEditorProtocol (WebSocketServerProtocol): 42 | def onConnect (self, request): 43 | return 'octopus' 44 | 45 | def onOpen (self): 46 | self.subscribedExperiments = {} 47 | self.sendPing() 48 | 49 | def onClose (self, wasClean, code, reason): 50 | self.factory.runtime.disconnected(self) 51 | 52 | def onMessage (self, payload, isBinary): 53 | if isBinary: 54 | raise ValueError("WebSocket message must be UTF-8") 55 | 56 | cmd = json.loads(payload) 57 | 58 | # log.msg("Command", cmd) 59 | 60 | self.factory.runtime.receive( 61 | cmd['protocol'], 62 | cmd['command'], 63 | cmd["payload"], 64 | self 65 | ) 66 | 67 | def subscribeExperiment (self, experiment): 68 | self.subscribedExperiments[experiment.id] = { 69 | "experiment": experiment, 70 | "streams": [], 71 | "properties": [] 72 | } 73 | 74 | def chooseExperimentProperties (self, experiment, properties): 75 | self.subscribedExperiments[experiment.id]['properties'] = properties 76 | 77 | def chooseExperimentStreams (self, experiment, streams): 78 | self.subscribedExperiments[experiment.id]['streams'] = streams 79 | 80 | def getExperimentProperties (self, experiment): 81 | try: 82 | return self.subscribedExperiments[experiment.id]['properties'] 83 | except KeyError: 84 | return [] 85 | 86 | def getExperimentStreams (self, experiment): 87 | try: 88 | return self.subscribedExperiments[experiment.id]['streams'] 89 | except KeyError: 90 | return [] 91 | -------------------------------------------------------------------------------- /octopus/blocktopus/templates/experiment-download.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | Experiment Download 13 | 14 | 38 | 39 | 40 | 41 | 42 |
    43 |

    Download Experiment Data

    44 |
    45 | 46 |
    47 | 48 |
    49 |

    Options

    50 | 51 |
    52 | 53 |
    54 | 55 |
    56 | 61 |
    62 |
    63 | 64 |
    65 | 66 |
    67 | 68 |
    69 |
    70 |
    71 |
    72 | 73 |
    74 |

    Select Data

    75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 93 | 99 | 100 | 101 | 102 | 103 |
    VariableTypeUnit
    89 | 90 | 91 | 92 | 94 | 95 | 96 | 97 | 98 |
    104 |
    105 | 106 |
    107 |
    108 | 109 | 110 | 111 |
    112 |
    113 | 114 |
    115 | 116 | 117 | 118 | -------------------------------------------------------------------------------- /octopus/blocktopus/templates/experiment-result.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | Experiment Results 4 | 5 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 39 | 40 |
    41 | 42 | 43 | 44 | 45 | 46 |
      47 |
      48 |
      49 | 50 | 51 | 52 | -------------------------------------------------------------------------------- /octopus/blocktopus/templates/experiment-running.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 32 | 33 |
      34 | 35 | 36 | 37 | 38 | 39 | 40 |
        41 |
        42 |
        43 | 44 | 45 | 46 | -------------------------------------------------------------------------------- /octopus/console.py: -------------------------------------------------------------------------------- 1 | # System Imports 2 | import os 3 | import tty 4 | import sys 5 | import termios 6 | 7 | # Twisted Imports 8 | from twisted.internet import reactor, stdio 9 | from twisted.conch.stdio import ConsoleManhole 10 | from twisted.conch.insults.insults import ServerProtocol 11 | from twisted.python import log 12 | 13 | # Package Imports 14 | from octopus.transport import basic 15 | # from octopus.sequence import shortcuts 16 | 17 | def run (): 18 | log.startLogging(open('child.log', 'w')) 19 | fd = sys.__stdin__.fileno() 20 | oldSettings = termios.tcgetattr(fd) 21 | tty.setraw(fd) 22 | 23 | try: 24 | locals = { 25 | "tcp": basic.tcp, 26 | "serial": basic.serial, 27 | # "s": shortcuts 28 | } 29 | 30 | p = ServerProtocol(ConsoleManhole, namespace = locals) 31 | stdio.StandardIO(p) 32 | reactor.run() 33 | 34 | finally: 35 | termios.tcsetattr(fd, termios.TCSANOW, oldSettings) 36 | os.write(fd, "\r\x1bc\r") 37 | 38 | if __name__ == '__main__': 39 | run() 40 | -------------------------------------------------------------------------------- /octopus/constants.py: -------------------------------------------------------------------------------- 1 | # Twisted Imports 2 | from twisted.python.constants import ValueConstant, Values 3 | 4 | class State (Values): 5 | READY = ValueConstant("ready") 6 | RUNNING = ValueConstant("running") 7 | PAUSED = ValueConstant("paused") 8 | COMPLETE = ValueConstant("complete") 9 | CANCELLED = ValueConstant("cancelled") 10 | ERROR = ValueConstant("error") 11 | 12 | class Event (Values): 13 | NEW_EXPERIMENT = ValueConstant("new-expt") 14 | EXPERIMENT = ValueConstant("e") 15 | INTERFACE = ValueConstant("i") 16 | STEP = ValueConstant("s") 17 | LOG = ValueConstant("l") 18 | TIMEZERO = ValueConstant("z") 19 | -------------------------------------------------------------------------------- /octopus/data/__init__.py: -------------------------------------------------------------------------------- 1 | from .data import Variable, Constant 2 | from . import errors 3 | from . import control 4 | from . import manipulation 5 | -------------------------------------------------------------------------------- /octopus/data/control.py: -------------------------------------------------------------------------------- 1 | """ 2 | Provides controls that appear in the web user interface of an experiment. 3 | 4 | Controls are generally associated with a data.Variable (most commonly 5 | a data.Property) which is updated when the user operates the control. 6 | 7 | UI updates of the control are handled by the Variable. 8 | 9 | A special case is the Button, which is associated with a function rather 10 | than a variable. 11 | """ 12 | 13 | # Package Imports 14 | from ..events import Event 15 | 16 | # Sibling Imports 17 | from . import errors 18 | 19 | class Control (object): 20 | _counter = 0 21 | 22 | type = "control" 23 | 24 | def __init__ (self, variable = None): 25 | Control._counter += 1 26 | self.alias = "control_" + str(Control._counter) 27 | 28 | self._variable = variable 29 | self._disabled = False 30 | 31 | self.event = Event() 32 | 33 | @property 34 | def title (self): 35 | try: 36 | return self._title 37 | except AttributeError: 38 | pass 39 | 40 | try: 41 | return self._variable.title 42 | except AttributeError: 43 | return "" 44 | 45 | @title.setter 46 | def title (self, value): 47 | self._title = value 48 | 49 | @property 50 | def unit (self): 51 | try: 52 | return self._variable.unit 53 | except AttributeError: 54 | return None 55 | 56 | @property 57 | def var_alias (self): 58 | try: 59 | return self._variable.alias 60 | except AttributeError: 61 | return None 62 | 63 | @property 64 | def value (self): 65 | try: 66 | return self._variable.value 67 | except AttributeError: 68 | return None 69 | 70 | def update (self, value): 71 | """Called when a remote client operates the control.""" 72 | 73 | try: 74 | if not self._disabled: 75 | return self._variable.set(value) 76 | 77 | except errors.Error: 78 | raise 79 | 80 | def disable (self): 81 | self._disabled = True 82 | self.event(item = self, disabled = True) 83 | 84 | def enable (self): 85 | self._disabled = False 86 | self.event(item = self, disabled = False) 87 | 88 | class Button (Control): 89 | """A push button control (not associated with a variable).""" 90 | 91 | type = "button" 92 | 93 | action = None 94 | """A function to perform when the button is pressed.""" 95 | 96 | args = [] 97 | """Arguments to be passed to the action function when it is called.""" 98 | 99 | kwargs = {} 100 | """Keywords to be passed to the action function when it is called.""" 101 | 102 | def __init__ (self, title, action = None, *args, **kwargs): 103 | Control.__init__ (self, None) 104 | self._title = title 105 | self.action = action 106 | self.args = args 107 | self.kwargs = kwargs 108 | 109 | def update (self, value): 110 | try: 111 | if not self._disabled: 112 | return self.action(*self.args, **self.kwargs) 113 | except TypeError: 114 | pass 115 | 116 | class Text (Control): 117 | type = "text" 118 | 119 | class Switch (Control): 120 | """ 121 | An on/off switch. 122 | 123 | The options property should be set with a tuple, where the 124 | first item is the value when false, and the second the 125 | value when true. 126 | """ 127 | 128 | type = "switch" 129 | options = (False, True) 130 | 131 | class Select (Control): 132 | """ 133 | A multi-select control. 134 | 135 | The options property should contain a dictionary containing 136 | value => title pairs. A default set of options is created 137 | from the variable's options property, if it exists. 138 | """ 139 | 140 | type = "select" 141 | _options = None 142 | 143 | @property 144 | def options (self): 145 | if self._options is not None: 146 | return self._options 147 | 148 | if hasattr("options", self._variable): 149 | return dict(zip( 150 | self._variable.options, 151 | [s.capitalize() for s in self._variable.options] 152 | )) 153 | 154 | @options.setter 155 | def options (self, value): 156 | self._options = value 157 | 158 | class Number (Control): 159 | type = "number" 160 | 161 | _min = None 162 | _max = None 163 | 164 | @property 165 | def min (self): 166 | if self._min is not None: 167 | return self._min 168 | elif hasattr("min", self._variable): 169 | return self._variable.min 170 | else: 171 | return None 172 | 173 | @min.setter 174 | def min (self, value): 175 | self._min = value 176 | 177 | @property 178 | def max (self): 179 | if self._max is not None: 180 | return self._max 181 | elif hasattr("max", self._variable): 182 | return self._variable.max 183 | else: 184 | return None 185 | 186 | @max.setter 187 | def min (self, value): 188 | self._max = value 189 | -------------------------------------------------------------------------------- /octopus/data/errors.py: -------------------------------------------------------------------------------- 1 | class Error (Exception): 2 | pass 3 | 4 | 5 | class InvalidType (Error): 6 | pass 7 | 8 | 9 | class Immutable (Error): 10 | pass 11 | 12 | 13 | class InvalidValue (Error): 14 | pass 15 | 16 | 17 | class ValueTooSmall (Error): 18 | pass 19 | 20 | 21 | class ValueTooLarge (Error): 22 | pass 23 | -------------------------------------------------------------------------------- /octopus/data/test/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/richardingham/octopus/64c97ec75f6d89f247c01257fa4013d5e83ad693/octopus/data/test/__init__.py -------------------------------------------------------------------------------- /octopus/data/test/test_data.py: -------------------------------------------------------------------------------- 1 | from twisted.internet import defer 2 | from twisted.trial import unittest 3 | 4 | from unittest.mock import Mock 5 | 6 | from .. import data 7 | 8 | class UtilsTestCase (unittest.TestCase): 9 | def setUp (self): 10 | self.x = [x for x in range(1, 5)] 11 | self.y = [x * 2 for x in self.x] 12 | self.min_x = min(self.x) 13 | self.max_x = max(self.x) 14 | 15 | def test_bounds (self): 16 | self.assertEqual(data._upper_bound(self.x, 2), 1) 17 | self.assertEqual(data._upper_bound(self.x, 3.5), 3) 18 | self.assertEqual(data._lower_bound(self.x, 3.5), 2) 19 | self.assertEqual(data._lower_bound(self.x, 4), 3) 20 | 21 | def test_get (self): 22 | # Get all data 23 | self.assertEqual( 24 | data._get(self.x, self.y, self.max_x, self.min_x, None, None), 25 | list(zip(self.x, self.y)) 26 | ) 27 | 28 | # Zero interval 29 | for i in range(self.min_x - 2, self.max_x + 2): 30 | self.assertEqual( 31 | len(data._get(self.x, self.y, self.max_x, self.min_x, i, None)), 32 | 1 33 | ) 34 | 35 | tests = [ 36 | (-2, 1, [(-2, 2), (-1, 2)]), 37 | (-2, 2, [(-2, 2), (0, 2)]), 38 | (-2, 3, [(-2, 2), (1, 2)]), 39 | (-2, 4, [(-2, 2), (1, 2), (2, 4)]), 40 | (-2, 8, [(-2, 2), (1, 2), (2, 4), (3, 6), (4, 8), (6, 8)]), 41 | ( 1, 8, [(1, 2), (2, 4), (3, 6), (4, 8), (9, 8)]), 42 | ( 1, 2, [(1, 2), (2, 4), (3, 6)]), 43 | ( 3, 1, [(3, 6), (4, 8)]), 44 | ( 3, 2, [(3, 6), (4, 8), (5, 8)]), 45 | ( 3, 8, [(3, 6), (4, 8), (11, 8)]) 46 | ] 47 | for start, interval, expected in tests: 48 | self.assertEqual(data._get(self.x, self.y, self.max_x, self.min_x, start, interval), expected) 49 | 50 | 51 | class VariablesTestCase (unittest.TestCase): 52 | def setUp (self): 53 | self.v = data.Variable(int, 2) 54 | 55 | def test_create (self): 56 | self.assertEqual(self.v.type, int) 57 | self.assertEqual(self.v.value, 2) 58 | 59 | def test_events (self): 60 | cleared = Mock() 61 | changed = Mock() 62 | 63 | self.v.on("change", changed) 64 | self.v.on("clear", cleared) 65 | 66 | self.v.set(3) 67 | self.assertEqual(changed.called, True) 68 | 69 | self.v.truncate() 70 | self.assertEqual(cleared.called, True) 71 | self.assertEqual(self.v.value, 3) 72 | 73 | def test_update (self): 74 | self.v._archive.min_delta = 0 75 | self.v.set(3) 76 | self.v.set(4) 77 | self.v.set(5) 78 | self.assertEqual(self.v._y, [2, 3, 4, 5]) 79 | self.assertEqual([y for x, y in self.v.get()], [2, 3, 4, 5]) 80 | 81 | def test_get (self): 82 | v = data.Variable(int) 83 | v._archive.min_delta = 0 84 | v._push(2, 1) 85 | v._push(3, 2) 86 | v._push(4, 3) 87 | v._push(5, 4) 88 | self.assertEqual(v._x, [1, 2, 3, 4]) 89 | self.assertEqual(v._y, [2, 3, 4, 5]) 90 | self.assertEqual(v.get(2, 1), [(2, 3), (3, 4)]) 91 | 92 | class ExpressionsTestCase (unittest.TestCase): 93 | def setUp (self): 94 | self.v = data.Variable(int, 2) 95 | 96 | def test_create (self): 97 | add = self.v + 2 98 | self.assertEqual(add.type, int) 99 | self.assertEqual(add.value, 4) 100 | self.assertEqual(add.__class__.__name__, "AddExpression") 101 | self.assertIn(add._changed, self.v._events["change"]) 102 | 103 | def test_propagate (self): 104 | self.v._archive.min_delta = 0 105 | add = self.v + 2 106 | 107 | changed = Mock() 108 | add.on("change", changed) 109 | 110 | self.v.set(4) 111 | self.assertEqual(changed.called, True) 112 | 113 | self.assertEqual(add.value, 6) 114 | 115 | -------------------------------------------------------------------------------- /octopus/data/test/test_variables.py: -------------------------------------------------------------------------------- 1 | from twisted.internet import defer 2 | from twisted.trial import unittest 3 | 4 | from unittest.mock import Mock 5 | 6 | from .. import data 7 | 8 | class VariablesTestCase (unittest.TestCase): 9 | def test_create (self): 10 | s = data.Variable(str, "abc") 11 | self.assertEqual(s.value, "abc") 12 | 13 | i = data.Variable(int, 1) 14 | self.assertEqual(i.value, 1) 15 | 16 | f = data.Variable(float, 1.5) 17 | self.assertEqual(f.value, 1.5) 18 | 19 | b = data.Variable(bool, False) 20 | self.assertEqual(b.value, False) 21 | 22 | def test_set (self): 23 | s = data.Variable(str, "abc") 24 | s.set("def") 25 | self.assertEqual(s.value, "def") 26 | 27 | i = data.Variable(int, 1) 28 | i.set(2) 29 | self.assertEqual(i.value, 2) 30 | 31 | f = data.Variable(float, 1.5) 32 | f.set(3.7) 33 | self.assertEqual(f.value, 3.7) 34 | 35 | b = data.Variable(bool, False) 36 | b.set(True) 37 | self.assertEqual(b.value, True) 38 | 39 | def test_add (self): 40 | s = data.Variable(str, "abc") 41 | self.assertEqual((s + "def").value, "abcdef") 42 | self.assertEqual((s + 3).value, "abc3") 43 | self.assertEqual((s + True).value, "abcTrue") 44 | 45 | i = data.Variable(int, 1) 46 | self.assertEqual((i + "def").value, "1def") 47 | self.assertEqual((i + 3).value, 4) 48 | self.assertEqual((i + True).value, 2) 49 | 50 | f = data.Variable(float, 1.5) 51 | self.assertEqual((f + "def").value, "1.5def") 52 | self.assertEqual((f + 3).value, 4.5) 53 | self.assertEqual((f + True).value, 2.5) 54 | 55 | b = data.Variable(bool, False) 56 | self.assertEqual((b + "def").value, "Falsedef") 57 | self.assertEqual((b + 3).value, 3) 58 | self.assertEqual((b + True).value, True) 59 | -------------------------------------------------------------------------------- /octopus/events.py: -------------------------------------------------------------------------------- 1 | # System Imports 2 | import functools 3 | 4 | # Twisted Imports 5 | from twisted.python import log 6 | 7 | 8 | class Event (object): 9 | def __init__(self): 10 | self.handlers = set() 11 | 12 | def handle(self, handler): 13 | self.handlers.add(handler) 14 | return self 15 | 16 | def unhandle(self, handler): 17 | self.handlers.discard(handler) 18 | return self 19 | 20 | def fire(self, *args, **kargs): 21 | for handler in self.handlers: 22 | handler(*args, **kargs) 23 | 24 | def getHandlerCount(self): 25 | return len(self.handlers) 26 | 27 | __iadd__ = handle 28 | __isub__ = unhandle 29 | __call__ = fire 30 | __len__ = getHandlerCount 31 | 32 | 33 | class EventEmitter (object): 34 | def on (self, name, function = None): 35 | def _on (function): 36 | try: 37 | self._events[name] 38 | except (TypeError, AttributeError): 39 | self._events = {} 40 | self._events[name] = [] 41 | except KeyError: 42 | self._events[name] = [] 43 | 44 | # Use is instead of in to avoid equality comparison 45 | # (this would create extra expression objects). 46 | for f in self._events[name]: 47 | if function is f: 48 | return function 49 | 50 | self._events[name].append(function) 51 | 52 | return function 53 | 54 | if function is None: 55 | return _on 56 | else: 57 | return _on(function) 58 | 59 | def once (self, name, function = None): 60 | def _once (function): 61 | @functools.wraps(function) 62 | def g (*args, **kwargs): 63 | function(*args, **kwargs) 64 | self.off(name, g) 65 | 66 | return g 67 | 68 | if function is None: 69 | return lambda function: self.on(name, _once(function)) 70 | else: 71 | self.on(name, _once(function)) 72 | 73 | def off (self, name = None, function = None): 74 | try: 75 | self._events 76 | except AttributeError: 77 | return 78 | 79 | # If no name is passed, remove all handlers 80 | if name is None: 81 | self._events.clear() 82 | 83 | # If no function is passed, remove all functions 84 | elif function is None: 85 | try: 86 | self._events[name] = [] 87 | except KeyError: 88 | pass 89 | 90 | # Remove handler [function] from [name] 91 | else: 92 | self._events[name].remove(function) 93 | 94 | def listeners (self, event): 95 | try: 96 | return self._events[event] 97 | except (AttributeError, KeyError): 98 | return [] 99 | 100 | def emit (self, _event, **data): 101 | handled = False 102 | 103 | try: 104 | events = self._events[_event][:] 105 | except AttributeError: 106 | return False # No events defined yet 107 | except KeyError: 108 | pass 109 | else: 110 | handled |= bool(len(events)) 111 | 112 | for function in events: 113 | try: 114 | function(data) 115 | except: 116 | log.err() 117 | 118 | try: 119 | events = self._events["all"][:] 120 | except KeyError: 121 | pass 122 | else: 123 | handled |= bool(len(events)) 124 | 125 | for function in events: 126 | try: 127 | function(_event, data) 128 | except: 129 | log.err() 130 | 131 | return handled 132 | -------------------------------------------------------------------------------- /octopus/image/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/richardingham/octopus/64c97ec75f6d89f247c01257fa4013d5e83ad693/octopus/image/__init__.py -------------------------------------------------------------------------------- /octopus/image/data.py: -------------------------------------------------------------------------------- 1 | # System Imports 2 | from io import BytesIO 3 | from time import time as now 4 | import base64 5 | from urllib.parse import quote 6 | 7 | # Library Imports 8 | import cv2 9 | import numpy 10 | 11 | # Twisted Imports 12 | from twisted.internet import defer 13 | 14 | # Package Imports 15 | from ..data.errors import Immutable 16 | from ..data.data import BaseVariable 17 | 18 | 19 | class ColorSpace: 20 | """ 21 | **SUMMARY** 22 | The colorspace class is used to encapsulate the color space of a given image. 23 | This class acts like C/C++ style enumerated type. 24 | See: http://stackoverflow.com/questions/2122706/detect-color-space-with-opencv 25 | """ 26 | UNKNOWN = 0 27 | BGR = 1 28 | GRAY = 2 29 | RGB = 3 30 | HLS = 4 31 | HSV = 5 32 | XYZ = 6 33 | YCrCb = 7 34 | 35 | 36 | class Image: 37 | data = None 38 | size: int = 0 39 | channels: int = 0 40 | colorspace = None 41 | 42 | def __init__ (self, data: numpy.ndarray, colorspace): 43 | self.data = data 44 | self.height = data.shape[0] 45 | self.width = data.shape[1] 46 | self.colorspace = colorspace 47 | 48 | try: 49 | self.channels = data.shape[2] 50 | except IndexError: 51 | self.channels = 1 52 | 53 | 54 | class BaseImageProperty (BaseVariable): 55 | 56 | @property 57 | def value (self): 58 | return self._value 59 | 60 | def get_value (self): 61 | return self._value 62 | 63 | @property 64 | def type (self): 65 | return Image 66 | 67 | def serialize (self): 68 | if self.alias is None: 69 | return "[Image]" 70 | else: 71 | return str(self.alias) 72 | 73 | def __init__ (self): 74 | self.alias = None 75 | self.title = "" 76 | self._value = None 77 | 78 | def setLogFile (self, logFile): 79 | pass 80 | 81 | def stopLogging (self): 82 | pass 83 | 84 | def __str__ (self): 85 | img = self.get_value() 86 | 87 | if img is None: 88 | return '' 89 | 90 | scaled_x = int(img.width / 4) 91 | scaled_y = int(img.height / 4) 92 | scaled = cv2.resize(img.data, (scaled_x, scaled_y)) 93 | 94 | # Encode 95 | is_success, buffer = cv2.imencode(".png", scaled) 96 | io_buf = BytesIO(buffer) 97 | 98 | encoded = "data:image/png;base64," + quote(base64.b64encode(io_buf.getvalue()).decode()) 99 | 100 | return encoded 101 | 102 | def __repr__ (self): 103 | return "<%s at %s>" % ( 104 | self.__class__.__name__, 105 | hex(id(self)) 106 | ) 107 | 108 | 109 | class ImageProperty (BaseImageProperty): 110 | 111 | def __init__ (self, title, fn): 112 | self.alias = None 113 | self.title = title 114 | self._image_fn = fn 115 | self._value = None 116 | 117 | @defer.inlineCallbacks 118 | def refresh (self): 119 | self._value = yield defer.maybeDeferred(self._image_fn) 120 | self.emit("change", value = None, time = now()) 121 | 122 | def set (self, value): 123 | raise Immutable 124 | 125 | 126 | class DerivedImageProperty (BaseImageProperty): 127 | 128 | def __init__ (self): 129 | self.alias = None 130 | self._value = None 131 | 132 | def set (self, value): 133 | self._value = value 134 | self.emit("change", value = None, time = now()) 135 | 136 | _push = set 137 | 138 | -------------------------------------------------------------------------------- /octopus/image/machine.py: -------------------------------------------------------------------------------- 1 | # Sibling Imports 2 | from .data import ImageProperty 3 | 4 | # Package Imports 5 | from ..machine import Machine 6 | 7 | 8 | class CameraViewer (Machine): 9 | 10 | protocolFactory = None 11 | name = "Monitor a webcam" 12 | 13 | def setup (self): 14 | # setup variables 15 | self.image = ImageProperty(title = "Image", fn = self._get_image) 16 | 17 | # def show (self): 18 | # self._get_image().show() 19 | 20 | def _get_image (self): 21 | return self.protocol.image() 22 | -------------------------------------------------------------------------------- /octopus/image/provider.py: -------------------------------------------------------------------------------- 1 | # Sibling Imports 2 | from .data import ImageProperty 3 | 4 | # Package Imports 5 | from ..machine import Machine 6 | 7 | 8 | class ImageProvider (Machine): 9 | protocolFactory = None 10 | name = "Provide an image from a webcam" 11 | update_frequency = 1 12 | 13 | def setup (self): 14 | # setup variables 15 | self.image = ImageProperty(title = "Tracked", fn = self._getImage) 16 | 17 | def _getImage (self): 18 | return self.protocol.image() 19 | 20 | def start (self): 21 | def monitor (): 22 | self.image.refresh() 23 | 24 | self._tick(monitor, self.update_frequency) 25 | 26 | def stop (self): 27 | self._stopTicks() 28 | 29 | def disconnect (self): 30 | self.stop() 31 | 32 | try: 33 | self.protocol.disconnect() 34 | except AttributeError: 35 | pass 36 | 37 | -------------------------------------------------------------------------------- /octopus/machine/__init__.py: -------------------------------------------------------------------------------- 1 | from .machine import Machine, Component, ComponentList, Property, Stream 2 | from .interface import InterfaceSection as ui 3 | -------------------------------------------------------------------------------- /octopus/machine/test/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/richardingham/octopus/64c97ec75f6d89f247c01257fa4013d5e83ad693/octopus/machine/test/__init__.py -------------------------------------------------------------------------------- /octopus/manufacturer/dummy.py: -------------------------------------------------------------------------------- 1 | # Twisted Imports 2 | from twisted.internet import defer 3 | from twisted.internet.protocol import Factory 4 | 5 | # Package Imports 6 | from ..util import now 7 | from ..machine import Machine, Component, Stream, Property 8 | from ..protocol.basic import QueuedLineReceiver as _qlr 9 | 10 | __all__ = ["Dummy"] 11 | 12 | 13 | class QueuedLineReceiver (_qlr): 14 | 15 | delimiter = b"\n\r" 16 | out_delimiter = b"\n" 17 | 18 | def sendLine (self, line): 19 | """ 20 | Sends a line to the other end of the connection. 21 | 22 | @param line: The line to send, not including the delimiter. 23 | @type line: C{str} 24 | """ 25 | return self.transport.write(line + self.out_delimiter) 26 | 27 | 28 | class Dummy (Machine): 29 | 30 | protocolFactory = Factory.forProtocol(QueuedLineReceiver) 31 | name = "Dummy Machine" 32 | 33 | def setup (self): 34 | pass 35 | 36 | def start (self): 37 | pass 38 | 39 | def stop (self): 40 | pass 41 | 42 | def reset (self): 43 | return defer.succeed('OK') 44 | 45 | def write (self, msg): 46 | return self.protocol.write(msg) 47 | 48 | def hope (self, msg): 49 | return self.protocol.write(msg, expectReply = False) 50 | -------------------------------------------------------------------------------- /octopus/manufacturer/kern.py: -------------------------------------------------------------------------------- 1 | # Twisted Imports 2 | from twisted.internet import defer 3 | from twisted.internet.protocol import Factory 4 | 5 | # Package Imports 6 | from ..machine import Machine, Stream, ui 7 | from ..util import now 8 | from ..protocol.basic import QueuedLineReceiver 9 | 10 | __all__ = ["PCB"] 11 | 12 | 13 | class PCB (Machine): 14 | 15 | protocolFactory = Factory.forProtocol(QueuedLineReceiver) 16 | name = "Kern PCB Balance" 17 | 18 | def setup (self): 19 | 20 | # setup variables 21 | self.weight = Stream(title = "Weight", type = float, unit = "g") 22 | 23 | self.ui = ui( 24 | traces = [], 25 | properties = [ 26 | self.weight 27 | ] 28 | ) 29 | 30 | def start (self): 31 | # setup monitor on a tick to update variables 32 | 33 | def interpret_weight (result): 34 | if result == " Error": 35 | # raise some error 36 | return 37 | 38 | if result[1] == "-": 39 | result = - float(result[2:12].strip()) 40 | else: 41 | result = float(result[1:12].strip()) 42 | 43 | self.weight._push(result, now()) 44 | 45 | def monitor_weight (): 46 | self.protocol.write("w").addCallback(interpret_weight) 47 | 48 | self._tick(monitor_weight, 1) 49 | 50 | def stop (self): 51 | self._stopTicks() 52 | 53 | def reset (self): 54 | return defer.succeed('OK') 55 | 56 | def getStableWeight (self): 57 | d = defer.Deferred() 58 | 59 | def interpret (result): 60 | result = result.strip() 61 | 62 | if result == "Error": 63 | raise Exception("Error fetching stable weight") 64 | else: 65 | return result 66 | 67 | self.protocol.write("s").addCallback(interpret).chainDeferred(d) 68 | 69 | return d 70 | 71 | def tare (self): 72 | return self.protocol.write("t", expectReply = False, wait = 5) 73 | -------------------------------------------------------------------------------- /octopus/manufacturer/mt.py: -------------------------------------------------------------------------------- 1 | # Twisted Imports 2 | from twisted.internet import defer 3 | from twisted.internet.protocol import Factory 4 | 5 | # Package Imports 6 | from ..machine import Machine, Stream, Property, ui 7 | from ..protocol.basic import QueuedLineReceiver 8 | 9 | # System Imports 10 | import json, re 11 | 12 | __all__ = ["SICSBalance"] 13 | 14 | 15 | _SICS_status_text = { 16 | "+": "overload", 17 | "-": "underload", 18 | "I": "busy", 19 | "S": "ok", 20 | } 21 | 22 | def _SICS_interpret_weight (result, self): 23 | result = result.split() 24 | 25 | if len(result) > 1: 26 | try: 27 | status = _SICS_status_text[result[1]] 28 | except KeyError: 29 | pass 30 | else: 31 | self.status._push(status) 32 | 33 | if status == "ok": 34 | unit = result[3] 35 | 36 | if unit == "g": 37 | self.weight._push(float(result[2])) 38 | 39 | 40 | class SICSBalance (Machine): 41 | 42 | protocolFactory = Factory.forProtocol(QueuedLineReceiver) 43 | name = "MT Balance (SICS)" 44 | 45 | def setup (self): 46 | 47 | # setup variables 48 | self.weight = Stream(title = "Weight", type = float, unit = "g") 49 | self.status = Property( 50 | title = "Status", 51 | type = str, 52 | options = ("ok", "busy", "overload", "underload") 53 | ) 54 | 55 | self.ui = ui( 56 | traces = [], 57 | properties = [ 58 | self.weight 59 | ] 60 | ) 61 | 62 | def start (self): 63 | # setup monitor on a tick to update variables 64 | 65 | def monitor_weight (): 66 | self.protocol.write("SI").addCallback(_SICS_interpret_weight, self) 67 | 68 | self._tick(monitor_weight, 1) 69 | 70 | def stop (self): 71 | self._stopTicks() 72 | 73 | def getStableWeight (self): 74 | result = defer.Deferred() 75 | 76 | d = self.protocol.write("S") 77 | d.addCallback(_SICS_interpret_weight, self) 78 | d.chainDeferred(result) 79 | 80 | return result 81 | 82 | def tare (self): 83 | return self.protocol.write("Z", expectReply = False, wait = 5) 84 | 85 | 86 | class ICIR (Machine): 87 | 88 | protocolFactory = Factory.forProtocol(QueuedLineReceiver) 89 | name = "MT iC IR Connector" 90 | 91 | def setup (self, stream_names = None): 92 | 93 | streams = [] 94 | self._streams = {} 95 | 96 | # setup variables 97 | for i in stream_names: 98 | safe_name = re.sub(r"[^a-zA-Z0-9_]", "", i) 99 | if not re.match(r"^[a-zA-Z]", safe_name): 100 | safe_name = "stream_" + safe_name 101 | 102 | stream = Stream(title = i, type = float, unit = "mAU") 103 | streams.append(stream) 104 | self._streams[i] = stream 105 | 106 | setattr(self, safe_name, stream) 107 | 108 | self.ui = ui( 109 | traces = [], 110 | properties = streams 111 | ) 112 | 113 | def start (self): 114 | # setup monitor on a tick to update variables 115 | 116 | self._last_time = None 117 | 118 | def interpret_data (result): 119 | data = json.loads(result) 120 | 121 | if data["time"] is None: 122 | return 123 | 124 | time = data["time"] / 1000 125 | 126 | if time == self._last_time: 127 | return 128 | else: 129 | self._last_time = time 130 | 131 | for i in range(len(data["streams"])): 132 | datum = data["streams"][i] 133 | name = datum["name"] 134 | value = float(datum["value"]) * 1000 135 | 136 | try: 137 | self._streams[name]._push(value, time) 138 | except KeyError: 139 | try: 140 | self._streams["stream_%s" % name]._push(value, time) 141 | except KeyError: 142 | pass 143 | 144 | def monitor_data (): 145 | return self.protocol.write("requestData").addCallback(interpret_data) 146 | 147 | self._tick(monitor_data, 1) 148 | 149 | return monitor_data() 150 | 151 | def stop (self): 152 | self._stopTicks() 153 | -------------------------------------------------------------------------------- /octopus/manufacturer/omega.py: -------------------------------------------------------------------------------- 1 | # Twisted Imports 2 | from twisted.internet import defer 3 | from twisted.internet.protocol import Factory 4 | from twisted.python import log 5 | 6 | # Package Imports 7 | from ..machine import Machine, Stream, ui 8 | from ..util import now 9 | from ..protocol.basic import VaryingDelimiterQueuedLineReceiver 10 | 11 | __all__ = ["HH306A"] 12 | 13 | 14 | # Connection: 9600, 8N1 15 | class HH306A (Machine): 16 | 17 | protocolFactory = Factory.forProtocol(VaryingDelimiterQueuedLineReceiver) 18 | name = "Omega HH306A Thermometer Data Logger" 19 | 20 | def setup (self): 21 | 22 | # setup variables 23 | self.temp1 = Stream(title = "Temperature 1", type = float, unit = "C") 24 | self.temp2 = Stream(title = "Temperature 2", type = float, unit = "C") 25 | 26 | self.ui = ui( 27 | traces = [], 28 | properties = [ 29 | self.temp1, 30 | self.temp2 31 | ] 32 | ) 33 | 34 | @defer.inlineCallbacks 35 | def start (self): 36 | # Set protocol defaults 37 | self.protocol.send_delimiter = '' 38 | self.protocol.start_delimiter = '\x02' 39 | self.protocol.end_delimiter = '\x03' 40 | 41 | # Check that the correct device is connected 42 | model_no = yield self.protocol.write( 43 | "K", 44 | length = 3, 45 | start_delimiter = '', 46 | end_delimiter = '\r' 47 | ) 48 | 49 | if model_no != "306": 50 | raise Exception("HH306A: Expected model '306', received '{:s}'".format(model_no)) 51 | 52 | data = yield self.protocol.write("A", length = 8) 53 | info = [(ord(data[0]) & (1 << i)) > 0 for i in range(8)] 54 | 55 | # Check if in MAX/MIN mode: 56 | if info[1] or info[2]: 57 | yield self.protocol.write("N", expect_reply = False) 58 | 59 | # Check if displaying time: 60 | if info[3]: 61 | yield self.protocol.write("T", expect_reply = False) 62 | 63 | # Check if in HOLD mode: 64 | if info[5]: 65 | yield self.protocol.write("H", expect_reply = False) 66 | 67 | def interpret_data (result): 68 | byte_2 = ord(result[0]) 69 | byte_3 = ord(result[1]) 70 | 71 | showing_time = byte_2 & 8 > 0 72 | in_f = byte_2 & 128 == 0 73 | 74 | t1_sign = -1 if (byte_3 & 2 > 0) else 1 75 | t1_factor = 1.0 if (byte_3 & 4 > 0) else 0.1 76 | 77 | t1 = ( 78 | (int(hex(ord(result[2]))[2:].strip('b')) * 100) + 79 | int(hex(ord(result[3]))[2:].strip('b')) 80 | ) * t1_factor * t1_sign 81 | 82 | if not showing_time: 83 | t2_sign = -1 if (byte_3 & 16 > 0) else -1 84 | t2_factor = 1.0 if (byte_3 & 32 > 0) else 0.1 85 | t2 = ( 86 | (int(hex(ord(result[6]))[2:].strip('b')) * 100) + 87 | int(hex(ord(result[7]))[2:].strip('b')) 88 | ) * t2_factor * t2_sign 89 | 90 | if in_f: 91 | t2 = round((t2 - 32.0) * 5.0 / 9.0, 1) 92 | 93 | self.temp2._push(t2) 94 | 95 | if in_f: 96 | t1 = round((t1 - 32.0) * 5.0 / 9.0, 1) 97 | 98 | self.temp1._push(t1) 99 | 100 | def monitor (): 101 | return self.protocol.write( 102 | "A", 103 | length = 8 104 | ).addCallback(interpret_data).addErrback(log.err) 105 | 106 | yield monitor() 107 | 108 | self._tick(monitor, 1) 109 | 110 | def stop (self): 111 | self._stopTicks() 112 | -------------------------------------------------------------------------------- /octopus/manufacturer/startech.py: -------------------------------------------------------------------------------- 1 | # Twisted Imports 2 | from twisted.internet import defer 3 | from twisted.internet.protocol import Factory 4 | from twisted.python import log 5 | 6 | # Package Imports 7 | from ..machine import Machine, Stream, Property, ui 8 | from ..util import now 9 | from ..protocol.basic import VaryingDelimiterQueuedLineReceiver 10 | 11 | __all__ = ["PowerRemoteControl"] 12 | 13 | 14 | class PowerRemoteControl (Machine): 15 | 16 | protocolFactory = Factory.forProtocol(VaryingDelimiterQueuedLineReceiver) 17 | name = "StarTech Power Remote Control AC Switch" 18 | 19 | def setup (self): 20 | self.bankNumber = 1 21 | 22 | def _setPort (portNumber): 23 | @defer.inlineCallbacks 24 | def setPort (value): 25 | if value == "on": 26 | cmd = "ON" 27 | else: 28 | cmd = "OF" 29 | 30 | result = yield self.protocol.write( 31 | "{:s} {:d} {:d}".format(cmd, self.bankNumber, portNumber), 32 | end_delimiter = '\n\n\r>\x08>' 33 | ) 34 | 35 | error_test = result.split('\r\n')[1].strip()[0:6] 36 | if error_test not in ('Usage: ', 'Bad Com'): 37 | 38 | success_test = result.split('\r\n\x08 ')[1].split() 39 | 40 | if success_test[1] == value\ 41 | and success_test[3] == str(portNumber): 42 | getattr(self, 'port' + str(portNumber))._push(value) 43 | defer.returnValue('OK') 44 | 45 | raise Exception('Could not set power {:s} for port {:d}'.format(value, portNumber)) 46 | 47 | return setPort 48 | 49 | # setup variables 50 | self.port1 = Property(title = "Port 1 Power", type = str, options = ("on", "off"), setter = _setPort(1)) 51 | self.port2 = Property(title = "Port 2 Power", type = str, options = ("on", "off"), setter = _setPort(2)) 52 | self.port3 = Property(title = "Port 3 Power", type = str, options = ("on", "off"), setter = _setPort(3)) 53 | self.port4 = Property(title = "Port 4 Power", type = str, options = ("on", "off"), setter = _setPort(4)) 54 | self.port5 = Property(title = "Port 5 Power", type = str, options = ("on", "off"), setter = _setPort(5)) 55 | self.port6 = Property(title = "Port 6 Power", type = str, options = ("on", "off"), setter = _setPort(6)) 56 | self.port7 = Property(title = "Port 7 Power", type = str, options = ("on", "off"), setter = _setPort(7)) 57 | self.port8 = Property(title = "Port 8 Power", type = str, options = ("on", "off"), setter = _setPort(8)) 58 | 59 | self.current = Stream(title = "Current", type = float) 60 | 61 | self.ui = ui( 62 | traces = [], 63 | properties = [ 64 | self.port1, 65 | self.port2, 66 | self.port3, 67 | self.port4, 68 | self.port5, 69 | self.port6, 70 | self.port7, 71 | self.port8 72 | ] 73 | ) 74 | 75 | @defer.inlineCallbacks 76 | def start (self): 77 | self.protocol.delimiter = '\r' 78 | self.protocol.end_delimiter = '\x08>' 79 | self.protocol.character_delay = 0.001 80 | 81 | result = yield self.protocol.write('\r', end_delimiter = '\r\n>') 82 | if not (result == '' or result.split('\r\n')[-2].strip() != 'Serial Command Mode.....connected'): 83 | raise Exception('Failed to connect') 84 | 85 | # Remove any timers 86 | yield self.protocol.write('TQ 1 0') 87 | 88 | def interpret_status (result): 89 | body = result.split('\r\n\x08 ')[1].split('\n\r') 90 | 91 | for row in body[3:11]: 92 | row = row.split() 93 | prop = getattr(self, 'port' + row[0]) 94 | prop._push('on' if row[1] == 'ON' else 'off') 95 | 96 | self.current._push(body[0].split('current: ')[1]) 97 | 98 | def monitor (): 99 | return self.protocol.write("ST 1")\ 100 | .addCallback(interpret_status)\ 101 | .addErrback(log.err) 102 | 103 | yield self.protocol.write("ST 1").addCallback(interpret_status) 104 | 105 | self._tick(monitor, 1) 106 | 107 | def stop (self): 108 | self._stopTicks() 109 | 110 | def reset (self): 111 | return self.protocol.write('OF 1 0') 112 | -------------------------------------------------------------------------------- /octopus/manufacturer/vici.py: -------------------------------------------------------------------------------- 1 | """ 2 | 3 | """ 4 | 5 | # Twisted Imports 6 | from twisted.internet import defer 7 | from twisted.internet.protocol import Factory 8 | 9 | # Package Imports 10 | from ..machine import Machine, Property, ui 11 | from ..protocol.basic import QueuedLineReceiver 12 | from ..util import now 13 | 14 | __all__ = ["MultiValve"] 15 | 16 | class LineReceiver (QueuedLineReceiver): 17 | timeoutDuration = 4.5 18 | delimiter = b"\r" 19 | 20 | class MultiValve (Machine): 21 | 22 | protocolFactory = Factory.forProtocol(LineReceiver) 23 | name = "Valco Multiposition Valve" 24 | 25 | def setup (self): 26 | 27 | # Number of positions 28 | self.num_positions = 0 29 | 30 | def _set_position (pos): 31 | return self.move(pos) 32 | 33 | # setup variables 34 | self.position = Property(title = "Position", type = int, setter = _set_position) 35 | 36 | self.ui = ui( 37 | properties = [self.position] 38 | ) 39 | 40 | _move_commands = { 41 | "c": "CW", 42 | "cw": "CW", 43 | "clockwise": "CW", 44 | "a": "CC", 45 | "cc": "CC", 46 | "counterclockwise": "CC", 47 | "anticlockwise": "CC", 48 | "f": "GO", 49 | "fastest": "GO" 50 | } 51 | 52 | @defer.inlineCallbacks 53 | def _getPosition (self): 54 | result = yield self.protocol.write("CP") 55 | try: 56 | defer.returnValue(int(result.split("=")[1])) 57 | except ValueError: 58 | return 59 | 60 | @defer.inlineCallbacks 61 | def move (self, position, direction = "f"): 62 | if direction not in self._move_commands: 63 | raise "Invalid direction" 64 | 65 | command = self._move_commands[direction] 66 | 67 | # Make sure position is between 1 and NP 68 | position = ((int(position) - 1) % self.num_positions) + 1 69 | 70 | if position == self.position.value: 71 | current_position = yield self._getPosition() 72 | if position == current_position: 73 | defer.returnValue('OK') 74 | 75 | yield self.protocol.write("%s%d" % (command, position), expectReply = False) 76 | new_position = yield self._getPosition() 77 | 78 | if new_position is None: 79 | # If there was an error in the move command, this will have been received 80 | # by the CP command due to the expectReply = False. 81 | # Read the current position again 82 | new_position = yield self._getPosition() 83 | 84 | if new_position != position: 85 | raise Exception("Move Failed") 86 | else: 87 | self.position._push(new_position) 88 | defer.returnValue("OK") 89 | 90 | def advance (self, positions): 91 | return self.move(int(self.position) + positions) 92 | 93 | def start (self): 94 | # Discover the number of positions 95 | def interpretPositions (result): 96 | self.num_positions = int(result.split("=")[1]) 97 | 98 | # Discover the current positions 99 | def interpretPosition (result): 100 | self.position._push(int(result.split("=")[1])) 101 | 102 | return defer.gatherResults([ 103 | self.protocol.write("NP").addCallback(interpretPositions), 104 | self.protocol.write("CP").addCallback(interpretPosition) 105 | ]) 106 | 107 | def reset (self): 108 | return defer.gatherResults([ 109 | self.position.set(1) 110 | ]) 111 | -------------------------------------------------------------------------------- /octopus/notifier/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/richardingham/octopus/64c97ec75f6d89f247c01257fa4013d5e83ad693/octopus/notifier/__init__.py -------------------------------------------------------------------------------- /octopus/notifier/sms.py: -------------------------------------------------------------------------------- 1 | # Twisted Imports 2 | from twisted.internet import reactor, protocol, defer 3 | from twisted.web.client import Agent 4 | from twisted.web.http_headers import Headers 5 | from twisted.internet.ssl import ClientContextFactory 6 | from twisted.python import log 7 | 8 | # System Imports 9 | try: 10 | from urllib.parse import urlencode 11 | except ImportError: 12 | from urllib import urlencode 13 | 14 | # Sibling Imports 15 | import util as notifier_util 16 | 17 | class WebClientContextFactory(ClientContextFactory): 18 | def getContext(self, hostname, port): 19 | return ClientContextFactory.getContext(self) 20 | 21 | class _Receiver (protocol.Protocol): 22 | def __init__ (self, d): 23 | self.buf = '' 24 | self.d = d 25 | 26 | def dataReceived (self, data): 27 | self.buf += data 28 | 29 | def connectionLost (self, reason): 30 | # TODO: test if reason is twisted.web.client.ResponseDone, if not, do an errback 31 | self.d.callback(self.buf) 32 | 33 | class ClockworkSMS (object): 34 | def __init__ (self, api_key): 35 | contextFactory = WebClientContextFactory() 36 | self.agent = Agent(reactor, contextFactory) 37 | self._api_key = api_key 38 | 39 | def notify (self, destination, message): 40 | 41 | destinations = destination.split(",") 42 | 43 | if len(destinations) > 50: 44 | log.msg("Max 50 SMS recipients allowed") 45 | 46 | params = { 47 | "key": self._api_key, 48 | "to": destination, 49 | "content": message.encode("utf_8", "replace") 50 | } 51 | 52 | uri = "https://api.clockworksms.com/http/send.aspx?{:s}" 53 | 54 | d = self.agent.request( 55 | "GET", 56 | uri.format(urlencode(params)), 57 | Headers({ 58 | 'User-Agent': ['octopus'], 59 | }), 60 | None 61 | ) 62 | 63 | def handle_response (response): 64 | d = defer.Deferred() 65 | response.deliverBody(_Receiver(d)) 66 | 67 | return d 68 | 69 | d.addCallback(handle_response) 70 | 71 | return d 72 | -------------------------------------------------------------------------------- /octopus/protocol/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/richardingham/octopus/64c97ec75f6d89f247c01257fa4013d5e83ad693/octopus/protocol/__init__.py -------------------------------------------------------------------------------- /octopus/queue.py: -------------------------------------------------------------------------------- 1 | # Twisted Imports 2 | from twisted.internet import reactor, defer 3 | 4 | # System Imports 5 | from collections import deque 6 | import functools 7 | 8 | # Sibling Imports 9 | from .events import Event 10 | 11 | 12 | class AsyncQueue (object): 13 | @property 14 | def running (self): 15 | return self._workers > 0 16 | 17 | @property 18 | def current (self): 19 | return self._current 20 | 21 | def __init__ (self, worker, concurrency = 1, paused = False): 22 | self._tasks = deque() 23 | self._worker = worker 24 | self._workers = 0 25 | self._concurrency = concurrency 26 | self._paused = int(paused) 27 | self._current = set() 28 | 29 | self.drained = Event() 30 | 31 | def pause (self): 32 | self._paused += 1 33 | 34 | def resume (self): 35 | self._paused -= 1 36 | self._process() 37 | 38 | def append (self, data): 39 | task = _AsyncQueueTask(data) 40 | self._tasks.append(task) 41 | reactor.callLater(0, self._process) 42 | return task.d 43 | 44 | def appendleft (self, data): 45 | task = _AsyncQueueTask(data) 46 | self._tasks.appendleft(task) 47 | reactor.callLater(0, self._process) 48 | return task.d 49 | 50 | def _process (self): 51 | if not self._paused and self._workers < self._concurrency: 52 | def run (task): 53 | worker_d = defer.maybeDeferred(self._worker, task.data) 54 | worker_d.addCallbacks(success, error) 55 | 56 | def success (result): 57 | task.d.callback(result) 58 | next() 59 | 60 | def error (reason): 61 | if reason.type is AsyncQueueRetry: 62 | run(task) 63 | else: 64 | task.d.errback(reason) 65 | next() 66 | 67 | def next (): 68 | self._workers -= 1 69 | self._current.discard(task) 70 | reactor.callLater(0, self._process) 71 | 72 | try: 73 | task = self._tasks.popleft() 74 | except IndexError: 75 | self.drained() 76 | else: 77 | self._workers += 1 78 | self._current.add(task) 79 | run(task) 80 | 81 | def __len__ (self): 82 | return len(self._tasks) 83 | 84 | 85 | class AsyncQueueRetry (Exception): 86 | pass 87 | 88 | 89 | class _AsyncQueueTask (object): 90 | def __init__ (self, data, deferred = None): 91 | self.data = data 92 | self.d = deferred or defer.Deferred() 93 | -------------------------------------------------------------------------------- /octopus/sequence/__init__.py: -------------------------------------------------------------------------------- 1 | from .sequence import * 2 | # from . import util 3 | -------------------------------------------------------------------------------- /octopus/sequence/error.py: -------------------------------------------------------------------------------- 1 | 2 | class Error (Exception): 3 | """Base class for exceptions in this module.""" 4 | pass 5 | 6 | class NotRunning (Error): 7 | """Exception raised if an attempt is made to stop a step 8 | which has not started running yet.""" 9 | pass 10 | 11 | class AlreadyRunning (Error): 12 | """Exception raised if an attempt is made to start a step 13 | which is currently running.""" 14 | pass 15 | 16 | class NotPaused (Error): 17 | """Exception raised if an attempt is made to resume a step 18 | which is not paused.""" 19 | pass 20 | 21 | class Stopped (Exception): 22 | """Exception raised if stop() is called on a step.""" 23 | pass 24 | -------------------------------------------------------------------------------- /octopus/sequence/runtime.py: -------------------------------------------------------------------------------- 1 | # System Imports 2 | import os 3 | 4 | # Twised Imports 5 | from twisted.internet import reactor, defer 6 | from twisted.python.failure import Failure 7 | 8 | # Sibling Imports 9 | from .sequence import Step 10 | from .. import data 11 | from . import experiment 12 | from ..machine import Machine 13 | from .shortcuts import * 14 | 15 | _experiment = experiment.Experiment() 16 | 17 | ## inject machine registration 18 | _old_machine_init = Machine.__init__ 19 | 20 | def _new_machine_init (self, *a, **k): 21 | _old_machine_init(self, *a, **k) 22 | _experiment.register_machine(self) 23 | 24 | Machine.__init__ = _new_machine_init 25 | 26 | 27 | def id (id): 28 | _experiment.id = id 29 | 30 | 31 | def title (title): 32 | _experiment.title = title 33 | 34 | 35 | def chdir (dir): 36 | return os.chdir(dir) 37 | 38 | 39 | def variable (value, alias, title, unit = ""): 40 | v = experiment.Variable(title, type(value), unit = unit) 41 | v.alias = alias 42 | v.set(value) 43 | 44 | return v 45 | 46 | 47 | def derived (expr, alias, title, unit = ""): 48 | expr.title = title 49 | expr.alias = alias 50 | 51 | return expr 52 | 53 | 54 | def constant (value, alias, title, unit = ""): 55 | v = data.Constant(value) 56 | v.title = title 57 | v.alias = alias 58 | v.unit = unit 59 | 60 | return v 61 | 62 | 63 | def log_variables (*variables): 64 | _experiment.log_variables(*variables) 65 | 66 | 67 | def run (step): 68 | started_reactor = False 69 | 70 | def _finished (result): 71 | if started_reactor: 72 | reactor.stop() 73 | 74 | if isinstance(result, Failure): 75 | raise result 76 | 77 | def _run (): 78 | d = _experiment.run() 79 | d.addBoth(_finished) 80 | 81 | if step is not None: 82 | _experiment.step = step 83 | 84 | reactor.callWhenRunning(_run) 85 | 86 | if reactor.running is False: 87 | started_reactor = True 88 | reactor.run() 89 | 90 | 91 | def run_later (step): 92 | if step is not None: 93 | _experiment.step = step 94 | 95 | if reactor.running is False: 96 | reactor.run() 97 | 98 | 99 | def log_output (name): 100 | return SetLogOutputStep(name) 101 | 102 | 103 | class SetLogOutputStep (Step): 104 | def _run (self): 105 | Step._run(self) 106 | _experiment.set_log_output(str(self._expr)) 107 | return self._complete(self._expr) 108 | -------------------------------------------------------------------------------- /octopus/sequence/shortcuts.py: -------------------------------------------------------------------------------- 1 | # System Imports 2 | import os 3 | 4 | # Twisted Imports 5 | from twisted.internet import reactor, defer 6 | 7 | # Sibling Imports 8 | from . import sequence as s 9 | from ..data import Variable 10 | 11 | # To implement: 12 | # with(sets, stmt) 13 | 14 | ## Helper functions 15 | 16 | def sequence (*steps): 17 | return s.Sequence(steps) 18 | 19 | 20 | def parallel (*steps): 21 | return s.Parallel(steps) 22 | 23 | 24 | def set (var, expr): 25 | if not isinstance(var, Variable): 26 | raise Exception('set(): first argument must be a Variable') 27 | 28 | return s.SetStep(var, expr) 29 | 30 | 31 | def increment (var): 32 | if not isinstance(var, Variable): 33 | raise Exception('increment(): argument must be a Variable') 34 | 35 | return s.SetStep(var, var + 1) 36 | 37 | 38 | def decrement (var): 39 | if not isinstance(var, Variable): 40 | raise Exception('increment(): argument must be a Variable') 41 | 42 | return s.SetStep(var, var - 1) 43 | 44 | 45 | def wait (time): 46 | return s.WaitStep(time) 47 | 48 | 49 | def wait_until (test): 50 | return s.WaitUntilStep(test) 51 | 52 | 53 | def loop_while (test, stmt, min_calls = 0): 54 | return s.WhileStep(test, stmt, min_calls) 55 | 56 | 57 | def loop_until (test, stmt, min_calls = 0): 58 | return loop_while(test == False, stmt, min_calls) 59 | 60 | 61 | def on (test, stmt, max_calls = None): 62 | return s.OnStep(test, stmt, max_calls) 63 | 64 | 65 | def once (test, stmt): 66 | return on(test, stmt, 1) 67 | 68 | 69 | def tick (stmt, interval, now = True, max_calls = None): 70 | return s.TickStep(stmt, interval, now, max_calls) 71 | 72 | 73 | def call (fn, *args, **kwargs): 74 | return s.CallStep(fn, *args, **kwargs) 75 | 76 | 77 | def do_if (test, stmt_true, stmt_false = None): 78 | if stmt_false is None: 79 | stmt_false = sequence() 80 | 81 | return s.IfStep(test, stmt_true, stmt_false) 82 | 83 | 84 | def cancel (step): 85 | return s.CancelStep(step) 86 | 87 | 88 | def log (message): 89 | return s.LogStep(message) 90 | 91 | def with_dependents (step, dependents): 92 | for dependent in dependents: 93 | step.dependents.add(dependent) 94 | 95 | return step 96 | -------------------------------------------------------------------------------- /octopus/sequence/test/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/richardingham/octopus/64c97ec75f6d89f247c01257fa4013d5e83ad693/octopus/sequence/test/__init__.py -------------------------------------------------------------------------------- /octopus/sequence/test/test_control.py: -------------------------------------------------------------------------------- 1 | from twisted.internet import defer 2 | from twisted.trial import unittest 3 | 4 | from unittest.mock import Mock 5 | 6 | from .. import sequence 7 | from .. import control 8 | from ...data import data 9 | 10 | class BindTestCase (unittest.TestCase): 11 | def test_boundVariable (self): 12 | v = data.Variable(int, 0) 13 | d = data.Variable(bool, False) 14 | 15 | s = sequence.Sequence([ 16 | sequence.LogStep("Running"), 17 | sequence.WhileStep(v < 10, [ 18 | sequence.SetStep(v, v + 1), 19 | sequence.LogStep("v = " + v + "; d = " + d), 20 | sequence.WaitStep(0.2), 21 | ]), 22 | sequence.LogStep("Complete"), 23 | ]) 24 | 25 | d_ctrl = control.Bind(d, v, lambda x: x > 5) 26 | s.dependents.add(d_ctrl) 27 | 28 | messages = [] 29 | 30 | @s.on("log") 31 | def onLog (data): 32 | messages.append(data['message']) 33 | 34 | def test (result): 35 | self.assertEqual(messages, [ 36 | 'Running', 37 | 'v = 1; d = False', 38 | 'v = 2; d = False', 39 | 'v = 3; d = False', 40 | 'v = 4; d = False', 41 | 'v = 5; d = False', 42 | 'v = 6; d = True', 43 | 'v = 7; d = True', 44 | 'v = 8; d = True', 45 | 'v = 9; d = True', 46 | 'v = 10; d = True', 47 | 'Complete' 48 | ]) 49 | 50 | return s.run().addCallback(test) 51 | -------------------------------------------------------------------------------- /octopus/sequence/test/test_sequence.py: -------------------------------------------------------------------------------- 1 | from twisted.internet import defer 2 | from twisted.trial import unittest 3 | 4 | from unittest.mock import Mock 5 | 6 | from .. import sequence 7 | from ...data import data 8 | 9 | class SequenceTestCase (unittest.TestCase): 10 | def test_simpleSequence (self): 11 | s = sequence.Sequence([ 12 | sequence.LogStep("one"), 13 | sequence.Sequence([ 14 | sequence.LogStep("two"), 15 | sequence.LogStep("three"), 16 | ]), 17 | sequence.WaitStep(0.5), 18 | sequence.LogStep("four"), 19 | ]) 20 | 21 | messages = [] 22 | 23 | @s.on("log") 24 | def onLog (data): 25 | messages.append(data['message']) 26 | 27 | def test (result): 28 | self.assertEqual(messages, ['one', 'two', 'three', 'four']) 29 | 30 | return s.run().addCallback(test) 31 | 32 | class ParallelTestCase (unittest.TestCase): 33 | def test_parallel (self): 34 | s = sequence.Parallel([ 35 | sequence.LogStep("one"), 36 | sequence.Sequence([ 37 | sequence.WaitStep(0.2), 38 | sequence.LogStep("two"), 39 | sequence.WaitStep(0.4), 40 | sequence.LogStep("four"), 41 | ]), 42 | sequence.Sequence([ 43 | sequence.WaitStep(0.4), 44 | sequence.LogStep("three"), 45 | ]), 46 | sequence.LogStep("final"), 47 | ]) 48 | 49 | messages = [] 50 | 51 | @s.on("log") 52 | def onLog (data): 53 | messages.append(data['message']) 54 | 55 | def test (result): 56 | self.assertEqual(messages, ['one', 'final', 'two', 'three', 'four']) 57 | 58 | return s.run().addCallback(test) 59 | 60 | class WhileTestCase (unittest.TestCase): 61 | def test_while (self): 62 | v = data.Variable(int, 0) 63 | 64 | w = sequence.WhileStep(v < 5, [ 65 | sequence.SetStep(v, v + 1) 66 | ]) 67 | 68 | def test (result): 69 | self.assertEqual(v.value, 5) 70 | 71 | return w.run().addCallback(test) 72 | 73 | def test_whileMinCalls (self): 74 | v = data.Variable(int, 0) 75 | 76 | w = sequence.WhileStep(v < 2, [ 77 | sequence.SetStep(v, v + 1) 78 | ], min_calls = 5) 79 | 80 | def test (result): 81 | self.assertEqual(v.value, 5) 82 | 83 | return w.run().addCallback(test) 84 | 85 | class WaitUntilTestCase (unittest.TestCase): 86 | def test_waituntil (self): 87 | v = data.Variable(int, 0) 88 | expr = (v > 0) 89 | 90 | s = sequence.Parallel([ 91 | sequence.Sequence([ 92 | sequence.WaitUntilStep(expr), 93 | sequence.LogStep("changed"), 94 | ]), 95 | sequence.Sequence([ 96 | sequence.WaitStep(0.1), 97 | sequence.LogStep("one"), 98 | sequence.WaitStep(0.1), 99 | sequence.LogStep("two"), 100 | sequence.WaitStep(0.1), 101 | sequence.LogStep("three"), 102 | sequence.WaitStep(0.1), 103 | sequence.LogStep("four"), 104 | ]), 105 | sequence.Sequence([ 106 | sequence.WaitStep(0.25), 107 | sequence.LogStep("set"), 108 | sequence.SetStep(v, 1), 109 | ]) 110 | ]) 111 | 112 | messages = [] 113 | 114 | @s.on("log") 115 | def onLog (data): 116 | messages.append(data['message']) 117 | 118 | def test (result): 119 | self.assertEqual(messages, ['one', 'two', 'set', 'changed', 'three', 'four']) 120 | 121 | return s.run().addCallback(test) 122 | 123 | class CallTestCase (unittest.TestCase): 124 | def test_call (self): 125 | fn = Mock() 126 | 127 | s = sequence.CallStep(fn, 1, 2, arg = 3) 128 | 129 | def test (result): 130 | fn.assert_called_once_with(1, 2, arg = 3) 131 | 132 | return s.run().addCallback(test) 133 | -------------------------------------------------------------------------------- /octopus/test/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/richardingham/octopus/64c97ec75f6d89f247c01257fa4013d5e83ad693/octopus/test/__init__.py -------------------------------------------------------------------------------- /octopus/transport/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/richardingham/octopus/64c97ec75f6d89f247c01257fa4013d5e83ad693/octopus/transport/__init__.py -------------------------------------------------------------------------------- /octopus/transport/basic.py: -------------------------------------------------------------------------------- 1 | # Zope Imports 2 | from zope.interface import implementer 3 | 4 | # Twisted Imports 5 | from twisted.internet import reactor, defer, serialport 6 | from twisted.internet.endpoints import TCP4ClientEndpoint 7 | from twisted.internet.interfaces import IAddress 8 | from twisted.python.util import FancyEqMixin 9 | 10 | # 11 | # Transports have a connect() function, taking a protocolFactory 12 | # object as the single argument. This should return an IProtocol 13 | # object or equivalent (or a deferred). 14 | # 15 | 16 | class tcp (object): 17 | def __init__ (self, host, port): 18 | self.point = TCP4ClientEndpoint(reactor, host, port) 19 | self.name = "tcp({!s}, {!s})".format(host, port) 20 | 21 | def connect (self, factory): 22 | return self.point.connect(factory) 23 | 24 | 25 | @implementer(IAddress) 26 | class SerialAddress (FancyEqMixin, object): 27 | """ 28 | Object representing a UNIX socket endpoint. 29 | 30 | @ivar name: The filename associated with this socket. 31 | @type name: C{str} 32 | """ 33 | 34 | compareAttributes = ('port', ) 35 | 36 | def __init__(self, port): 37 | self.port = port 38 | 39 | def __repr__(self): 40 | return 'SerialAddress({!r})'.format(self.port) 41 | 42 | def __hash__(self): 43 | if self.port is None: 44 | return hash((self.__class__, None)) 45 | else: 46 | return hash(self.port) 47 | 48 | 49 | class serial (object): 50 | 51 | PARITY_NONE = serialport.PARITY_NONE 52 | PARITY_EVEN = serialport.PARITY_EVEN 53 | PARITY_ODD = serialport.PARITY_ODD 54 | STOPBITS_ONE = serialport.STOPBITS_ONE 55 | STOPBITS_TWO = serialport.STOPBITS_TWO 56 | FIVEBITS = serialport.FIVEBITS 57 | SIXBITS = serialport.SIXBITS 58 | SEVENBITS = serialport.SEVENBITS 59 | EIGHTBITS = serialport.EIGHTBITS 60 | 61 | _factory = serialport.SerialPort 62 | _serial = None 63 | 64 | def __init__ (self, port, baudrate = 19200, **args): 65 | self._args = args 66 | self.port = port 67 | self.baudrate = baudrate 68 | self.name = "serial({!s})".format(port) 69 | 70 | def connect (self, factory): 71 | addr = SerialAddress(self.port) 72 | protocol = factory.buildProtocol(addr) 73 | 74 | self._serial = self._factory(protocol, self.port, reactor, self.baudrate, **self._args) 75 | 76 | return protocol 77 | -------------------------------------------------------------------------------- /octopus/transport/gsioc.py: -------------------------------------------------------------------------------- 1 | # Twisted Imports 2 | from twisted.internet import defer 3 | 4 | class Slave (object): 5 | def __init__ (self, immediate_command, buffered_command, name): 6 | self.immediate_command = immediate_command 7 | self.buffered_command = buffered_command 8 | self.name = name 9 | 10 | def connect (self, protocolFactory): 11 | return defer.succeed(self) 12 | -------------------------------------------------------------------------------- /octopus/transport/phidgets.py: -------------------------------------------------------------------------------- 1 | # Twisted Imports 2 | from twisted.internet import reactor, defer, task 3 | from twisted.internet.interfaces import IAddress 4 | from twisted.python import log 5 | 6 | # Zope Imports 7 | from zope.interface import implementer 8 | 9 | # Phidget Imports 10 | from Phidgets.PhidgetException import PhidgetErrorCodes, PhidgetException 11 | 12 | # System Imports 13 | import time 14 | import logging 15 | 16 | # Compatibility Imports 17 | from __future__ import print_function 18 | 19 | 20 | @implementer(IAddress) 21 | class PhidgetAddress (object): 22 | compareAttributes = ('device_class', 'id') 23 | 24 | def __init__ (self, id): 25 | self.id = id 26 | 27 | def __repr__ (self): 28 | return "{!s}({!s})".format(self.__class__.__name__, self.id) 29 | 30 | 31 | class PhidgetTransport (object): 32 | def __init__ (self, protocol): 33 | self.protocol = protocol 34 | 35 | def loseConnection (self): 36 | try: 37 | self.protocol.closePhidget() 38 | except (AttributeError, PhidgetException): 39 | pass 40 | 41 | 42 | class Phidget (object): 43 | def __init__ (self, id): 44 | self.id = id 45 | self.name = "phidget({!s})".format(id) 46 | 47 | def connect (self, protocolFactory): 48 | d = defer.Deferred() 49 | addr = PhidgetAddress(self.id) 50 | protocol = protocolFactory.buildProtocol(addr) 51 | protocol.transport = PhidgetTransport(protocol) 52 | 53 | @defer.inlineCallbacks 54 | def check_attached (): 55 | tries = 0 56 | 57 | while tries < 20: 58 | if protocol.isAttached(): 59 | serial = protocol.getSerialNum() 60 | name = protocol.getDeviceName() 61 | 62 | log.msg( 63 | "Phidget Device '{!s}', Serial Number: {!s} connected".format( 64 | name, serial 65 | ), logging.INFO) 66 | 67 | defer.returnValue(protocol) 68 | else: 69 | tries += 1 70 | yield task.deferLater(reactor, 0.5, lambda: True) 71 | 72 | raise Exception("Attachment to phidget timed out") 73 | 74 | try: 75 | protocol.openPhidget(self.id) 76 | 77 | # Blocks to allow time for phidget to initialise 78 | time.sleep(0.00125) 79 | 80 | check_attached().addCallbacks(d.callback, d.errback) 81 | except PhidgetException as e: 82 | d.errback(e) 83 | 84 | return d 85 | -------------------------------------------------------------------------------- /octopus/util.py: -------------------------------------------------------------------------------- 1 | from time import time as now 2 | from numpy import arange 3 | 4 | 5 | def timerange (start, interval, step): 6 | if start < 0: 7 | start = now() + start 8 | 9 | return arange(start, start + interval, step, float) 10 | 11 | 12 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "octopus", 3 | "version": "0.0.0", 4 | "description": "", 5 | "directories": { 6 | "test": "tests" 7 | }, 8 | "scripts": { 9 | "build": "./node_modules/.bin/rollup -c", 10 | "watch": "./node_modules/.bin/rollup -c --watch" 11 | }, 12 | "author": "Richard Ingham", 13 | "license": "MIT", 14 | "devDependencies": { 15 | "glob": "^7.1.2", 16 | "rollup": "^2.7.0", 17 | "rollup-plugin-copy": "^3.3.0", 18 | "@rollup/plugin-multi-entry": "^3.0.0", 19 | "rollup-plugin-node-builtins": "^2.1.0" 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | cryptography==3.2.1 2 | twisted 3 | pandas==1.2.2 4 | numpy 5 | scipy 6 | pyserial 7 | xlsxwriter 8 | pyasn1 9 | autobahn 10 | wget 11 | opencv-python 12 | bcrypt 13 | click 14 | -------------------------------------------------------------------------------- /rollup.config.js: -------------------------------------------------------------------------------- 1 | import builtins from 'rollup-plugin-node-builtins'; 2 | import multiEntry from '@rollup/plugin-multi-entry'; 3 | 4 | export default [{ 5 | // core input options 6 | input: { 7 | include: [ 8 | 'octopus/blocktopus/blockly/core/blockly.js', 9 | 'octopus/blocktopus/blockly/blocks/*.js', 10 | ], 11 | exclude: [ 12 | 'octopus/blocktopus/blockly/blocks/mixins.js', 13 | 'octopus/blocktopus/blockly/blocks/lists.js' 14 | ] 15 | }, 16 | output: { 17 | file: 'octopus/blocktopus/resources/blockly/pack/blockly.js', 18 | format: 'iife', 19 | name: 'Blockly', 20 | globals: { 21 | tinycolor: 'tinycolor' 22 | } 23 | }, 24 | plugins: [ 25 | builtins(), 26 | multiEntry() 27 | ] 28 | }, { 29 | input: { 30 | include: [ 31 | 'octopus/blocktopus/blockly/generators/python-octo.js', 32 | 'octopus/blocktopus/blockly/generators/python-octo/*.js', 33 | ], 34 | exclude: [ 35 | 'octopus/blocktopus/blockly/generators/python-octo/lists.js', 36 | ] 37 | }, 38 | plugins: [ 39 | multiEntry() 40 | ], 41 | output: { 42 | file: 'octopus/blocktopus/resources/blockly/pack/octopus-generator.js', 43 | format: 'iife', 44 | name: 'PythonOctoGenerator', 45 | globals: { 46 | Blockly: 'Blockly' 47 | } 48 | } 49 | }, { 50 | input: 'octopus/blocktopus/blockly/msg/messages.js', 51 | output: { 52 | file: 'octopus/blocktopus/resources/blockly/pack/blockly-messages.js', 53 | format: 'iife', 54 | name: 'Blockly.Msg', 55 | globals: { 56 | Blockly: 'Blockly' 57 | } 58 | } 59 | }]; 60 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | import setuptools 2 | 3 | with open("README.md", "r") as fh: 4 | long_description = fh.read() 5 | 6 | with open('requirements.txt') as f: 7 | requirements = f.read().splitlines() 8 | 9 | setuptools.setup( 10 | name="octopus", 11 | version="0.3", 12 | author="Richard Ingham", 13 | description="Real-time laboratory automation and monitoring in Python.", 14 | long_description=long_description, 15 | long_description_content_type="text/markdown", 16 | url="https://github.com/richardingham/octopus", 17 | packages=['octopus', 'octopus.manufacturer', 'octopus.blocks'], 18 | install_requires=requirements, 19 | include_package_data=True, 20 | classifiers=[ 21 | "Programming Language :: Python :: 3.9", 22 | "License :: OSI Approved :: MIT License", 23 | "Operating System :: OS Independent", 24 | ], 25 | entry_points={ 26 | 'console_scripts': [ 27 | 'blocktopus_server = octopus.blocktopus.server:run_server', 28 | ], 29 | }, 30 | ) 31 | -------------------------------------------------------------------------------- /tools/build.py: -------------------------------------------------------------------------------- 1 | import json 2 | import wget 3 | import os.path 4 | from distutils.dir_util import mkpath 5 | from urllib.error import HTTPError 6 | 7 | blocktopus_dir = os.path.abspath( 8 | os.path.join(os.path.dirname(__file__), '..', 'octopus', 'blocktopus') 9 | ) 10 | 11 | resources_dir = os.path.join(blocktopus_dir, 'resources', 'cache') 12 | resources_json = os.path.join(blocktopus_dir, 'templates', 'template-resources.json') 13 | 14 | 15 | def fetch_resource (url, filename, allow_fail = False): 16 | cache_file = os.path.join(resources_dir, filename) 17 | cache_file_dir = os.path.dirname(cache_file) 18 | mkpath(cache_file_dir) 19 | 20 | if os.path.isfile(cache_file): 21 | print(f"{filename} already downloaded") 22 | return 23 | 24 | print(f"Downloading {url}") 25 | 26 | try: 27 | downloaded_file = wget.download( 28 | url = url, 29 | out = cache_file 30 | ) 31 | print("\n") 32 | except HTTPError: 33 | if allow_fail: 34 | print(" [Not found]") 35 | else: 36 | raise 37 | 38 | 39 | if __name__ == "__main__": 40 | try: 41 | os.mkdir(resources_dir) 42 | except FileExistsError: 43 | pass 44 | 45 | print ("Downloading third-party resources") 46 | 47 | with open(resources_json) as templates_file: 48 | resources = {} 49 | 50 | for template_items in json.load(templates_file).values(): 51 | resources.update(template_items) 52 | 53 | extra_resources = {} 54 | for cache_filename, resource_url in resources.items(): 55 | split_filename = cache_filename.split('.') 56 | 57 | if len(split_filename) > 3 and split_filename[-2] == 'min' and split_filename[-1] in ('js', 'css'): 58 | base_filename = os.path.splitext(cache_filename)[0] 59 | base_url = os.path.splitext(resource_url)[0] 60 | 61 | ext = '.map' 62 | extra_resources[base_filename + ext] = base_url + ext 63 | 64 | elif split_filename[-1] == 'ttf': 65 | base_filename = os.path.splitext(cache_filename)[0] 66 | base_url = os.path.splitext(resource_url)[0] 67 | 68 | for ext in ('.eot', '.woff', '.woff2', '.svg'): 69 | extra_resources[base_filename + ext] = base_url + ext 70 | 71 | for cache_filename, resource_url in resources.items(): 72 | fetch_resource(resource_url, cache_filename) 73 | 74 | for cache_filename, resource_url in extra_resources.items(): 75 | fetch_resource(resource_url, cache_filename, allow_fail = True) 76 | 77 | -------------------------------------------------------------------------------- /tools/iCIR_server/irserver.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Installation: Requires node.js (for windows). 3 | * 4 | * www.nodejs.org 5 | * 6 | * Configuration: Set iC IR to Auto-Export Trend Data with a file 7 | * format of ${Date}_{$Time}_TrendData so that all of the files 8 | * end up in the same directory. 9 | * 10 | * Update directory variable below to reflect the folder. 11 | * 12 | * This script checks this directory and picks data from the 13 | * file with the greatest filename. It updates the internal data as 14 | * new files are added. 15 | * 16 | * You may need to open port 8124. 17 | * 18 | * Usage: node irserver.js 19 | * 20 | * Ctrl+C to quit. 21 | */ 22 | 23 | var net = require('net'); 24 | var fs = require('fs'); 25 | var path = require('path'); 26 | 27 | // Directory that will contain the data files 28 | // NB the trailing "\\" is required. 29 | var directory = "C:\\Users\\[my-user]\\IR-Export\\"; 30 | 31 | var server = net.createServer(function (socket) { 32 | 33 | console.log('client connected'); 34 | socket.on('close', function () { 35 | console.log('client disconnected'); 36 | }); 37 | 38 | var 39 | buffer = "", 40 | encoding = "utf8", 41 | delimeter = "\r\n" 42 | delimeterLength = delimeter.length; 43 | 44 | socket.on('data', function (data) { 45 | buffer += data; 46 | 47 | var pos = buffer.indexOf(delimeter); 48 | if (pos !== -1) { 49 | var line = buffer.substring(0, pos); 50 | buffer = buffer.substring(pos + delimeterLength); 51 | 52 | action(line, function (error, response) { 53 | if (error) { 54 | socket.write("error" + delimeter, encoding); 55 | } else { 56 | socket.write(response + delimeter, encoding); 57 | } 58 | }); 59 | } 60 | 61 | }); 62 | }); 63 | 64 | var lastData = { 65 | time: null, 66 | streams: [] 67 | }; 68 | 69 | function action (req, callback) { 70 | updateStreams(function (error) { 71 | if (error) callback(error); 72 | 73 | if (req === "requestData") { 74 | return callback(null, JSON.stringify(lastData)); 75 | } 76 | }); 77 | } 78 | 79 | function updateStreams (callback) { 80 | fs.readdir(directory, function (error, dirlist) { 81 | if (error) callback(error); 82 | 83 | var latestFile = ""; 84 | 85 | dirlist.forEach(function (fileName) { 86 | if (fileName.substr(-13) === "TrendData.txt") 87 | if (fileName > latestFile) 88 | latestFile = fileName; 89 | }); 90 | 91 | if (latestFile.length) { 92 | return readFile(directory + latestFile, callback); 93 | } else { 94 | return callback(); 95 | } 96 | }); 97 | } 98 | 99 | function readFile (fileName, callback) { 100 | fs.readFile(fileName, 'utf8', function (error, data) { 101 | if (error) callback(error); 102 | 103 | // Pull the time out of the filename. Expects the filename 104 | // to be in the format "YYYY-MM-DD_HH-MM-SS_TrendData.txt" 105 | // Uses the local timezone. 106 | var time = +(new (Function.bind.apply( 107 | Date, 108 | path.basename(fileName).split("_").slice(0, 2).map(function (s) { 109 | // Produce an array of two arrays of integers. 110 | return s.split("-").map(function (n) { 111 | return +n; 112 | }); 113 | }).reduce(function (p, c) { 114 | // Combine the two arrays 115 | return p.concat(c); 116 | }, [null]).map(function (n, i) { 117 | // Compensate for the fact that months are indexed 0-11 118 | return n - +(i === 2); 119 | }) 120 | ))); 121 | 122 | var lines = data.split("\n").slice(1).map(function (line) { 123 | return line.trim().split(",").map(function (v) { 124 | return v.slice(1,-1).trim(); 125 | }); 126 | }); 127 | 128 | var data = { 129 | time: time, 130 | streams: [] 131 | }; 132 | 133 | lines.forEach(function (line) { 134 | if (line[0] === "") return; 135 | 136 | data.streams.push({ 137 | name: line[0], 138 | value: parseFloat(line[1]) 139 | }); 140 | }); 141 | 142 | lastData = data; 143 | 144 | callback(); 145 | }); 146 | } 147 | 148 | var port = 8124; 149 | server.listen(port, function () { 150 | console.log('IR Data server listening on port ' + port); 151 | }); 152 | -------------------------------------------------------------------------------- /tools/knauer_mock/knauer_mock.js: -------------------------------------------------------------------------------- 1 | var net = require('net'); 2 | 3 | var server = net.createServer(); 4 | 5 | server.on('connection', handleConnection); 6 | 7 | server.listen(8050, function() { 8 | console.log('server listening to %j', server.address()); 9 | }); 10 | 11 | function handleConnection(conn) { 12 | var remoteAddress = conn.remoteAddress + ':' + conn.remotePort; 13 | var flowRate = 0; 14 | var power = false; 15 | 16 | console.log('new client connection from %s', remoteAddress); 17 | 18 | conn.setEncoding('utf8'); 19 | 20 | conn.on('data', onConnData); 21 | conn.once('close', onConnClose); 22 | conn.on('error', onConnError); 23 | 24 | var buffer = ''; 25 | 26 | function onConnData (data) { 27 | console.log(">" + data); 28 | var prev = 0, next; 29 | data = data.toString('utf8'); // assuming utf8 data... 30 | while ((next = data.indexOf('\r', prev)) > -1) { 31 | buffer += data.substring(prev, next); 32 | 33 | handleMessage(buffer); 34 | 35 | buffer = ''; 36 | prev = next + 1; 37 | } 38 | buffer += data.substring(prev); 39 | } 40 | 41 | function handleMessage(message) { 42 | if (message == "S?") { 43 | connWrite("S" + (power ? "0" : "\x10") + "\x00"); 44 | } else if (message == "F?") { 45 | connWrite("F" + flowRate.toString() + "\r"); 46 | } else if (message == "V?") { 47 | connWrite("V03.30\r"); 48 | } else if (message == "M0") { 49 | power = false; 50 | connWrite("OK\r") 51 | } else if (message == "M1") { 52 | power = true; 53 | connWrite("OK\r") 54 | } else if (message[0] == "F") { 55 | flowRate = parseInt(message.slice(1)); 56 | connWrite("OK\r") 57 | } 58 | } 59 | 60 | function connWrite(data) { 61 | console.log("<" + data); 62 | conn.write(data); 63 | } 64 | 65 | function onConnClose() { 66 | console.log('connection from %s closed', remoteAddress); 67 | } 68 | 69 | function onConnError(err) { 70 | console.log('Connection %s error: %s', remoteAddress, err.message); 71 | } 72 | } --------------------------------------------------------------------------------