flowchem-virtual 1.0.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- flowchem_virtual-1.0.0/LICENSE +21 -0
- flowchem_virtual-1.0.0/PKG-INFO +127 -0
- flowchem_virtual-1.0.0/README.md +89 -0
- flowchem_virtual-1.0.0/pyproject.toml +71 -0
- flowchem_virtual-1.0.0/setup.cfg +4 -0
- flowchem_virtual-1.0.0/src/flowchem_virtual/__init__.py +0 -0
- flowchem_virtual-1.0.0/src/flowchem_virtual/__main__.py +85 -0
- flowchem_virtual-1.0.0/src/flowchem_virtual/devices/__init__.py +18 -0
- flowchem_virtual-1.0.0/src/flowchem_virtual/devices/biochem/__init__.py +6 -0
- flowchem_virtual-1.0.0/src/flowchem_virtual/devices/biochem/virtual_solenoid.py +55 -0
- flowchem_virtual-1.0.0/src/flowchem_virtual/devices/bronkhorst/__init__.py +3 -0
- flowchem_virtual-1.0.0/src/flowchem_virtual/devices/bronkhorst/virtal_el_flow.py +71 -0
- flowchem_virtual-1.0.0/src/flowchem_virtual/devices/custom/__init__.py +5 -0
- flowchem_virtual-1.0.0/src/flowchem_virtual/devices/custom/virtual_mpikg_box.py +88 -0
- flowchem_virtual-1.0.0/src/flowchem_virtual/devices/custom/virtual_peltier_cooler.py +58 -0
- flowchem_virtual-1.0.0/src/flowchem_virtual/devices/dataapex/__init__.py +3 -0
- flowchem_virtual-1.0.0/src/flowchem_virtual/devices/dataapex/virtual_clarity.py +48 -0
- flowchem_virtual-1.0.0/src/flowchem_virtual/devices/hamilton/__init__.py +4 -0
- flowchem_virtual-1.0.0/src/flowchem_virtual/devices/hamilton/virtual_ml600.py +167 -0
- flowchem_virtual-1.0.0/src/flowchem_virtual/devices/harvardapparatus/__init__.py +4 -0
- flowchem_virtual-1.0.0/src/flowchem_virtual/devices/harvardapparatus/virtual_elite11.py +45 -0
- flowchem_virtual-1.0.0/src/flowchem_virtual/devices/huber/__init__.py +4 -0
- flowchem_virtual-1.0.0/src/flowchem_virtual/devices/huber/virtual_chiller.py +54 -0
- flowchem_virtual-1.0.0/src/flowchem_virtual/devices/knauer/__init__.py +15 -0
- flowchem_virtual-1.0.0/src/flowchem_virtual/devices/knauer/virtuals.py +243 -0
- flowchem_virtual-1.0.0/src/flowchem_virtual/devices/magritek/__init__.py +4 -0
- flowchem_virtual-1.0.0/src/flowchem_virtual/devices/magritek/virtual_spinsolve.py +54 -0
- flowchem_virtual-1.0.0/src/flowchem_virtual/devices/manson/__init__.py +4 -0
- flowchem_virtual-1.0.0/src/flowchem_virtual/devices/manson/virtual_manson_power.py +45 -0
- flowchem_virtual-1.0.0/src/flowchem_virtual/devices/mettlertoledo/__init__.py +4 -0
- flowchem_virtual-1.0.0/src/flowchem_virtual/devices/mettlertoledo/virtual_icir.py +32 -0
- flowchem_virtual-1.0.0/src/flowchem_virtual/devices/phidgets/__init__.py +12 -0
- flowchem_virtual-1.0.0/src/flowchem_virtual/devices/phidgets/virtuals.py +72 -0
- flowchem_virtual-1.0.0/src/flowchem_virtual/devices/runze/__init__.py +4 -0
- flowchem_virtual-1.0.0/src/flowchem_virtual/devices/runze/virtual_runze_valve.py +99 -0
- flowchem_virtual-1.0.0/src/flowchem_virtual/devices/vacuubrand/__init__.py +4 -0
- flowchem_virtual-1.0.0/src/flowchem_virtual/devices/vacuubrand/virtual_cvc3000.py +44 -0
- flowchem_virtual-1.0.0/src/flowchem_virtual/devices/vapourtec/__init__.py +4 -0
- flowchem_virtual-1.0.0/src/flowchem_virtual/devices/vapourtec/virtuals.py +175 -0
- flowchem_virtual-1.0.0/src/flowchem_virtual/devices/vicivalco/__init__.py +4 -0
- flowchem_virtual-1.0.0/src/flowchem_virtual/devices/vicivalco/virtual_vici.py +27 -0
- flowchem_virtual-1.0.0/src/flowchem_virtual/devices/waters/__init__.py +3 -0
- flowchem_virtual-1.0.0/src/flowchem_virtual/devices/waters/virtual.py +22 -0
- flowchem_virtual-1.0.0/src/flowchem_virtual/server/__init__.py +0 -0
- flowchem_virtual-1.0.0/src/flowchem_virtual/server/core.py +54 -0
- flowchem_virtual-1.0.0/src/flowchem_virtual.egg-info/PKG-INFO +127 -0
- flowchem_virtual-1.0.0/src/flowchem_virtual.egg-info/SOURCES.txt +50 -0
- flowchem_virtual-1.0.0/src/flowchem_virtual.egg-info/dependency_links.txt +1 -0
- flowchem_virtual-1.0.0/src/flowchem_virtual.egg-info/entry_points.txt +2 -0
- flowchem_virtual-1.0.0/src/flowchem_virtual.egg-info/requires.txt +27 -0
- flowchem_virtual-1.0.0/src/flowchem_virtual.egg-info/top_level.txt +1 -0
- flowchem_virtual-1.0.0/tests/test_virtualdevices.py +200 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 Automated Chemistry @ Max Plank Institute of Colloids and Interfaces
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, 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,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: flowchem_virtual
|
|
3
|
+
Version: 1.0.0
|
|
4
|
+
Summary: Flowchem-Virtual is a virtual version of the flowchem - library to control instruments and devices commonly found in chemistry labs via an interoperable web API.
|
|
5
|
+
Author-email: Jakob Wolf <75418671+JB-Wolf@users.noreply.github.com>, Samuel Saraiva <88909409+samuelvitorsaraiva@users.noreply.github.com>
|
|
6
|
+
Maintainer-email: Jakob Wolf <75418671+JB-Wolf@users.noreply.github.com>, Samuel Saraiva <88909409+samuelvitorsaraiva@users.noreply.github.com>
|
|
7
|
+
License-Expression: MIT
|
|
8
|
+
Project-URL: homepage, https://github.com/automatedchemistry/flowchem-virtual
|
|
9
|
+
Project-URL: repository, https://github.com/automatedchemistry/flowchem-virtual
|
|
10
|
+
Keywords: chemistry,automation,laboratory,science
|
|
11
|
+
Requires-Python: >=3.10
|
|
12
|
+
Description-Content-Type: text/markdown
|
|
13
|
+
License-File: LICENSE
|
|
14
|
+
Requires-Dist: flowchem
|
|
15
|
+
Provides-Extra: all
|
|
16
|
+
Requires-Dist: black; extra == "all"
|
|
17
|
+
Requires-Dist: mypy; extra == "all"
|
|
18
|
+
Requires-Dist: pre-commit; extra == "all"
|
|
19
|
+
Requires-Dist: ruff>=0.0.252; extra == "all"
|
|
20
|
+
Provides-Extra: ci
|
|
21
|
+
Requires-Dist: black; extra == "ci"
|
|
22
|
+
Requires-Dist: mypy; extra == "ci"
|
|
23
|
+
Requires-Dist: pre-commit; extra == "ci"
|
|
24
|
+
Requires-Dist: ruff>=0.0.252; extra == "ci"
|
|
25
|
+
Provides-Extra: dev
|
|
26
|
+
Requires-Dist: black; extra == "dev"
|
|
27
|
+
Requires-Dist: mypy; extra == "dev"
|
|
28
|
+
Requires-Dist: pre-commit; extra == "dev"
|
|
29
|
+
Requires-Dist: ruff>=0.0.252; extra == "dev"
|
|
30
|
+
Provides-Extra: test
|
|
31
|
+
Requires-Dist: httpx; extra == "test"
|
|
32
|
+
Requires-Dist: pytest; extra == "test"
|
|
33
|
+
Requires-Dist: pytest-asyncio; extra == "test"
|
|
34
|
+
Requires-Dist: pytest-cov; extra == "test"
|
|
35
|
+
Requires-Dist: pytest-mock; extra == "test"
|
|
36
|
+
Requires-Dist: pytest-xprocess; extra == "test"
|
|
37
|
+
Dynamic: license-file
|
|
38
|
+
|
|
39
|
+
# flowchem-virtual
|
|
40
|
+
|
|
41
|
+
`flowchem-virtual` provides **virtual devices (digital twins)** for the hardware implemented in the main `flowchem` [package](https://github.com/automatedchemistry/flowchem).
|
|
42
|
+
|
|
43
|
+
It’s designed as a **drop-in replacement** for `flowchem` when you want to:
|
|
44
|
+
|
|
45
|
+
* Develop and debug flows
|
|
46
|
+
|
|
47
|
+
* Test configuration files
|
|
48
|
+
|
|
49
|
+
* Run CI pipelines
|
|
50
|
+
|
|
51
|
+
…without having the physical hardware connected.
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
# Installation
|
|
55
|
+
|
|
56
|
+
```bash
|
|
57
|
+
pip install flowchem-virtual
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
# Usage
|
|
61
|
+
|
|
62
|
+
If you already know how `flowchem` works, using `flowchem-virtual` is straightforward.
|
|
63
|
+
|
|
64
|
+
Normally, you would start a flowchem server with:
|
|
65
|
+
|
|
66
|
+
```bash
|
|
67
|
+
flowchem /path/to/configuration_file.toml
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
To use the virtual devices instead, simply run:
|
|
71
|
+
|
|
72
|
+
```bash
|
|
73
|
+
flowchem-virtual /path/to/configuration_file.toml
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
`flowchem-virtual` will:
|
|
77
|
+
|
|
78
|
+
* Read the same configuration file
|
|
79
|
+
|
|
80
|
+
* Expose **virtual counterparts** of all devices defined there
|
|
81
|
+
|
|
82
|
+
* Keep the same API and behavior as much as possible, but without touching real hardware
|
|
83
|
+
|
|
84
|
+
This makes it a convenient **simulation layer** for your existing setups.
|
|
85
|
+
|
|
86
|
+
# When should I use flowchem-virtual?
|
|
87
|
+
|
|
88
|
+
Typical scenarios:
|
|
89
|
+
|
|
90
|
+
* 💻 **Local development** of client code without access to the lab
|
|
91
|
+
|
|
92
|
+
* 🧪 **Unit tests / CI** where hardware is not available (or not desirable)
|
|
93
|
+
|
|
94
|
+
* 📦 **Trying out configuration changes** before deploying them to a real setup
|
|
95
|
+
|
|
96
|
+
* 📚 **Teaching and demos** of flowchem-based automation
|
|
97
|
+
|
|
98
|
+
# Tips
|
|
99
|
+
|
|
100
|
+
If you are **not yet familiar with flowchem**, it’s highly recommended to learn the basics there first.
|
|
101
|
+
|
|
102
|
+
👉 [flowchem documentation](https://flowchem.readthedocs.io/en/latest/)
|
|
103
|
+
|
|
104
|
+
Once you understand how devices and configuration files work in `flowchem`, switching to the virtual version is just a matter of changing the command from:
|
|
105
|
+
```bash
|
|
106
|
+
flowchem ...
|
|
107
|
+
```
|
|
108
|
+
to:
|
|
109
|
+
```bash
|
|
110
|
+
flowchem-virtual ...
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
# Limitations
|
|
114
|
+
|
|
115
|
+
* Virtual devices **do not control real hardware** (obviously 😄).
|
|
116
|
+
|
|
117
|
+
* Most part of device-specific behavior (e.g. exact timing, error states) may be simplified.
|
|
118
|
+
|
|
119
|
+
* If your code depends on very low-level hardware quirks, the virtual version might not reproduce them perfectly.
|
|
120
|
+
|
|
121
|
+
# Contributing
|
|
122
|
+
|
|
123
|
+
Pull requests, bug reports and feature requests are welcome!
|
|
124
|
+
|
|
125
|
+
# License
|
|
126
|
+
|
|
127
|
+
This project is licensed under the MIT License – see the LICENSE file for details.
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
# flowchem-virtual
|
|
2
|
+
|
|
3
|
+
`flowchem-virtual` provides **virtual devices (digital twins)** for the hardware implemented in the main `flowchem` [package](https://github.com/automatedchemistry/flowchem).
|
|
4
|
+
|
|
5
|
+
It’s designed as a **drop-in replacement** for `flowchem` when you want to:
|
|
6
|
+
|
|
7
|
+
* Develop and debug flows
|
|
8
|
+
|
|
9
|
+
* Test configuration files
|
|
10
|
+
|
|
11
|
+
* Run CI pipelines
|
|
12
|
+
|
|
13
|
+
…without having the physical hardware connected.
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
# Installation
|
|
17
|
+
|
|
18
|
+
```bash
|
|
19
|
+
pip install flowchem-virtual
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
# Usage
|
|
23
|
+
|
|
24
|
+
If you already know how `flowchem` works, using `flowchem-virtual` is straightforward.
|
|
25
|
+
|
|
26
|
+
Normally, you would start a flowchem server with:
|
|
27
|
+
|
|
28
|
+
```bash
|
|
29
|
+
flowchem /path/to/configuration_file.toml
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
To use the virtual devices instead, simply run:
|
|
33
|
+
|
|
34
|
+
```bash
|
|
35
|
+
flowchem-virtual /path/to/configuration_file.toml
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
`flowchem-virtual` will:
|
|
39
|
+
|
|
40
|
+
* Read the same configuration file
|
|
41
|
+
|
|
42
|
+
* Expose **virtual counterparts** of all devices defined there
|
|
43
|
+
|
|
44
|
+
* Keep the same API and behavior as much as possible, but without touching real hardware
|
|
45
|
+
|
|
46
|
+
This makes it a convenient **simulation layer** for your existing setups.
|
|
47
|
+
|
|
48
|
+
# When should I use flowchem-virtual?
|
|
49
|
+
|
|
50
|
+
Typical scenarios:
|
|
51
|
+
|
|
52
|
+
* 💻 **Local development** of client code without access to the lab
|
|
53
|
+
|
|
54
|
+
* 🧪 **Unit tests / CI** where hardware is not available (or not desirable)
|
|
55
|
+
|
|
56
|
+
* 📦 **Trying out configuration changes** before deploying them to a real setup
|
|
57
|
+
|
|
58
|
+
* 📚 **Teaching and demos** of flowchem-based automation
|
|
59
|
+
|
|
60
|
+
# Tips
|
|
61
|
+
|
|
62
|
+
If you are **not yet familiar with flowchem**, it’s highly recommended to learn the basics there first.
|
|
63
|
+
|
|
64
|
+
👉 [flowchem documentation](https://flowchem.readthedocs.io/en/latest/)
|
|
65
|
+
|
|
66
|
+
Once you understand how devices and configuration files work in `flowchem`, switching to the virtual version is just a matter of changing the command from:
|
|
67
|
+
```bash
|
|
68
|
+
flowchem ...
|
|
69
|
+
```
|
|
70
|
+
to:
|
|
71
|
+
```bash
|
|
72
|
+
flowchem-virtual ...
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
# Limitations
|
|
76
|
+
|
|
77
|
+
* Virtual devices **do not control real hardware** (obviously 😄).
|
|
78
|
+
|
|
79
|
+
* Most part of device-specific behavior (e.g. exact timing, error states) may be simplified.
|
|
80
|
+
|
|
81
|
+
* If your code depends on very low-level hardware quirks, the virtual version might not reproduce them perfectly.
|
|
82
|
+
|
|
83
|
+
# Contributing
|
|
84
|
+
|
|
85
|
+
Pull requests, bug reports and feature requests are welcome!
|
|
86
|
+
|
|
87
|
+
# License
|
|
88
|
+
|
|
89
|
+
This project is licensed under the MIT License – see the LICENSE file for details.
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=64", "wheel"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "flowchem_virtual"
|
|
7
|
+
version = "1.0.0"
|
|
8
|
+
|
|
9
|
+
description = "Flowchem-Virtual is a virtual version of the flowchem - library to control instruments and devices commonly found in chemistry labs via an interoperable web API."
|
|
10
|
+
readme = "README.md"
|
|
11
|
+
requires-python = ">=3.10"
|
|
12
|
+
license = "MIT"
|
|
13
|
+
license-files = ['LICEN[CS]E*',]
|
|
14
|
+
keywords = ["chemistry", "automation", "laboratory", "science"]
|
|
15
|
+
authors = [
|
|
16
|
+
{ name = "Jakob Wolf", email = "75418671+JB-Wolf@users.noreply.github.com" },
|
|
17
|
+
{ name = "Samuel Saraiva", email = "88909409+samuelvitorsaraiva@users.noreply.github.com" },
|
|
18
|
+
]
|
|
19
|
+
maintainers = [
|
|
20
|
+
{ name = "Jakob Wolf", email = "75418671+JB-Wolf@users.noreply.github.com" },
|
|
21
|
+
{ name = "Samuel Saraiva", email = "88909409+samuelvitorsaraiva@users.noreply.github.com" }
|
|
22
|
+
]
|
|
23
|
+
dependencies = [
|
|
24
|
+
"flowchem",
|
|
25
|
+
]
|
|
26
|
+
[project.optional-dependencies]
|
|
27
|
+
all = [
|
|
28
|
+
"black",
|
|
29
|
+
"mypy",
|
|
30
|
+
"pre-commit",
|
|
31
|
+
"ruff>=0.0.252"
|
|
32
|
+
]
|
|
33
|
+
ci = [
|
|
34
|
+
"black",
|
|
35
|
+
"mypy",
|
|
36
|
+
"pre-commit",
|
|
37
|
+
"ruff>=0.0.252"
|
|
38
|
+
]
|
|
39
|
+
dev = [
|
|
40
|
+
"black",
|
|
41
|
+
"mypy",
|
|
42
|
+
"pre-commit",
|
|
43
|
+
"ruff>=0.0.252"
|
|
44
|
+
]
|
|
45
|
+
test = [
|
|
46
|
+
"httpx",
|
|
47
|
+
"pytest",
|
|
48
|
+
"pytest-asyncio",
|
|
49
|
+
"pytest-cov",
|
|
50
|
+
"pytest-mock",
|
|
51
|
+
"pytest-xprocess",
|
|
52
|
+
]
|
|
53
|
+
[project.urls]
|
|
54
|
+
homepage = "https://github.com/automatedchemistry/flowchem-virtual"
|
|
55
|
+
repository = "https://github.com/automatedchemistry/flowchem-virtual"
|
|
56
|
+
|
|
57
|
+
[project.scripts]
|
|
58
|
+
flowchem-virtual = "flowchem_virtual.__main__:main"
|
|
59
|
+
|
|
60
|
+
[tool.setuptools]
|
|
61
|
+
package-dir = { "" = "src" }
|
|
62
|
+
|
|
63
|
+
[tool.setuptools.packages.find]
|
|
64
|
+
where = ["src"]
|
|
65
|
+
|
|
66
|
+
[tool.mypy]
|
|
67
|
+
ignore_missing_imports = true
|
|
68
|
+
python_version = "3.11"
|
|
69
|
+
|
|
70
|
+
[tool.ruff]
|
|
71
|
+
line-length = 120
|
|
File without changes
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
"""Entry-point module for the command line prefixes, called in case you use `python -m flowchem`.
|
|
2
|
+
Why does this file exist, and why `__main__`? For more info, read:
|
|
3
|
+
- https://www.python.org/dev/peps/pep-0338/
|
|
4
|
+
- https://docs.python.org/3/using/cmdline.html#cmdoption-m.
|
|
5
|
+
"""
|
|
6
|
+
import asyncio
|
|
7
|
+
import sys
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
|
|
10
|
+
import rich_click as click
|
|
11
|
+
import uvicorn
|
|
12
|
+
from loguru import logger
|
|
13
|
+
|
|
14
|
+
from flowchem_virtual.server.core import FlowchemVirtual
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
@click.argument("device_config_file", type=click.Path(), required=True)
|
|
18
|
+
@click.option(
|
|
19
|
+
"-l",
|
|
20
|
+
"--log",
|
|
21
|
+
"logfile",
|
|
22
|
+
type=click.Path(),
|
|
23
|
+
default=None,
|
|
24
|
+
help="Save logs to file.",
|
|
25
|
+
)
|
|
26
|
+
@click.option(
|
|
27
|
+
"-h",
|
|
28
|
+
"--host",
|
|
29
|
+
"host",
|
|
30
|
+
type=str,
|
|
31
|
+
default="0.0.0.0",
|
|
32
|
+
help="Server host. 0.0.0.0 is used to bind to all addresses, do not use for internet-exposed devices__!",
|
|
33
|
+
)
|
|
34
|
+
@click.option("-d", "--debug", is_flag=True, help="Print debug info.")
|
|
35
|
+
@click.version_option()
|
|
36
|
+
@click.command()
|
|
37
|
+
def main(device_config_file, logfile, host, debug):
|
|
38
|
+
"""Flowchem main program.
|
|
39
|
+
|
|
40
|
+
Parse device_config_file and starts a server exposing the devices__ via REST-ful API.
|
|
41
|
+
|
|
42
|
+
In order to operate a simulated device for educational purposes without requiring
|
|
43
|
+
any connected device, simply execute 'flowchem example'
|
|
44
|
+
|
|
45
|
+
Args:
|
|
46
|
+
----
|
|
47
|
+
device_config_file: Flowchem configuration file specifying device connection settings (TOML)
|
|
48
|
+
logfile: Output file for logs.
|
|
49
|
+
host: IP on which the server will be listening. Loopback IP as default, use LAN IP to enable remote access.
|
|
50
|
+
debug: Print debug info
|
|
51
|
+
"""
|
|
52
|
+
if sys.platform == "win32":
|
|
53
|
+
asyncio.set_event_loop_policy(asyncio.WindowsSelectorEventLoopPolicy())
|
|
54
|
+
|
|
55
|
+
if not debug:
|
|
56
|
+
# Set stderr to info
|
|
57
|
+
logger.remove()
|
|
58
|
+
logger.add(sys.stderr, level="INFO")
|
|
59
|
+
|
|
60
|
+
logger.info("Starting flowchem-virtual!")
|
|
61
|
+
if logfile:
|
|
62
|
+
logger.add(Path(logfile), level="DEBUG")
|
|
63
|
+
logger.debug(f"Starting server with configuration file: '{device_config_file}'")
|
|
64
|
+
|
|
65
|
+
async def main_loop():
|
|
66
|
+
"""Main application loop, the event loop is shared between uvicorn and flowchem."""
|
|
67
|
+
flowchem = FlowchemVirtual()
|
|
68
|
+
await flowchem.setup(Path(device_config_file))
|
|
69
|
+
|
|
70
|
+
config = uvicorn.Config(
|
|
71
|
+
flowchem.http.app,
|
|
72
|
+
host=host,
|
|
73
|
+
port=flowchem.port,
|
|
74
|
+
log_level="info",
|
|
75
|
+
timeout_keep_alive=3600,
|
|
76
|
+
)
|
|
77
|
+
server = uvicorn.Server(config)
|
|
78
|
+
logger.info("Click on http://127.0.0.1:8000 to access device server.")
|
|
79
|
+
await server.serve()
|
|
80
|
+
|
|
81
|
+
asyncio.run(main_loop())
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
if __name__ == "__main__":
|
|
85
|
+
main()
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
# ruff: noqa
|
|
2
|
+
from .biochem import *
|
|
3
|
+
from .bronkhorst import *
|
|
4
|
+
from .dataapex import *
|
|
5
|
+
from .hamilton import *
|
|
6
|
+
from .harvardapparatus import *
|
|
7
|
+
from .huber import *
|
|
8
|
+
from .knauer import *
|
|
9
|
+
from .magritek import *
|
|
10
|
+
from .manson import *
|
|
11
|
+
from .mettlertoledo import *
|
|
12
|
+
from .phidgets import *
|
|
13
|
+
from .vacuubrand import *
|
|
14
|
+
from .vapourtec import *
|
|
15
|
+
from .vicivalco import *
|
|
16
|
+
from .runze import *
|
|
17
|
+
from .custom import *
|
|
18
|
+
from .waters import *
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
from flowchem.devices.biochem.solenoid_valve import (
|
|
2
|
+
BioChemSolenoidValve,
|
|
3
|
+
SolenoidValve,
|
|
4
|
+
SolenoidValve2Way,
|
|
5
|
+
)
|
|
6
|
+
from flowchem.components.technical.relay import Relay
|
|
7
|
+
|
|
8
|
+
from loguru import logger
|
|
9
|
+
import asyncio
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class VirtualBioChemSolenoidValve(BioChemSolenoidValve):
|
|
13
|
+
async def initialize(self):
|
|
14
|
+
n = 0
|
|
15
|
+
while self.support_platform not in Relay.INSTANCES:
|
|
16
|
+
await asyncio.sleep(0.5)
|
|
17
|
+
n += 1
|
|
18
|
+
if n > 6:
|
|
19
|
+
raise Exception(
|
|
20
|
+
f"The relay support_platform '{self.support_platform}' was not declared or initialized. "
|
|
21
|
+
"The valve cannot be initialized without a support_platform "
|
|
22
|
+
f"(Please add '{self.support_platform}' to the configuration file!)."
|
|
23
|
+
)
|
|
24
|
+
|
|
25
|
+
self._io = Relay.INSTANCES[self.support_platform]
|
|
26
|
+
# Register the standard SolenoidValve component/API on this device
|
|
27
|
+
self.components.append(SolenoidValve("valve", self))
|
|
28
|
+
conf_valve = "normally open" if self.normally_open else "normally closed"
|
|
29
|
+
logger.info(
|
|
30
|
+
f"Connected to Virtual BioChemSolenoidValve {conf_valve} on "
|
|
31
|
+
f"'{self.support_platform}' channel {self.channel}!"
|
|
32
|
+
)
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
class VirtualBioChemSolenoid2WayValve(VirtualBioChemSolenoidValve):
|
|
36
|
+
async def initialize(self):
|
|
37
|
+
n = 0
|
|
38
|
+
while self.support_platform not in Relay.INSTANCES:
|
|
39
|
+
await asyncio.sleep(0.5)
|
|
40
|
+
n += 1
|
|
41
|
+
if n > 6:
|
|
42
|
+
raise Exception(
|
|
43
|
+
f"The relay support_platform '{self.support_platform}' was not declared or initialized. "
|
|
44
|
+
"The valve cannot be initialized without a support_platform "
|
|
45
|
+
f"(Please add '{self.support_platform}' to the configuration file!)."
|
|
46
|
+
)
|
|
47
|
+
|
|
48
|
+
self._io = Relay.INSTANCES[self.support_platform]
|
|
49
|
+
# Register the standard SolenoidValve component/API on this device
|
|
50
|
+
self.components.append(SolenoidValve2Way("valve", self))
|
|
51
|
+
conf_valve = "normally open" if self.normally_open else "normally closed"
|
|
52
|
+
logger.info(
|
|
53
|
+
f"Connected to Virtual BioChemSolenoidValve {conf_valve} on "
|
|
54
|
+
f"'{self.support_platform}' channel {self.channel}!"
|
|
55
|
+
)
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
from flowchem.devices.bronkhorst.el_flow_component import EPCComponent, MFCComponent
|
|
2
|
+
from flowchem.devices.flowchem_device import FlowchemDevice
|
|
3
|
+
from flowchem.utils.people import samuel_saraiva
|
|
4
|
+
from flowchem import ureg
|
|
5
|
+
from loguru import logger
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def isfloat(num):
|
|
9
|
+
try:
|
|
10
|
+
float(num)
|
|
11
|
+
return True
|
|
12
|
+
except ValueError:
|
|
13
|
+
return False
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class VirtualEPC(FlowchemDevice):
|
|
17
|
+
def __init__(self, name="", *args, max_pressure: float = 10, **kwargs) -> None:
|
|
18
|
+
super().__init__(name)
|
|
19
|
+
self.device_info.authors = [samuel_saraiva]
|
|
20
|
+
self.device_info.manufacturer = "VirtualBronkhorst"
|
|
21
|
+
self.device_info.model = "EPC"
|
|
22
|
+
logger.info("Connected virtual EPC")
|
|
23
|
+
|
|
24
|
+
self.pressure = "0 bar"
|
|
25
|
+
self.max_pressure = max_pressure
|
|
26
|
+
|
|
27
|
+
async def initialize(self):
|
|
28
|
+
await self.set_pressure("0 bar")
|
|
29
|
+
self.components.append(EPCComponent("EPC", self))
|
|
30
|
+
|
|
31
|
+
async def set_pressure(self, pressure: str):
|
|
32
|
+
if pressure.isnumeric() or isfloat(pressure):
|
|
33
|
+
pressure = pressure + "bar"
|
|
34
|
+
logger.warning("No units provided to set_pressure, assuming bar.")
|
|
35
|
+
self.pressure = pressure
|
|
36
|
+
|
|
37
|
+
async def get_pressure(self) -> float:
|
|
38
|
+
return ureg.Quantity(self.pressure).magnitude
|
|
39
|
+
|
|
40
|
+
async def get_pressure_percentage(self) -> float:
|
|
41
|
+
return 100 * ureg.Quantity(self.pressure).magnitude / self.max_pressure
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
class VirtualMFC(FlowchemDevice):
|
|
45
|
+
def __init__(self, name="", *args, max_flow: float = 9, **kwargs) -> None:
|
|
46
|
+
super().__init__(name)
|
|
47
|
+
self.device_info.authors = [samuel_saraiva]
|
|
48
|
+
self.device_info.manufacturer = "VirtualBronkhorst"
|
|
49
|
+
self.device_info.model = "EL-FLOW"
|
|
50
|
+
logger.debug("Connected virtual MFC")
|
|
51
|
+
|
|
52
|
+
self.max_flow = max_flow
|
|
53
|
+
self.flow = "0 ml/min"
|
|
54
|
+
|
|
55
|
+
async def initialize(self):
|
|
56
|
+
await self.set_flow_setpoint("0 ml/min")
|
|
57
|
+
self.components.append(MFCComponent("MFC", self))
|
|
58
|
+
|
|
59
|
+
async def set_flow_setpoint(self, flowrate: str):
|
|
60
|
+
if flowrate.isnumeric() or isfloat(flowrate):
|
|
61
|
+
flowrate = flowrate + "ml/min"
|
|
62
|
+
logger.warning(
|
|
63
|
+
"No units provided to set_flow_rate, assuming milliliter/minutes.",
|
|
64
|
+
)
|
|
65
|
+
self.flow = flowrate
|
|
66
|
+
|
|
67
|
+
async def get_flow_setpoint(self) -> float:
|
|
68
|
+
return ureg.Quantity(self.flow).magnitude
|
|
69
|
+
|
|
70
|
+
async def get_flow_percentage(self) -> float:
|
|
71
|
+
return 100 * ureg.Quantity(self.flow).magnitude / self.max_flow
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
from flowchem.devices.custom.mpikg_switch_box import SwitchBoxMPIKG
|
|
2
|
+
from flowchem.devices.custom.mpikg_switch_box_component import (
|
|
3
|
+
SwitchBoxADC,
|
|
4
|
+
SwitchBoxRelay,
|
|
5
|
+
SwitchBoxDAC,
|
|
6
|
+
)
|
|
7
|
+
from pint.registry import Quantity
|
|
8
|
+
from loguru import logger
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class VirtualSwitchBoxMPIKG(SwitchBoxMPIKG):
|
|
12
|
+
"""Switch Box MPIKG module class"""
|
|
13
|
+
|
|
14
|
+
def __init__(self, box_io, name: str = "") -> None:
|
|
15
|
+
super().__init__(box_io, name)
|
|
16
|
+
|
|
17
|
+
self.rele = [0] * 32
|
|
18
|
+
self.ADC = [0]
|
|
19
|
+
self.DAC = [0]
|
|
20
|
+
|
|
21
|
+
@classmethod
|
|
22
|
+
def from_config(
|
|
23
|
+
cls,
|
|
24
|
+
port: str,
|
|
25
|
+
name: str = "",
|
|
26
|
+
**serial_kwargs,
|
|
27
|
+
):
|
|
28
|
+
return cls(box_io="switch_io", name=name)
|
|
29
|
+
|
|
30
|
+
async def initialize(self):
|
|
31
|
+
self.device_info.version = "Virtual"
|
|
32
|
+
self.components.extend(
|
|
33
|
+
[
|
|
34
|
+
SwitchBoxADC("adc", self),
|
|
35
|
+
SwitchBoxDAC("dac", self),
|
|
36
|
+
SwitchBoxRelay("relay-A", self, "a"), # Channel 1 to 8
|
|
37
|
+
SwitchBoxRelay("relay-B", self, "b"), # Channel 9 to 16
|
|
38
|
+
SwitchBoxRelay("relay-C", self, "c"), # Channel 17 to 24
|
|
39
|
+
SwitchBoxRelay("relay-D", self, "d"), # Channel 25 to 32
|
|
40
|
+
]
|
|
41
|
+
)
|
|
42
|
+
|
|
43
|
+
logger.info("Connected to Virtual SwitchBoxMPIKG!")
|
|
44
|
+
|
|
45
|
+
async def set_relay_port(self, values: list[int], port: str = "a"):
|
|
46
|
+
map_ch = {"a": 0, "b": 7, "c": 15, "d": 23}
|
|
47
|
+
n = len(values)
|
|
48
|
+
if n >= 8:
|
|
49
|
+
self.rele[map_ch[port] : map_ch[port] + 8] = values[:8]
|
|
50
|
+
else:
|
|
51
|
+
self.rele[map_ch[port] : map_ch[port] + 8] = values + [0] * (
|
|
52
|
+
8 - len(values)
|
|
53
|
+
)
|
|
54
|
+
return True
|
|
55
|
+
|
|
56
|
+
async def set_relay_single_channel(
|
|
57
|
+
self,
|
|
58
|
+
channel: int,
|
|
59
|
+
value: int = 2,
|
|
60
|
+
keep_port_status=True,
|
|
61
|
+
port_identify: str = "a",
|
|
62
|
+
):
|
|
63
|
+
if channel > 8:
|
|
64
|
+
self.rele[channel] = value
|
|
65
|
+
else:
|
|
66
|
+
map_ch = {"a": 0, "b": 7, "c": 15, "d": 23}
|
|
67
|
+
self.rele[map_ch[port_identify] + channel] = value
|
|
68
|
+
return True
|
|
69
|
+
|
|
70
|
+
async def get_relay_channels(self):
|
|
71
|
+
return {
|
|
72
|
+
"a": self.rele[:8],
|
|
73
|
+
"b": self.rele[8:16],
|
|
74
|
+
"c": self.rele[16:24],
|
|
75
|
+
"d": self.rele[24:],
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
""" ADC/DAC Commands """
|
|
79
|
+
|
|
80
|
+
async def get_adc(self):
|
|
81
|
+
return {f"ADC{i}": v for i, v in enumerate(self.ADC)}
|
|
82
|
+
|
|
83
|
+
async def get_dac(self, channel: int = 1, volts: bool = True):
|
|
84
|
+
return self.DAC[channel - 1]
|
|
85
|
+
|
|
86
|
+
async def set_dac(self, value: Quantity, channel: int = 1) -> bool:
|
|
87
|
+
self.DAC[channel - 1] = value.to("V").magnitude
|
|
88
|
+
return True
|