flowchem-qt 0.0.1__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_qt-0.0.1/LICENSE +21 -0
- flowchem_qt-0.0.1/PKG-INFO +14 -0
- flowchem_qt-0.0.1/README.md +124 -0
- flowchem_qt-0.0.1/app/__init__.py +0 -0
- flowchem_qt-0.0.1/app/main_window.py +83 -0
- flowchem_qt-0.0.1/app/server_manager.py +66 -0
- flowchem_qt-0.0.1/app/tabs/__init__.py +0 -0
- flowchem_qt-0.0.1/app/tabs/config_tab.py +78 -0
- flowchem_qt-0.0.1/app/tabs/discover_tab.py +78 -0
- flowchem_qt-0.0.1/app/tabs/logs_tab.py +46 -0
- flowchem_qt-0.0.1/app/tabs/server_tab.py +102 -0
- flowchem_qt-0.0.1/app/tray.py +51 -0
- flowchem_qt-0.0.1/flowchem_qt.egg-info/PKG-INFO +14 -0
- flowchem_qt-0.0.1/flowchem_qt.egg-info/SOURCES.txt +19 -0
- flowchem_qt-0.0.1/flowchem_qt.egg-info/dependency_links.txt +1 -0
- flowchem_qt-0.0.1/flowchem_qt.egg-info/entry_points.txt +2 -0
- flowchem_qt-0.0.1/flowchem_qt.egg-info/requires.txt +9 -0
- flowchem_qt-0.0.1/flowchem_qt.egg-info/top_level.txt +2 -0
- flowchem_qt-0.0.1/main.py +47 -0
- flowchem_qt-0.0.1/pyproject.toml +26 -0
- flowchem_qt-0.0.1/setup.cfg +4 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 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,14 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: flowchem-qt
|
|
3
|
+
Version: 0.0.1
|
|
4
|
+
Requires-Python: >=3.11
|
|
5
|
+
License-File: LICENSE
|
|
6
|
+
Requires-Dist: flowchem
|
|
7
|
+
Requires-Dist: PySide6>=6.5
|
|
8
|
+
Requires-Dist: PySide6-Fluent-Widgets>=1.11.2
|
|
9
|
+
Requires-Dist: tomli-w>=1.0
|
|
10
|
+
Requires-Dist: loguru>=0.7
|
|
11
|
+
Requires-Dist: zeroconf>=0.149.7
|
|
12
|
+
Provides-Extra: test
|
|
13
|
+
Requires-Dist: pytest; extra == "test"
|
|
14
|
+
Dynamic: license-file
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
<p align="center">
|
|
2
|
+
<img src="resources/icons/flowchem_logo.svg" alt="FlowChem logo" width="400"/>
|
|
3
|
+
</p>
|
|
4
|
+
|
|
5
|
+
<h1 align="center">FlowChem GUI</h1>
|
|
6
|
+
|
|
7
|
+
<p align="center">
|
|
8
|
+
A desktop application to manage the <a href="https://github.com/automatedchemistry/flowchem">FlowChem</a> server — configure devices, control the server process, and auto-discover hardware, all from one window.
|
|
9
|
+
</p>
|
|
10
|
+
|
|
11
|
+
---
|
|
12
|
+
|
|
13
|
+
## Requirements
|
|
14
|
+
|
|
15
|
+
- Python ≥ 3.11
|
|
16
|
+
- FlowChem installed and available on `PATH` (activate your FlowChem virtualenv before launching)
|
|
17
|
+
|
|
18
|
+
## Installation
|
|
19
|
+
|
|
20
|
+
```bash
|
|
21
|
+
pip install flowchem-qt
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
Or, to install from source:
|
|
25
|
+
|
|
26
|
+
```bash
|
|
27
|
+
pip install -e .
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
## Usage
|
|
31
|
+
|
|
32
|
+
```bash
|
|
33
|
+
flowchem-qt # launches silently (no console window on Windows)
|
|
34
|
+
python main.py # development / debug
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
---
|
|
38
|
+
|
|
39
|
+
## Window Overview
|
|
40
|
+
|
|
41
|
+
The main window has four tabs and a persistent status bar at the bottom showing a coloured dot:
|
|
42
|
+
|
|
43
|
+
- **Red dot** — server is stopped
|
|
44
|
+
- **Green dot** — server is running
|
|
45
|
+
|
|
46
|
+
### Config editor
|
|
47
|
+
|
|
48
|
+
Edit the FlowChem `config.toml` directly in the app. Browse for a file, load it into the editor, make changes, and save — no external editor needed. This tab is disabled while the server is running.
|
|
49
|
+
|
|
50
|
+
<p align="center">
|
|
51
|
+
<img src="resources/editor.png" alt="Config editor tab" width="480"/>
|
|
52
|
+
</p>
|
|
53
|
+
|
|
54
|
+
| Element | Description |
|
|
55
|
+
|---|---|
|
|
56
|
+
| Path field | Full path to the config file. Remembered between sessions. |
|
|
57
|
+
| Browse | Opens a file dialog to pick a `.toml` file. |
|
|
58
|
+
| Load | Reads the selected file into the editor. |
|
|
59
|
+
| Save | Writes the editor content back to disk. |
|
|
60
|
+
| Editor area | Plain-text editor for the raw TOML content. The FlowChem server validates on startup. |
|
|
61
|
+
|
|
62
|
+
---
|
|
63
|
+
|
|
64
|
+
### Server
|
|
65
|
+
|
|
66
|
+
Start and stop the FlowChem server and open the interactive API browser.
|
|
67
|
+
|
|
68
|
+
<p align="center">
|
|
69
|
+
<img src="resources/server.png" alt="Server tab" width="480"/>
|
|
70
|
+
</p>
|
|
71
|
+
|
|
72
|
+
| Element | Description |
|
|
73
|
+
|---|---|
|
|
74
|
+
| Server address | Base URL of the running server (default `http://localhost:8000`). |
|
|
75
|
+
| Debug mode | Passes `--debug` to FlowChem for verbose log output. |
|
|
76
|
+
| Simulation mode | Launches `flowchem-sim` instead of `flowchem`. All device drivers are replaced with simulated counterparts — no physical hardware required. |
|
|
77
|
+
| Start / Stop | Single toggle button. Starts or stops the server process. |
|
|
78
|
+
| Open API browser | Opens `<server address>/docs` in the system browser. |
|
|
79
|
+
|
|
80
|
+
> The address field, debug checkbox, and simulation mode checkbox are disabled while the server is running.
|
|
81
|
+
|
|
82
|
+
---
|
|
83
|
+
|
|
84
|
+
### Discover
|
|
85
|
+
|
|
86
|
+
Run `flowchem-autodiscover` to detect connected devices and generate a starter config.
|
|
87
|
+
|
|
88
|
+
<p align="center">
|
|
89
|
+
<img src="resources/discover.png" alt="Discover tab" width="480"/>
|
|
90
|
+
</p>
|
|
91
|
+
|
|
92
|
+
| Element | Description |
|
|
93
|
+
|---|---|
|
|
94
|
+
| Run autodiscover | Shows a safety warning, then runs `flowchem-autodiscover`. Output streams in real time. |
|
|
95
|
+
| Output area | Read-only display of autodiscover stdout and stderr. |
|
|
96
|
+
| Copy to editor | Copies the output into the Config editor for review and saving. |
|
|
97
|
+
|
|
98
|
+
> **Warning:** autodiscovery communicates over serial ports and may place unsupported devices in an unsafe state. A confirmation dialog is shown before running.
|
|
99
|
+
>
|
|
100
|
+
> This tab is disabled while the server is running.
|
|
101
|
+
|
|
102
|
+
---
|
|
103
|
+
|
|
104
|
+
### Logs
|
|
105
|
+
|
|
106
|
+
Live log viewer — always accessible, even while the server is running.
|
|
107
|
+
|
|
108
|
+
| Element | Description |
|
|
109
|
+
|---|---|
|
|
110
|
+
| Log area | Streams output from the FlowChem server process and internal GUI events. Auto-scrolls to the latest entry. |
|
|
111
|
+
| Clear | Clears the log area. |
|
|
112
|
+
|
|
113
|
+
---
|
|
114
|
+
|
|
115
|
+
## System tray
|
|
116
|
+
|
|
117
|
+
Closing the main window minimises to the system tray — the application keeps running in the background. Right-click the tray icon to access:
|
|
118
|
+
|
|
119
|
+
| Menu item | Description |
|
|
120
|
+
|---|---|
|
|
121
|
+
| Show window | Brings the main window back to the foreground. |
|
|
122
|
+
| Start server | Starts the FlowChem server (uses the config path set in the Config editor). |
|
|
123
|
+
| Stop server | Stops the running server. |
|
|
124
|
+
| Quit | Fully exits the application. |
|
|
File without changes
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
from pathlib import Path
|
|
2
|
+
|
|
3
|
+
from PySide6.QtGui import QIcon
|
|
4
|
+
from PySide6.QtWidgets import QLabel, QMainWindow, QMessageBox, QStatusBar, QTabWidget
|
|
5
|
+
|
|
6
|
+
from app.server_manager import ServerManager
|
|
7
|
+
from app.tabs.config_tab import ConfigTab
|
|
8
|
+
from app.tabs.discover_tab import DiscoverTab
|
|
9
|
+
from app.tabs.logs_tab import LogsTab
|
|
10
|
+
from app.tabs.server_tab import ServerTab
|
|
11
|
+
|
|
12
|
+
_ICON_PATH = (
|
|
13
|
+
Path(__file__).parent.parent / "resources" / "icons" / "flowchem_app_icon.ico"
|
|
14
|
+
)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class MainWindow(QMainWindow):
|
|
18
|
+
def __init__(self, parent=None):
|
|
19
|
+
super().__init__(parent)
|
|
20
|
+
self.setWindowTitle("FlowChem Manager")
|
|
21
|
+
self.setWindowIcon(QIcon(str(_ICON_PATH)))
|
|
22
|
+
self.resize(900, 650)
|
|
23
|
+
|
|
24
|
+
self.server_manager = ServerManager(self)
|
|
25
|
+
|
|
26
|
+
self.config_tab = ConfigTab()
|
|
27
|
+
self.server_tab = ServerTab(self.server_manager, self.config_tab)
|
|
28
|
+
self.discover_tab = DiscoverTab(self.config_tab)
|
|
29
|
+
self.logs_tab = LogsTab()
|
|
30
|
+
|
|
31
|
+
tabs = QTabWidget()
|
|
32
|
+
tabs.addTab(self.config_tab, "Config editor")
|
|
33
|
+
tabs.addTab(self.server_tab, "Server")
|
|
34
|
+
tabs.addTab(self.discover_tab, "Discover")
|
|
35
|
+
tabs.addTab(self.logs_tab, "Logs")
|
|
36
|
+
tabs.setTabToolTip(0, "Edit and save the FlowChem TOML configuration.")
|
|
37
|
+
tabs.setTabToolTip(1, "Start, stop, and open the FlowChem server API.")
|
|
38
|
+
tabs.setTabToolTip(2, "Detect connected devices and copy the generated config.")
|
|
39
|
+
tabs.setTabToolTip(3, "View FlowChem server output and GUI events.")
|
|
40
|
+
self.setCentralWidget(tabs)
|
|
41
|
+
|
|
42
|
+
self._status_dot = QLabel("● Server stopped")
|
|
43
|
+
self._status_dot.setToolTip("Current FlowChem server status.")
|
|
44
|
+
self._status_dot.setStyleSheet("color: red;")
|
|
45
|
+
status_bar = QStatusBar()
|
|
46
|
+
status_bar.setToolTip("Shows the latest FlowChem Manager status message.")
|
|
47
|
+
status_bar.addWidget(self._status_dot)
|
|
48
|
+
self.setStatusBar(status_bar)
|
|
49
|
+
|
|
50
|
+
self.server_manager.started.connect(self._on_started)
|
|
51
|
+
self.server_manager.stopped.connect(self._on_stopped)
|
|
52
|
+
self.server_manager.error.connect(self._on_error)
|
|
53
|
+
self.server_manager.stdout_ready.connect(self.logs_tab.append_process_output)
|
|
54
|
+
self.server_manager.stderr_ready.connect(self.logs_tab.append_process_output)
|
|
55
|
+
|
|
56
|
+
def closeEvent(self, event):
|
|
57
|
+
event.ignore()
|
|
58
|
+
self.hide()
|
|
59
|
+
|
|
60
|
+
def _on_started(self):
|
|
61
|
+
self._status_dot.setText("● Server running")
|
|
62
|
+
self._status_dot.setToolTip("FlowChem server is running.")
|
|
63
|
+
self._status_dot.setStyleSheet("color: green;")
|
|
64
|
+
self.statusBar().showMessage("Server started", 3000)
|
|
65
|
+
self.config_tab.setEnabled(False)
|
|
66
|
+
self.discover_tab.setEnabled(False)
|
|
67
|
+
|
|
68
|
+
def _on_stopped(self, exit_code):
|
|
69
|
+
self._status_dot.setText("● Server stopped")
|
|
70
|
+
self._status_dot.setToolTip("FlowChem server is stopped.")
|
|
71
|
+
self._status_dot.setStyleSheet("color: red;")
|
|
72
|
+
msg = (
|
|
73
|
+
"Server stopped"
|
|
74
|
+
if exit_code == 0
|
|
75
|
+
else f"Server stopped (exit code {exit_code})"
|
|
76
|
+
)
|
|
77
|
+
self.statusBar().showMessage(msg, 5000)
|
|
78
|
+
self.config_tab.setEnabled(True)
|
|
79
|
+
self.discover_tab.setEnabled(True)
|
|
80
|
+
|
|
81
|
+
def _on_error(self, msg: str):
|
|
82
|
+
self.statusBar().showMessage(f"Error: {msg}", 0) # 0 = stays until next message
|
|
83
|
+
QMessageBox.critical(self, "Server error", msg)
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
from PySide6.QtCore import QObject, QProcess, QTimer, Signal
|
|
2
|
+
from loguru import logger
|
|
3
|
+
|
|
4
|
+
_PROC_ERRORS = {
|
|
5
|
+
QProcess.ProcessError.FailedToStart: "Failed to start — is 'flowchem' on your PATH?",
|
|
6
|
+
QProcess.ProcessError.Crashed: "Process crashed unexpectedly",
|
|
7
|
+
QProcess.ProcessError.Timedout: "Process timed out",
|
|
8
|
+
QProcess.ProcessError.WriteError: "Write error communicating with process",
|
|
9
|
+
QProcess.ProcessError.ReadError: "Read error communicating with process",
|
|
10
|
+
QProcess.ProcessError.UnknownError: "Unknown process error",
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class ServerManager(QObject):
|
|
15
|
+
started = Signal()
|
|
16
|
+
stopped = Signal(int)
|
|
17
|
+
stdout_ready = Signal(str)
|
|
18
|
+
stderr_ready = Signal(str)
|
|
19
|
+
error = Signal(str)
|
|
20
|
+
|
|
21
|
+
def __init__(self, parent=None):
|
|
22
|
+
super().__init__(parent)
|
|
23
|
+
self._proc = QProcess(self)
|
|
24
|
+
self._proc.readyReadStandardOutput.connect(self._on_stdout)
|
|
25
|
+
self._proc.readyReadStandardError.connect(self._on_stderr)
|
|
26
|
+
self._proc.started.connect(self.started)
|
|
27
|
+
self._proc.finished.connect(self._on_finished)
|
|
28
|
+
self._proc.errorOccurred.connect(self._on_error)
|
|
29
|
+
|
|
30
|
+
def start(self, config_path: str, debug: bool = False, sim: bool = False):
|
|
31
|
+
exe = "flowchem-sim" if sim else "flowchem"
|
|
32
|
+
args = [config_path] + (["--debug"] if debug else [])
|
|
33
|
+
logger.info(f"Starting: {exe} {' '.join(args)}")
|
|
34
|
+
self._proc.start(exe, args)
|
|
35
|
+
|
|
36
|
+
def stop(self):
|
|
37
|
+
if not self.is_running():
|
|
38
|
+
return
|
|
39
|
+
logger.info("Stopping flowchem server")
|
|
40
|
+
self._proc.terminate()
|
|
41
|
+
QTimer.singleShot(3000, self._force_kill)
|
|
42
|
+
|
|
43
|
+
def is_running(self) -> bool:
|
|
44
|
+
return self._proc.state() == QProcess.ProcessState.Running
|
|
45
|
+
|
|
46
|
+
def _force_kill(self):
|
|
47
|
+
if self.is_running():
|
|
48
|
+
logger.warning("Server did not stop gracefully — killing process")
|
|
49
|
+
self._proc.kill()
|
|
50
|
+
|
|
51
|
+
def _on_stdout(self):
|
|
52
|
+
text = self._proc.readAllStandardOutput().data().decode(errors="replace")
|
|
53
|
+
self.stdout_ready.emit(text)
|
|
54
|
+
|
|
55
|
+
def _on_stderr(self):
|
|
56
|
+
text = self._proc.readAllStandardError().data().decode(errors="replace")
|
|
57
|
+
self.stderr_ready.emit(text)
|
|
58
|
+
|
|
59
|
+
def _on_error(self, error):
|
|
60
|
+
msg = _PROC_ERRORS.get(error, f"Process error ({error})")
|
|
61
|
+
logger.error(f"FlowChem process error: {msg}")
|
|
62
|
+
self.error.emit(msg)
|
|
63
|
+
|
|
64
|
+
def _on_finished(self, exit_code, _status):
|
|
65
|
+
logger.info(f"FlowChem exited with code {exit_code}")
|
|
66
|
+
self.stopped.emit(exit_code)
|
|
File without changes
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
from pathlib import Path
|
|
2
|
+
|
|
3
|
+
from PySide6.QtCore import QSettings
|
|
4
|
+
from PySide6.QtWidgets import (
|
|
5
|
+
QFileDialog,
|
|
6
|
+
QHBoxLayout,
|
|
7
|
+
QPlainTextEdit,
|
|
8
|
+
QVBoxLayout,
|
|
9
|
+
QWidget,
|
|
10
|
+
)
|
|
11
|
+
from qfluentwidgets import LineEdit, PrimaryPushButton, PushButton
|
|
12
|
+
from loguru import logger
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class ConfigTab(QWidget):
|
|
16
|
+
def __init__(self, parent=None):
|
|
17
|
+
super().__init__(parent)
|
|
18
|
+
self._settings = QSettings("flowchem", "gui")
|
|
19
|
+
|
|
20
|
+
self.path_edit = LineEdit()
|
|
21
|
+
self.path_edit.setPlaceholderText("Path to config.toml")
|
|
22
|
+
browse_btn = PushButton("Browse")
|
|
23
|
+
load_btn = PushButton("Load")
|
|
24
|
+
self.save_btn = PrimaryPushButton("Save")
|
|
25
|
+
self.editor = QPlainTextEdit()
|
|
26
|
+
|
|
27
|
+
self.path_edit.setToolTip("Path to the FlowChem TOML configuration file.")
|
|
28
|
+
browse_btn.setToolTip("Select a FlowChem TOML configuration file.")
|
|
29
|
+
load_btn.setToolTip("Load the selected TOML file into the editor.")
|
|
30
|
+
self.save_btn.setToolTip("Save the editor contents to the selected TOML file.")
|
|
31
|
+
self.editor.setToolTip("Edit the raw FlowChem TOML configuration.")
|
|
32
|
+
|
|
33
|
+
path_row = QHBoxLayout()
|
|
34
|
+
path_row.addWidget(self.path_edit)
|
|
35
|
+
path_row.addWidget(browse_btn)
|
|
36
|
+
path_row.addWidget(load_btn)
|
|
37
|
+
path_row.addWidget(self.save_btn)
|
|
38
|
+
|
|
39
|
+
layout = QVBoxLayout(self)
|
|
40
|
+
layout.addLayout(path_row)
|
|
41
|
+
layout.addWidget(self.editor)
|
|
42
|
+
|
|
43
|
+
browse_btn.clicked.connect(self._browse)
|
|
44
|
+
load_btn.clicked.connect(self._load)
|
|
45
|
+
self.save_btn.clicked.connect(self._save)
|
|
46
|
+
|
|
47
|
+
saved = self._settings.value("config_path", "")
|
|
48
|
+
if saved and Path(saved).exists():
|
|
49
|
+
self.path_edit.setText(saved)
|
|
50
|
+
self._load()
|
|
51
|
+
|
|
52
|
+
def get_config_path(self) -> str:
|
|
53
|
+
return self.path_edit.text()
|
|
54
|
+
|
|
55
|
+
def set_content(self, text: str):
|
|
56
|
+
self.editor.setPlainText(text)
|
|
57
|
+
|
|
58
|
+
def _browse(self):
|
|
59
|
+
path, _ = QFileDialog.getOpenFileName(
|
|
60
|
+
self, "Select config file", "", "TOML files (*.toml)"
|
|
61
|
+
)
|
|
62
|
+
if path:
|
|
63
|
+
self.path_edit.setText(path)
|
|
64
|
+
|
|
65
|
+
def _load(self):
|
|
66
|
+
path = Path(self.path_edit.text())
|
|
67
|
+
if not path.exists():
|
|
68
|
+
logger.warning(f"Config file not found: {path}")
|
|
69
|
+
return
|
|
70
|
+
self.editor.setPlainText(path.read_text(encoding="utf-8"))
|
|
71
|
+
self._settings.setValue("config_path", str(path))
|
|
72
|
+
logger.info(f"Loaded config: {path}")
|
|
73
|
+
|
|
74
|
+
def _save(self):
|
|
75
|
+
path = Path(self.path_edit.text())
|
|
76
|
+
path.write_text(self.editor.toPlainText(), encoding="utf-8")
|
|
77
|
+
self._settings.setValue("config_path", str(path))
|
|
78
|
+
logger.info(f"Saved config: {path}")
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
from PySide6.QtCore import QProcess
|
|
2
|
+
from PySide6.QtWidgets import (
|
|
3
|
+
QHBoxLayout,
|
|
4
|
+
QMessageBox,
|
|
5
|
+
QPlainTextEdit,
|
|
6
|
+
QVBoxLayout,
|
|
7
|
+
QWidget,
|
|
8
|
+
)
|
|
9
|
+
from qfluentwidgets import PrimaryPushButton, PushButton
|
|
10
|
+
from loguru import logger
|
|
11
|
+
|
|
12
|
+
_WARNING = (
|
|
13
|
+
"Autodiscover communicates over serial ports.\n"
|
|
14
|
+
"Unsupported devices could be placed in an unsafe state.\n\n"
|
|
15
|
+
"Continue?"
|
|
16
|
+
)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class DiscoverTab(QWidget):
|
|
20
|
+
def __init__(self, config_tab, parent=None):
|
|
21
|
+
super().__init__(parent)
|
|
22
|
+
self._cfg = config_tab
|
|
23
|
+
self._proc = QProcess(self)
|
|
24
|
+
|
|
25
|
+
self.output = QPlainTextEdit(readOnly=True)
|
|
26
|
+
run_btn = PrimaryPushButton("Run autodiscover")
|
|
27
|
+
self.copy_btn = PushButton("Copy to editor")
|
|
28
|
+
self.copy_btn.setEnabled(False)
|
|
29
|
+
|
|
30
|
+
self.output.setToolTip("Autodiscover output from stdout and stderr.")
|
|
31
|
+
run_btn.setToolTip("Run flowchem-autodiscover after confirming the warning.")
|
|
32
|
+
self.copy_btn.setToolTip("Copy autodiscover output into the Config editor.")
|
|
33
|
+
|
|
34
|
+
btn_row = QHBoxLayout()
|
|
35
|
+
btn_row.addWidget(run_btn)
|
|
36
|
+
btn_row.addWidget(self.copy_btn)
|
|
37
|
+
btn_row.addStretch()
|
|
38
|
+
|
|
39
|
+
layout = QVBoxLayout(self)
|
|
40
|
+
layout.addLayout(btn_row)
|
|
41
|
+
layout.addWidget(self.output)
|
|
42
|
+
|
|
43
|
+
run_btn.clicked.connect(self._run)
|
|
44
|
+
self.copy_btn.clicked.connect(self._copy_to_editor)
|
|
45
|
+
self._proc.readyReadStandardOutput.connect(
|
|
46
|
+
lambda: self._append(self._proc.readAllStandardOutput().data())
|
|
47
|
+
)
|
|
48
|
+
self._proc.readyReadStandardError.connect(
|
|
49
|
+
lambda: self._append(self._proc.readAllStandardError().data())
|
|
50
|
+
)
|
|
51
|
+
self._proc.finished.connect(self._on_finished)
|
|
52
|
+
|
|
53
|
+
def _run(self):
|
|
54
|
+
reply = QMessageBox.warning(
|
|
55
|
+
self,
|
|
56
|
+
"Warning",
|
|
57
|
+
_WARNING,
|
|
58
|
+
QMessageBox.StandardButton.Ok | QMessageBox.StandardButton.Cancel,
|
|
59
|
+
)
|
|
60
|
+
if reply != QMessageBox.StandardButton.Ok:
|
|
61
|
+
return
|
|
62
|
+
self.output.clear()
|
|
63
|
+
self.copy_btn.setEnabled(False)
|
|
64
|
+
logger.info("Running flowchem-autodiscover")
|
|
65
|
+
self._proc.start("flowchem-autodiscover", ["--assume-yes"])
|
|
66
|
+
|
|
67
|
+
def _append(self, data: bytes):
|
|
68
|
+
text = data.decode(errors="replace").rstrip()
|
|
69
|
+
if text:
|
|
70
|
+
self.output.appendPlainText(text)
|
|
71
|
+
|
|
72
|
+
def _on_finished(self, exit_code, _status):
|
|
73
|
+
logger.info(f"Autodiscover finished (exit code {exit_code})")
|
|
74
|
+
self.copy_btn.setEnabled(bool(self.output.toPlainText()))
|
|
75
|
+
|
|
76
|
+
def _copy_to_editor(self):
|
|
77
|
+
self._cfg.set_content(self.output.toPlainText())
|
|
78
|
+
logger.info("Autodiscover output copied to config editor")
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
from PySide6.QtWidgets import (
|
|
2
|
+
QHBoxLayout,
|
|
3
|
+
QPlainTextEdit,
|
|
4
|
+
QVBoxLayout,
|
|
5
|
+
QWidget,
|
|
6
|
+
)
|
|
7
|
+
from qfluentwidgets import PushButton
|
|
8
|
+
from loguru import logger
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class LogsTab(QWidget):
|
|
12
|
+
def __init__(self, parent=None):
|
|
13
|
+
super().__init__(parent)
|
|
14
|
+
self.log_view = QPlainTextEdit(readOnly=True)
|
|
15
|
+
|
|
16
|
+
clear_btn = PushButton("Clear")
|
|
17
|
+
self.log_view.setToolTip("Live FlowChem process output and GUI log events.")
|
|
18
|
+
clear_btn.setToolTip("Clear the displayed log output.")
|
|
19
|
+
clear_btn.clicked.connect(self.log_view.clear)
|
|
20
|
+
|
|
21
|
+
btn_row = QHBoxLayout()
|
|
22
|
+
btn_row.addStretch()
|
|
23
|
+
btn_row.addWidget(clear_btn)
|
|
24
|
+
|
|
25
|
+
layout = QVBoxLayout(self)
|
|
26
|
+
layout.addWidget(self.log_view)
|
|
27
|
+
layout.addLayout(btn_row)
|
|
28
|
+
|
|
29
|
+
logger.add(
|
|
30
|
+
self._log_sink,
|
|
31
|
+
level="DEBUG",
|
|
32
|
+
format="{time:HH:mm:ss} | {level:<8} | {message}",
|
|
33
|
+
)
|
|
34
|
+
|
|
35
|
+
def _log_sink(self, message):
|
|
36
|
+
self._append(message.strip())
|
|
37
|
+
|
|
38
|
+
def append_process_output(self, text: str):
|
|
39
|
+
self._append(text.rstrip())
|
|
40
|
+
|
|
41
|
+
def _append(self, text: str):
|
|
42
|
+
if not text:
|
|
43
|
+
return
|
|
44
|
+
self.log_view.appendPlainText(text)
|
|
45
|
+
sb = self.log_view.verticalScrollBar()
|
|
46
|
+
sb.setValue(sb.maximum())
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
import socket
|
|
2
|
+
from urllib.parse import urlparse
|
|
3
|
+
|
|
4
|
+
from PySide6.QtCore import QUrl
|
|
5
|
+
from PySide6.QtWidgets import (
|
|
6
|
+
QFormLayout,
|
|
7
|
+
QHBoxLayout,
|
|
8
|
+
QMessageBox,
|
|
9
|
+
QVBoxLayout,
|
|
10
|
+
QWidget,
|
|
11
|
+
)
|
|
12
|
+
from qfluentwidgets import CheckBox, HyperlinkButton, LineEdit, PrimaryPushButton
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class ServerTab(QWidget):
|
|
16
|
+
def __init__(self, server_manager, config_tab, parent=None):
|
|
17
|
+
super().__init__(parent)
|
|
18
|
+
self._mgr = server_manager
|
|
19
|
+
self._cfg = config_tab
|
|
20
|
+
|
|
21
|
+
self.address_edit = LineEdit()
|
|
22
|
+
self.address_edit.setText("http://localhost:8000")
|
|
23
|
+
self.debug_chk = CheckBox("Debug mode")
|
|
24
|
+
self.sim_chk = CheckBox("Simulation mode")
|
|
25
|
+
self.toggle_btn = PrimaryPushButton("Start")
|
|
26
|
+
self.open_btn = HyperlinkButton(self._resolved_docs_url(), self._docs_label())
|
|
27
|
+
|
|
28
|
+
self.address_edit.setToolTip("Base URL for the running FlowChem server.")
|
|
29
|
+
self.debug_chk.setToolTip("Pass --debug to FlowChem for verbose logging.")
|
|
30
|
+
self.sim_chk.setToolTip("Launch flowchem-sim instead of real device drivers.")
|
|
31
|
+
self.toggle_btn.setToolTip("Start or stop the FlowChem server.")
|
|
32
|
+
self.open_btn.setToolTip("Open the server API documentation in your browser.")
|
|
33
|
+
|
|
34
|
+
form = QFormLayout()
|
|
35
|
+
form.addRow("Server address:", self.address_edit)
|
|
36
|
+
form.addRow("", self.debug_chk)
|
|
37
|
+
form.addRow("", self.sim_chk)
|
|
38
|
+
|
|
39
|
+
btn_row = QHBoxLayout()
|
|
40
|
+
btn_row.addWidget(self.toggle_btn)
|
|
41
|
+
btn_row.addWidget(self.open_btn)
|
|
42
|
+
btn_row.addStretch()
|
|
43
|
+
|
|
44
|
+
layout = QVBoxLayout(self)
|
|
45
|
+
layout.addLayout(form)
|
|
46
|
+
layout.addLayout(btn_row)
|
|
47
|
+
layout.addStretch()
|
|
48
|
+
|
|
49
|
+
self.toggle_btn.clicked.connect(self._toggle)
|
|
50
|
+
self.address_edit.textChanged.connect(self._update_open_btn)
|
|
51
|
+
server_manager.started.connect(self._on_started)
|
|
52
|
+
server_manager.stopped.connect(self._on_stopped)
|
|
53
|
+
|
|
54
|
+
def _toggle(self):
|
|
55
|
+
if self._mgr.is_running():
|
|
56
|
+
self._mgr.stop()
|
|
57
|
+
else:
|
|
58
|
+
path = self._cfg.get_config_path()
|
|
59
|
+
if not path:
|
|
60
|
+
QMessageBox.warning(
|
|
61
|
+
self,
|
|
62
|
+
"No config file",
|
|
63
|
+
"Please select a config file in the Config editor tab before starting the server.",
|
|
64
|
+
)
|
|
65
|
+
return
|
|
66
|
+
self._mgr.start(
|
|
67
|
+
path, debug=self.debug_chk.isChecked(), sim=self.sim_chk.isChecked()
|
|
68
|
+
)
|
|
69
|
+
|
|
70
|
+
def _resolved_docs_url(self) -> str:
|
|
71
|
+
base = self.address_edit.text().rstrip("/")
|
|
72
|
+
try:
|
|
73
|
+
parsed = urlparse(base)
|
|
74
|
+
if parsed.hostname in ("localhost", "127.0.0.1", "::1"):
|
|
75
|
+
ip = socket.gethostbyname(socket.gethostname())
|
|
76
|
+
base = base.replace(parsed.hostname, ip, 1)
|
|
77
|
+
except OSError:
|
|
78
|
+
pass
|
|
79
|
+
return base + "/docs"
|
|
80
|
+
|
|
81
|
+
def _docs_label(self) -> str:
|
|
82
|
+
url = self._resolved_docs_url()
|
|
83
|
+
display = url.replace("http://", "").replace("https://", "")
|
|
84
|
+
return f"Open API browser — {display}"
|
|
85
|
+
|
|
86
|
+
def _update_open_btn(self):
|
|
87
|
+
self.open_btn.setUrl(QUrl(self._resolved_docs_url()))
|
|
88
|
+
self.open_btn.setText(self._docs_label())
|
|
89
|
+
|
|
90
|
+
def _on_started(self):
|
|
91
|
+
self.toggle_btn.setText("Stop")
|
|
92
|
+
self.toggle_btn.setToolTip("Stop the running FlowChem server.")
|
|
93
|
+
self.address_edit.setEnabled(False)
|
|
94
|
+
self.debug_chk.setEnabled(False)
|
|
95
|
+
self.sim_chk.setEnabled(False)
|
|
96
|
+
|
|
97
|
+
def _on_stopped(self, _exit_code):
|
|
98
|
+
self.toggle_btn.setText("Start")
|
|
99
|
+
self.toggle_btn.setToolTip("Start the FlowChem server.")
|
|
100
|
+
self.address_edit.setEnabled(True)
|
|
101
|
+
self.debug_chk.setEnabled(True)
|
|
102
|
+
self.sim_chk.setEnabled(True)
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
from PySide6.QtGui import QIcon
|
|
2
|
+
from PySide6.QtWidgets import QApplication, QMenu, QSystemTrayIcon
|
|
3
|
+
from loguru import logger
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class TrayIcon(QSystemTrayIcon):
|
|
7
|
+
def __init__(self, window, server_manager, icon: QIcon, parent=None):
|
|
8
|
+
super().__init__(icon, parent)
|
|
9
|
+
self._window = window
|
|
10
|
+
self._mgr = server_manager
|
|
11
|
+
self.setToolTip("Flowchem Manager")
|
|
12
|
+
|
|
13
|
+
menu = QMenu()
|
|
14
|
+
act_show = menu.addAction("Show window", window.show)
|
|
15
|
+
act_show.setToolTip("Show the FlowChem Manager window.")
|
|
16
|
+
menu.addSeparator()
|
|
17
|
+
self._act_start = menu.addAction("Start server", self._start)
|
|
18
|
+
self._act_stop = menu.addAction("Stop server", server_manager.stop)
|
|
19
|
+
self._act_start.setToolTip("Start the FlowChem server.")
|
|
20
|
+
self._act_stop.setToolTip("Stop the FlowChem server.")
|
|
21
|
+
menu.addSeparator()
|
|
22
|
+
act_quit = menu.addAction("Quit", QApplication.quit)
|
|
23
|
+
act_quit.setToolTip("Quit FlowChem Manager.")
|
|
24
|
+
self.setContextMenu(menu)
|
|
25
|
+
|
|
26
|
+
self.activated.connect(self._on_activated)
|
|
27
|
+
server_manager.started.connect(self._on_started)
|
|
28
|
+
server_manager.stopped.connect(self._on_stopped)
|
|
29
|
+
self._on_stopped(0)
|
|
30
|
+
|
|
31
|
+
if not QSystemTrayIcon.isSystemTrayAvailable():
|
|
32
|
+
logger.warning("System tray not available on this desktop environment")
|
|
33
|
+
else:
|
|
34
|
+
self.show()
|
|
35
|
+
|
|
36
|
+
def _on_activated(self, reason):
|
|
37
|
+
if reason == QSystemTrayIcon.ActivationReason.DoubleClick:
|
|
38
|
+
self._window.show()
|
|
39
|
+
self._window.raise_()
|
|
40
|
+
|
|
41
|
+
def _start(self):
|
|
42
|
+
self._window.show()
|
|
43
|
+
self._window.server_tab._toggle()
|
|
44
|
+
|
|
45
|
+
def _on_started(self):
|
|
46
|
+
self._act_start.setEnabled(False)
|
|
47
|
+
self._act_stop.setEnabled(True)
|
|
48
|
+
|
|
49
|
+
def _on_stopped(self, _exit_code=0):
|
|
50
|
+
self._act_start.setEnabled(True)
|
|
51
|
+
self._act_stop.setEnabled(False)
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: flowchem-qt
|
|
3
|
+
Version: 0.0.1
|
|
4
|
+
Requires-Python: >=3.11
|
|
5
|
+
License-File: LICENSE
|
|
6
|
+
Requires-Dist: flowchem
|
|
7
|
+
Requires-Dist: PySide6>=6.5
|
|
8
|
+
Requires-Dist: PySide6-Fluent-Widgets>=1.11.2
|
|
9
|
+
Requires-Dist: tomli-w>=1.0
|
|
10
|
+
Requires-Dist: loguru>=0.7
|
|
11
|
+
Requires-Dist: zeroconf>=0.149.7
|
|
12
|
+
Provides-Extra: test
|
|
13
|
+
Requires-Dist: pytest; extra == "test"
|
|
14
|
+
Dynamic: license-file
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
LICENSE
|
|
2
|
+
README.md
|
|
3
|
+
main.py
|
|
4
|
+
pyproject.toml
|
|
5
|
+
app/__init__.py
|
|
6
|
+
app/main_window.py
|
|
7
|
+
app/server_manager.py
|
|
8
|
+
app/tray.py
|
|
9
|
+
app/tabs/__init__.py
|
|
10
|
+
app/tabs/config_tab.py
|
|
11
|
+
app/tabs/discover_tab.py
|
|
12
|
+
app/tabs/logs_tab.py
|
|
13
|
+
app/tabs/server_tab.py
|
|
14
|
+
flowchem_qt.egg-info/PKG-INFO
|
|
15
|
+
flowchem_qt.egg-info/SOURCES.txt
|
|
16
|
+
flowchem_qt.egg-info/dependency_links.txt
|
|
17
|
+
flowchem_qt.egg-info/entry_points.txt
|
|
18
|
+
flowchem_qt.egg-info/requires.txt
|
|
19
|
+
flowchem_qt.egg-info/top_level.txt
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import ctypes
|
|
2
|
+
import sys
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
|
|
5
|
+
from qfluentwidgets import Theme, setTheme, setThemeColor
|
|
6
|
+
from PySide6.QtGui import QIcon
|
|
7
|
+
from PySide6.QtWidgets import QApplication
|
|
8
|
+
|
|
9
|
+
from app.main_window import MainWindow
|
|
10
|
+
from app.tray import TrayIcon
|
|
11
|
+
|
|
12
|
+
_APP_ID = "org.flowchem.gui"
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def _set_windows_app_id():
|
|
16
|
+
if sys.platform != "win32":
|
|
17
|
+
return
|
|
18
|
+
ctypes.windll.shell32.SetCurrentProcessExplicitAppUserModelID(_APP_ID)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def main():
|
|
22
|
+
_set_windows_app_id()
|
|
23
|
+
|
|
24
|
+
# pythonw suppresses the console — redirect stderr so crashes before the
|
|
25
|
+
# Qt window appears are not silently swallowed.
|
|
26
|
+
log_path = Path.home() / ".flowchem-qt" / "error.log"
|
|
27
|
+
log_path.parent.mkdir(exist_ok=True)
|
|
28
|
+
sys.stderr = open(log_path, "a", encoding="utf-8")
|
|
29
|
+
|
|
30
|
+
app = QApplication(sys.argv)
|
|
31
|
+
setTheme(Theme.AUTO)
|
|
32
|
+
setThemeColor("#0065d5")
|
|
33
|
+
app.setQuitOnLastWindowClosed(False)
|
|
34
|
+
|
|
35
|
+
icon_dir = Path(__file__).parent / "resources" / "icons"
|
|
36
|
+
window_icon = QIcon(str(icon_dir / "flowchem_app_icon.ico"))
|
|
37
|
+
tray_icon = QIcon(str(icon_dir / "flowchem_logo.svg"))
|
|
38
|
+
app.setWindowIcon(window_icon)
|
|
39
|
+
|
|
40
|
+
window = MainWindow()
|
|
41
|
+
TrayIcon(window, window.server_manager, tray_icon, app)
|
|
42
|
+
window.show()
|
|
43
|
+
sys.exit(app.exec())
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
if __name__ == "__main__":
|
|
47
|
+
main()
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=68"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "flowchem-qt"
|
|
7
|
+
version = "0.0.1"
|
|
8
|
+
requires-python = ">=3.11"
|
|
9
|
+
dependencies = [
|
|
10
|
+
"flowchem",
|
|
11
|
+
"PySide6>=6.5",
|
|
12
|
+
"PySide6-Fluent-Widgets>=1.11.2",
|
|
13
|
+
"tomli-w>=1.0",
|
|
14
|
+
"loguru>=0.7",
|
|
15
|
+
"zeroconf>=0.149.7",
|
|
16
|
+
]
|
|
17
|
+
|
|
18
|
+
[project.optional-dependencies]
|
|
19
|
+
test = ["pytest"]
|
|
20
|
+
|
|
21
|
+
[project.gui-scripts]
|
|
22
|
+
flowchem-qt = "main:main"
|
|
23
|
+
|
|
24
|
+
[tool.setuptools]
|
|
25
|
+
py-modules = ["main"]
|
|
26
|
+
packages = ["app", "app.tabs"]
|