Compare commits
12 Commits
Author | SHA1 | Date | |
---|---|---|---|
![]() |
b3bb6d8e47 | ||
![]() |
1a090224ad | ||
![]() |
9ac3f73566 | ||
![]() |
e42ef07be9 | ||
![]() |
2395d89206 | ||
![]() |
acdbb7beb1 | ||
![]() |
9b3f2af772 | ||
![]() |
a0f7de8b70 | ||
![]() |
bb2249c574 | ||
![]() |
0e61cd37c3 | ||
![]() |
d6442077e4 | ||
![]() |
a106f817b5 |
31
.scripts/convert_docs_for_website.py
Normal file
31
.scripts/convert_docs_for_website.py
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
"""
|
||||||
|
Convert the docs to html, so that they can be copy-pasted into the BayernCollab Wiki
|
||||||
|
"""
|
||||||
|
|
||||||
|
import os, markdown
|
||||||
|
|
||||||
|
DOC_DIR = "~/cpd-dev/cpdctrl-gui/cpdctrl_gui/resources"
|
||||||
|
OUT_DIR = "~/cpd-dev/docs"
|
||||||
|
|
||||||
|
def convert(filepath: str):
|
||||||
|
with open(filepath, "r") as file:
|
||||||
|
content = file.read()
|
||||||
|
html = markdown.markdown(content, output_format="html")
|
||||||
|
return html
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
DOC_DIR = os.path.expanduser(DOC_DIR)
|
||||||
|
docs = [os.path.join(DOC_DIR, f) for f in os.listdir(DOC_DIR)]
|
||||||
|
docs.sort()
|
||||||
|
OUT_DIR = os.path.expanduser(OUT_DIR)
|
||||||
|
if not os.path.isdir(OUT_DIR):
|
||||||
|
os.makedirs(OUT_DIR)
|
||||||
|
|
||||||
|
for file in docs:
|
||||||
|
if file.endswith(".md"):
|
||||||
|
html_file = os.path.join(OUT_DIR, os.path.basename(file).replace(".md", ".html"))
|
||||||
|
print(f"{file} -> {html_file}")
|
||||||
|
html = convert(file)
|
||||||
|
html = html.replace("<br>", "<br />")
|
||||||
|
with open(html_file, "w") as file:
|
||||||
|
file.write(html)
|
9
.scripts/cpdctrl_gui_launcher.py
Normal file
9
.scripts/cpdctrl_gui_launcher.py
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
# import and run cpdctrl_gui
|
||||||
|
# when packaging this with pyinstaller,
|
||||||
|
# cpdctrl_gui will be imported as python package and thus
|
||||||
|
# have the package metadata (including version number) available
|
||||||
|
from cpdctrl_gui import init
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
import sys
|
||||||
|
sys.exit(init.run())
|
16
.scripts/install_gui.ps1
Normal file
16
.scripts/install_gui.ps1
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
# This creates & installs cpdctrl-gui using pyinstaller
|
||||||
|
# into a single, gigantic exe file. Very modern.
|
||||||
|
# The --paths ..\cpdctrl is necessary, otherwise it takes the version that was installed with pip
|
||||||
|
cd ~\cpd-dev
|
||||||
|
.\venv\Scripts\activate.ps1
|
||||||
|
mkdir build
|
||||||
|
cd build
|
||||||
|
pyinstaller ..\cpdctrl_gui_launcher.py `
|
||||||
|
--onefile `
|
||||||
|
--name "cpdctrl-gui" `
|
||||||
|
--windowed `
|
||||||
|
--icon ..\cpdctrl-gui\cpdctrl_gui\resources\icons\icon.png `
|
||||||
|
--exclude-module pyside6 `
|
||||||
|
--paths ..\cpdctrl `
|
||||||
|
--paths ..\cpdctrl_gui `
|
||||||
|
--add-data ..\cpdctrl-gui\cpdctrl_gui\resources:cpdctrl_gui\resources
|
75
.scripts/make_manuals.py
Normal file
75
.scripts/make_manuals.py
Normal file
@ -0,0 +1,75 @@
|
|||||||
|
"""
|
||||||
|
Create pdf manuals from the markdown files in the cpdctrl-gui resources directory
|
||||||
|
"""
|
||||||
|
import os, markdown
|
||||||
|
import pymupdf
|
||||||
|
import datetime
|
||||||
|
|
||||||
|
from markdown_pdf import MarkdownPdf, Section
|
||||||
|
|
||||||
|
DOC_DIR = "~/cpd-dev/cpdctrl-gui/cpdctrl_gui/resources"
|
||||||
|
OUT_DIR = "~/cpd-dev/docs"
|
||||||
|
|
||||||
|
def convert(filepath: str):
|
||||||
|
with open(filepath, "r") as file:
|
||||||
|
content = file.read()
|
||||||
|
html = markdown.markdown(content, output_format="html")
|
||||||
|
return html
|
||||||
|
|
||||||
|
def load_md(filename: str):
|
||||||
|
with open(os.path.join(DOC_DIR, filename), "r") as file:
|
||||||
|
return file.read()
|
||||||
|
|
||||||
|
metadata = {
|
||||||
|
"author": "Matthias Quintern",
|
||||||
|
"modDate": datetime.datetime.now().isoformat(),
|
||||||
|
}
|
||||||
|
|
||||||
|
def make_software_manual():
|
||||||
|
filepath = os.path.join(OUT_DIR, "cpdctrl-gui-manual.pdf")
|
||||||
|
sections = [[
|
||||||
|
"# N203 Vacuum CPD Setup: User Manual\n",
|
||||||
|
"user_guide.md",
|
||||||
|
"sample_changing.md",
|
||||||
|
"measurement_settings.md",
|
||||||
|
"led_control.md",
|
||||||
|
"controller_calibration.md",
|
||||||
|
"troubleshooting.md",
|
||||||
|
], [
|
||||||
|
"# cpdctrl Software Documentation",
|
||||||
|
"about.md",
|
||||||
|
"technical_information.md",
|
||||||
|
]
|
||||||
|
]
|
||||||
|
|
||||||
|
pdf = MarkdownPdf(toc_level=2)
|
||||||
|
|
||||||
|
for section in sections:
|
||||||
|
sec_md = ""
|
||||||
|
for entry in section:
|
||||||
|
if entry.endswith(".md"):
|
||||||
|
entry_md = load_md(entry)
|
||||||
|
else:
|
||||||
|
entry_md = entry
|
||||||
|
sec_md += entry_md + "\n"
|
||||||
|
pdf.add_section(Section(sec_md))
|
||||||
|
|
||||||
|
|
||||||
|
for key, val in (metadata | { "title": "cpdctrl-gui-manual"}).items():
|
||||||
|
pdf.meta[key] = val
|
||||||
|
pdf.save(filepath)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
def recorder(elpos):
|
||||||
|
pass
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
DOC_DIR = os.path.expanduser(DOC_DIR)
|
||||||
|
docs = [os.path.join(DOC_DIR, f) for f in os.listdir(DOC_DIR)]
|
||||||
|
docs.sort()
|
||||||
|
OUT_DIR = os.path.expanduser(OUT_DIR)
|
||||||
|
if not os.path.isdir(OUT_DIR):
|
||||||
|
os.makedirs(OUT_DIR)
|
||||||
|
|
||||||
|
make_software_manual()
|
@ -48,6 +48,7 @@ logging.basicConfig(
|
|||||||
)
|
)
|
||||||
log = logging.getLogger(__name__)
|
log = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
# Mechanism to catch, log and display uncaught exceptions
|
||||||
# This is taken from:
|
# This is taken from:
|
||||||
# https://timlehr.com/2018/01/python-exception-hooks-with-qt-message-box/index.html
|
# https://timlehr.com/2018/01/python-exception-hooks-with-qt-message-box/index.html
|
||||||
handler = logging.StreamHandler(stream=sys.stdout)
|
handler = logging.StreamHandler(stream=sys.stdout)
|
||||||
@ -59,7 +60,8 @@ def show_exception_box(log_msg):
|
|||||||
"""
|
"""
|
||||||
if QtWidgets.QApplication.instance() is not None:
|
if QtWidgets.QApplication.instance() is not None:
|
||||||
errorbox = QtWidgets.QMessageBox()
|
errorbox = QtWidgets.QMessageBox()
|
||||||
errorbox.setText("Oops. An unexpected error occured:\n{0}".format(log_msg))
|
errorbox.setWindowTitle("An Unexpected Error Occurred")
|
||||||
|
errorbox.setText(log_msg)
|
||||||
errorbox.exec()
|
errorbox.exec()
|
||||||
else:
|
else:
|
||||||
log.debug("No QApplication instance available.")
|
log.debug("No QApplication instance available.")
|
||||||
@ -67,8 +69,9 @@ def show_exception_box(log_msg):
|
|||||||
class UncaughtHook(QtCore.QObject):
|
class UncaughtHook(QtCore.QObject):
|
||||||
_exception_caught = QtCore.pyqtSignal(object)
|
_exception_caught = QtCore.pyqtSignal(object)
|
||||||
|
|
||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, show_traceback=False, **kwargs):
|
||||||
super(UncaughtHook, self).__init__(*args, **kwargs)
|
super(UncaughtHook, self).__init__(*args, **kwargs)
|
||||||
|
self.show_traceback = show_traceback
|
||||||
|
|
||||||
# this registers the exception_hook() function as hook with the Python interpreter
|
# this registers the exception_hook() function as hook with the Python interpreter
|
||||||
sys.excepthook = self.exception_hook
|
sys.excepthook = self.exception_hook
|
||||||
@ -85,12 +88,14 @@ class UncaughtHook(QtCore.QObject):
|
|||||||
sys.__excepthook__(exc_type, exc_value, exc_traceback)
|
sys.__excepthook__(exc_type, exc_value, exc_traceback)
|
||||||
else:
|
else:
|
||||||
exc_info = (exc_type, exc_value, exc_traceback)
|
exc_info = (exc_type, exc_value, exc_traceback)
|
||||||
log_msg = '\n'.join([''.join(traceback.format_tb(exc_traceback)),
|
log_msg = f"{exc_type.__name__}: {exc_value}\n"
|
||||||
'{0}: {1}'.format(exc_type.__name__, exc_value)])
|
if self.show_traceback:
|
||||||
|
log_msg += "\n".join(traceback.format_tb(exc_traceback))
|
||||||
log.critical(f"Uncaught exception:\n {log_msg}", exc_info=exc_info)
|
log.critical(f"Uncaught exception:\n {log_msg}", exc_info=exc_info)
|
||||||
|
|
||||||
# trigger message box show
|
# trigger message box show
|
||||||
self._exception_caught.emit(log_msg)
|
self._exception_caught.emit(log_msg)
|
||||||
|
|
||||||
# create a global instance of our class to register the hook
|
if AppConfig.MAIN_CFG.get_or("error_show_dialog", True):
|
||||||
# qt_exception_hook = UncaughtHook()
|
# create a global instance of our class to register the hook
|
||||||
|
qt_exception_hook = UncaughtHook(show_traceback=AppConfig.MAIN_CFG.get_or("error_stack_trace", False))
|
||||||
|
@ -1,3 +1,4 @@
|
|||||||
|
import os
|
||||||
import sys
|
import sys
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
if __package__ is None:
|
if __package__ is None:
|
||||||
@ -14,5 +15,4 @@ from cpdctrl_gui import init
|
|||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
import sys
|
import sys
|
||||||
|
sys.exit(init.run())
|
||||||
sys.exit(init.run())
|
|
@ -1,5 +1,6 @@
|
|||||||
## About
|
## About
|
||||||
|
`cpdctrl-gui` is a program for conducting contact potential difference (CPD) measurements under illumination.
|
||||||
|
|
||||||
- Author: Matthias Quintern
|
- Author: Matthias Quintern
|
||||||
- License: [GPLv3](https://www.gnu.org/licenses/gpl-3.0.en.html)
|
- License: [GPLv3](https://www.gnu.org/licenses/gpl-3.0.en.html)
|
||||||
- Source Code: https://git.quintern.xyz/MatthiasQuintern/cpdctrl-gui
|
- Source Code: https://git.quintern.xyz/MatthiasQuintern/cpdctrl-gui[controller_calibration.md](controller_calibration.md)
|
13
cpdctrl_gui/resources/controller_calibration.md
Normal file
13
cpdctrl_gui/resources/controller_calibration.md
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
## CPD Controller Calibration
|
||||||
|
**TODO: validate and extend**
|
||||||
|
|
||||||
|
This process is not always necessary. Ask the superuser!
|
||||||
|
1. Set CPD Controller to:
|
||||||
|
- Manual
|
||||||
|
- Offset=Gold Work Function
|
||||||
|
- Filter off (so you can see what happens immediately)
|
||||||
|
- Oscillator to ~2
|
||||||
|
2. Move the micrometer screw close until a signal appears
|
||||||
|
3. Set Automatic offset so that the output is 0V
|
||||||
|
|
||||||
|
|
91
cpdctrl_gui/resources/icons/power_off.svg
Normal file
91
cpdctrl_gui/resources/icons/power_off.svg
Normal file
@ -0,0 +1,91 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||||
|
<!-- Created with Inkscape (http://www.inkscape.org/) -->
|
||||||
|
|
||||||
|
<svg
|
||||||
|
width="128"
|
||||||
|
height="128"
|
||||||
|
viewBox="0 0 128 128"
|
||||||
|
version="1.1"
|
||||||
|
id="svg1"
|
||||||
|
inkscape:version="1.4 (86a8ad7, 2024-10-11)"
|
||||||
|
sodipodi:docname="power_off.svg"
|
||||||
|
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||||
|
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
xmlns:svg="http://www.w3.org/2000/svg">
|
||||||
|
<sodipodi:namedview
|
||||||
|
id="namedview1"
|
||||||
|
pagecolor="#ffffff"
|
||||||
|
bordercolor="#000000"
|
||||||
|
borderopacity="0.25"
|
||||||
|
inkscape:showpageshadow="2"
|
||||||
|
inkscape:pageopacity="0.0"
|
||||||
|
inkscape:pagecheckerboard="0"
|
||||||
|
inkscape:deskcolor="#d1d1d1"
|
||||||
|
inkscape:document-units="px"
|
||||||
|
inkscape:export-bgcolor="#ffffff00"
|
||||||
|
showgrid="true"
|
||||||
|
inkscape:zoom="2.9023437"
|
||||||
|
inkscape:cx="23.084792"
|
||||||
|
inkscape:cy="-8.7860028"
|
||||||
|
inkscape:window-width="1680"
|
||||||
|
inkscape:window-height="987"
|
||||||
|
inkscape:window-x="-8"
|
||||||
|
inkscape:window-y="-8"
|
||||||
|
inkscape:window-maximized="1"
|
||||||
|
inkscape:current-layer="layer2">
|
||||||
|
<inkscape:grid
|
||||||
|
id="grid1"
|
||||||
|
units="px"
|
||||||
|
originx="0"
|
||||||
|
originy="0"
|
||||||
|
spacingx="1"
|
||||||
|
spacingy="1"
|
||||||
|
empcolor="#0099e5"
|
||||||
|
empopacity="0.30196078"
|
||||||
|
color="#0099e5"
|
||||||
|
opacity="0.14901961"
|
||||||
|
empspacing="8"
|
||||||
|
enabled="true"
|
||||||
|
visible="true" />
|
||||||
|
</sodipodi:namedview>
|
||||||
|
<defs
|
||||||
|
id="defs1" />
|
||||||
|
<g
|
||||||
|
inkscape:label="Ebene 1"
|
||||||
|
inkscape:groupmode="layer"
|
||||||
|
id="layer1">
|
||||||
|
<path
|
||||||
|
style="fill:none;stroke:#000000;stroke-width:8;stroke-linecap:round;stroke-linejoin:round"
|
||||||
|
id="path1"
|
||||||
|
sodipodi:type="arc"
|
||||||
|
sodipodi:cx="64"
|
||||||
|
sodipodi:cy="64"
|
||||||
|
sodipodi:rx="40"
|
||||||
|
sodipodi:ry="40"
|
||||||
|
sodipodi:start="5.4977871"
|
||||||
|
sodipodi:end="3.9269908"
|
||||||
|
sodipodi:arc-type="arc"
|
||||||
|
d="M 92.28427,35.715728 A 40,40 0 0 1 100.95518,79.307336 40,40 0 0 1 64,104 40,40 0 0 1 27.044819,79.307338 40,40 0 0 1 35.715728,35.715729"
|
||||||
|
sodipodi:open="true" />
|
||||||
|
<path
|
||||||
|
style="fill:none;stroke:#000000;stroke-width:8;stroke-linecap:round;stroke-linejoin:round"
|
||||||
|
d="M 64,16 V 64"
|
||||||
|
id="path2" />
|
||||||
|
</g>
|
||||||
|
<g
|
||||||
|
inkscape:groupmode="layer"
|
||||||
|
id="layer2"
|
||||||
|
inkscape:label="Off">
|
||||||
|
<circle
|
||||||
|
style="fill:none;stroke:#aa0000;stroke-width:8;stroke-linecap:round;stroke-linejoin:round"
|
||||||
|
id="path3"
|
||||||
|
cx="64"
|
||||||
|
cy="64"
|
||||||
|
r="60" />
|
||||||
|
<path
|
||||||
|
style="fill:none;stroke:#aa0000;stroke-width:8;stroke-linecap:round;stroke-linejoin:round"
|
||||||
|
d="m 22,22 82,82"
|
||||||
|
id="path4" />
|
||||||
|
</g>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 2.6 KiB |
76
cpdctrl_gui/resources/icons/power_on.svg
Normal file
76
cpdctrl_gui/resources/icons/power_on.svg
Normal file
@ -0,0 +1,76 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||||
|
<!-- Created with Inkscape (http://www.inkscape.org/) -->
|
||||||
|
|
||||||
|
<svg
|
||||||
|
width="128"
|
||||||
|
height="128"
|
||||||
|
viewBox="0 0 128 128"
|
||||||
|
version="1.1"
|
||||||
|
id="svg1"
|
||||||
|
inkscape:version="1.4 (86a8ad7, 2024-10-11)"
|
||||||
|
sodipodi:docname="power_on.svg"
|
||||||
|
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||||
|
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
xmlns:svg="http://www.w3.org/2000/svg">
|
||||||
|
<sodipodi:namedview
|
||||||
|
id="namedview1"
|
||||||
|
pagecolor="#ffffff"
|
||||||
|
bordercolor="#000000"
|
||||||
|
borderopacity="0.25"
|
||||||
|
inkscape:showpageshadow="2"
|
||||||
|
inkscape:pageopacity="0.0"
|
||||||
|
inkscape:pagecheckerboard="0"
|
||||||
|
inkscape:deskcolor="#d1d1d1"
|
||||||
|
inkscape:document-units="px"
|
||||||
|
inkscape:export-bgcolor="#ffffff00"
|
||||||
|
showgrid="true"
|
||||||
|
inkscape:zoom="2.9023437"
|
||||||
|
inkscape:cx="8.9582774"
|
||||||
|
inkscape:cy="50.476448"
|
||||||
|
inkscape:window-width="1680"
|
||||||
|
inkscape:window-height="987"
|
||||||
|
inkscape:window-x="-8"
|
||||||
|
inkscape:window-y="-8"
|
||||||
|
inkscape:window-maximized="1"
|
||||||
|
inkscape:current-layer="layer1">
|
||||||
|
<inkscape:grid
|
||||||
|
id="grid1"
|
||||||
|
units="px"
|
||||||
|
originx="0"
|
||||||
|
originy="0"
|
||||||
|
spacingx="1"
|
||||||
|
spacingy="1"
|
||||||
|
empcolor="#0099e5"
|
||||||
|
empopacity="0.30196078"
|
||||||
|
color="#0099e5"
|
||||||
|
opacity="0.14901961"
|
||||||
|
empspacing="8"
|
||||||
|
enabled="true"
|
||||||
|
visible="true" />
|
||||||
|
</sodipodi:namedview>
|
||||||
|
<defs
|
||||||
|
id="defs1" />
|
||||||
|
<g
|
||||||
|
inkscape:label="Ebene 1"
|
||||||
|
inkscape:groupmode="layer"
|
||||||
|
id="layer1">
|
||||||
|
<path
|
||||||
|
style="fill:none;stroke:#000000;stroke-width:8;stroke-linecap:round;stroke-linejoin:round"
|
||||||
|
id="path1"
|
||||||
|
sodipodi:type="arc"
|
||||||
|
sodipodi:cx="64"
|
||||||
|
sodipodi:cy="64"
|
||||||
|
sodipodi:rx="40"
|
||||||
|
sodipodi:ry="40"
|
||||||
|
sodipodi:start="5.4977871"
|
||||||
|
sodipodi:end="3.9269908"
|
||||||
|
sodipodi:arc-type="arc"
|
||||||
|
d="M 92.28427,35.715728 A 40,40 0 0 1 100.95518,79.307336 40,40 0 0 1 64,104 40,40 0 0 1 27.044819,79.307338 40,40 0 0 1 35.715728,35.715729"
|
||||||
|
sodipodi:open="true" />
|
||||||
|
<path
|
||||||
|
style="fill:none;stroke:#000000;stroke-width:8;stroke-linecap:round;stroke-linejoin:round"
|
||||||
|
d="M 64,16 V 64"
|
||||||
|
id="path2" />
|
||||||
|
</g>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 2.2 KiB |
42
cpdctrl_gui/resources/led_control.md
Normal file
42
cpdctrl_gui/resources/led_control.md
Normal file
@ -0,0 +1,42 @@
|
|||||||
|
## LED Control
|
||||||
|
To automatically control the LED state during the measurement, you can
|
||||||
|
use a LED script.
|
||||||
|
The LED script is a simple text file with lines in this format:
|
||||||
|
`<duration> <led state>`
|
||||||
|
The script starts at the top, applies `<led state>` for `<duration>` and moves to the next line.
|
||||||
|
|
||||||
|
- `<duration>`: Integer, time in seconds. Alternatively, you may append `s`, `m` or `h`, for seconds, minutes, hours.
|
||||||
|
- `<led state>`: LED brightness in percent. Can be an integer between 0-100 or `on` or `off`.
|
||||||
|
Numbers between 1-99 may only be used if the LED controller supports that (the Arduino/LEDD1B does not).
|
||||||
|
|
||||||
|
Comments may start with `#`. Leading and trailing whitespaces as well as empty lines are ignored.
|
||||||
|
|
||||||
|
### Example
|
||||||
|
```
|
||||||
|
# turn on (100%) for 20 seconds
|
||||||
|
20 on
|
||||||
|
5 off# comments after statements allowed
|
||||||
|
5 100 # whitespace before comments after statements allowed
|
||||||
|
# turn off for 5 seconds
|
||||||
|
5s 0
|
||||||
|
# set to 65% for 1 minute
|
||||||
|
1m 65
|
||||||
|
# turn off for 1 minute and 10 seconds
|
||||||
|
1m10s off
|
||||||
|
# turn on off for 1 hour, 5 minutes and 45 seconds
|
||||||
|
1h5m45s on
|
||||||
|
```
|
||||||
|
|
||||||
|
### Live changes
|
||||||
|
The script can be adapted during the measurement in two ways:
|
||||||
|
First, in the GUI through the table in the `LED Script` tab on the left.
|
||||||
|
Second, by modifying the text file that was loaded (requires "Watch LED Script" checkbox to be ticked).
|
||||||
|
|
||||||
|
You can:
|
||||||
|
- change any LED value
|
||||||
|
- change durations of future steps
|
||||||
|
- add/remove future lines (currently only by modifying the file)
|
||||||
|
|
||||||
|
You can not:
|
||||||
|
- change steps from the past
|
||||||
|
- make the current step so short that it would be skipped
|
25
cpdctrl_gui/resources/measurement_settings.md
Normal file
25
cpdctrl_gui/resources/measurement_settings.md
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
## Measurement settings
|
||||||
|
|
||||||
|
### Measurement Modes
|
||||||
|
In the **normal** measurement mode, the `cpdctrl` asks the voltage measurement device
|
||||||
|
for a new value in regular intervals.
|
||||||
|
This has the advantage that the LED state is always correctly synced with the voltage values,
|
||||||
|
however, the intervals might not be consistent since a software timer is used, which
|
||||||
|
is very inconsistent below 500ms.
|
||||||
|
The signal-to-noise ratio might also not be good, since the device will spend a lot of time not measuring the voltage.
|
||||||
|
|
||||||
|
The **buffer** mode fixes this, by using voltage measurement device's internal data buffer and timing capabilities.
|
||||||
|
This allows for lower and very consistent intervals, but the timing of the LED value might be off
|
||||||
|
by a few tens of milliseconds, since these are still controlled by the lab computer.
|
||||||
|
Since this should not be big problem, **buffer mode should be preferred**.
|
||||||
|
|
||||||
|
### Data Flushing
|
||||||
|
Measurements can be performed over many hours or multiple days.
|
||||||
|
To make sure that unforeseen accidents like Windows Updates do not ruin the entire measurement,
|
||||||
|
the data can be continuously written to the disk.
|
||||||
|
To enable this feature, set `Flush After` to a non-zero value (in the Measurement Settings tab).
|
||||||
|
After \<Flush After\> number of datapoints are recorded, they are written to the cache directory
|
||||||
|
as serialized numpy array.
|
||||||
|
When the measurement is finished, all partial arrays are loaded again and the complete data
|
||||||
|
can be saved as `csv` or serialized numpy array (`.pkl`).
|
||||||
|
|
36
cpdctrl_gui/resources/sample_changing.md
Normal file
36
cpdctrl_gui/resources/sample_changing.md
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
## Changing the sample
|
||||||
|
1. Stop the turbopump:
|
||||||
|
1. Press the stop button on the control panel
|
||||||
|
2. Close the valve between pump and chamber (on top of the turbo pump), by screwing it into the pump
|
||||||
|
2. Ventilate the chamber
|
||||||
|
1. Open the gate valve
|
||||||
|
2. Carefully open the needle valve (usually less than 1/8 turn)
|
||||||
|
3. When the chamber can be moved, close both valves again
|
||||||
|
3. Take off the chamber
|
||||||
|
1. Make some space to put the chamber (mind the grease on the bottom edge)
|
||||||
|
2. Carefully lift the chamber. Good places to grab it are the tubing on the left and the right bottom edge.
|
||||||
|
***Caution***: The tube connecting the chamber with the pump will exert a force to the right!
|
||||||
|
**Make sure you do not crash the chamber into the sample tray!**
|
||||||
|
4. Move the sample away from the gold mesh
|
||||||
|
1. Move the tray fully down by turning the upper micrometer screw *counterclockwise*
|
||||||
|
2. Move the tray fully to the left using the larger micrometer screw
|
||||||
|
3. Loosen the screw on the clamp holding the sample in place and remove the sample
|
||||||
|
5. Prepare the new sample
|
||||||
|
1. Put the sample on the tray and place the clamp on it.
|
||||||
|
2. Tighten the clamp screw (you might need to grab the nut on the bottom side)
|
||||||
|
3. Move the tray in x-position using the large micrometer screw.
|
||||||
|
The gold mesh should be well aligned with the sample surface
|
||||||
|
4. Carefully move the tray up by turning the small micrometer screw *clockwise*.
|
||||||
|
Leave about half a millimeter of space between the sample and the gold mesh
|
||||||
|
**Do not crash the sample into the gold mesh!**
|
||||||
|
6. Put the chamber back on
|
||||||
|
1. Put the chamber back on and align it on the two posts on the bottom and right
|
||||||
|
2. Check the backside to make sure that no cables are sticking out
|
||||||
|
7. Vacuum the chamber
|
||||||
|
1. Make sure the nitrogen venting valves are closed
|
||||||
|
2. **Make sure the turbopump is not spinning anymore**
|
||||||
|
3. When the turbopump has fully stopped, slowly open the valve on top of the pump
|
||||||
|
4. Fully open the valve
|
||||||
|
5. **Wait** until the vacuum is in the lower 1E-2 range, eg 4.0E-2 (read the value from the sensor attached to the turbopump)
|
||||||
|
6. Only when this target pressure is reached, turn on the turbopump (control panel on top shelf)
|
||||||
|
7. Wait until the desired vacuum for measurement is reached (usually 1E-5)
|
5
cpdctrl_gui/resources/small_about.md
Normal file
5
cpdctrl_gui/resources/small_about.md
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
## About
|
||||||
|
|
||||||
|
- Author: Matthias Quintern
|
||||||
|
- License: [GPLv3](https://www.gnu.org/licenses/gpl-3.0.en.html)
|
||||||
|
- Source Code: https://git.quintern.xyz/MatthiasQuintern/cpdctrl-gui
|
29
cpdctrl_gui/resources/technical_information.md
Normal file
29
cpdctrl_gui/resources/technical_information.md
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
## Technical Information
|
||||||
|
### cpdctrl-gui Software
|
||||||
|
`cpdctrl-gui` is written in python and uses the Qt framework for the graphical user interface.
|
||||||
|
Most functionality like interaction with the various devices is provided by the `cpdctrl` package,
|
||||||
|
which can be used without the GUI in an ipython shell for advanced or debugging purposes.
|
||||||
|
|
||||||
|
### Arduino Code
|
||||||
|
To control the ThorLabs LEDD1B, it is connected to an Arduino Nano, which is connected to the lab PC
|
||||||
|
via USB. The Arduino must have the correct software loaded in order to communicate with `cpdctrl` and the LEDD1B.
|
||||||
|
1. Open the Arduino IDE
|
||||||
|
2. Open the sketch `~/cpd-dev/cpdctrl/arduino-thorlabs-led/led_control/led_control.ino`
|
||||||
|
3. Select `COM4` and `Arduino Nano` and then 'Upload Sketch'
|
||||||
|
|
||||||
|
|
||||||
|
### Documentation
|
||||||
|
The documentation files are in the `cpdtrl-gui` directory: `cpdctl-gui/cpdctrl_gui/resources`.
|
||||||
|
**Only edit these files** and then generate the documentation for the Wiki and the PDF manual using
|
||||||
|
the scripts in the repository.
|
||||||
|
|
||||||
|
#### PDF Manual
|
||||||
|
To generate the pdf manual, adapt and run the script found in the cpdctrl-gui repository, in `.scripts/make-manual.py`.
|
||||||
|
It converts the markdown files to html and generates the PDF from that.
|
||||||
|
|
||||||
|
#### Wiki
|
||||||
|
Convert the markdown files to html and directly copy-paste that into the BayernCollab Wiki.
|
||||||
|
1. Adapt and run the script in the cpdctrl-gui repository, in `.scripts/convert_docs_for_website.py`.
|
||||||
|
4. Open the `.html` files in a text editor (Notepad) and copy the content
|
||||||
|
3. While editing a page in collab, click the `</>` button in the top right corner to enable source code editing
|
||||||
|
4. Paste the html in the appropriate place
|
@ -1,6 +1,27 @@
|
|||||||
## Troubleshooting
|
## Troubleshooting
|
||||||
### 1) The LED does not work
|
### 1) General
|
||||||
|
1) Restart the connected devices and then the program
|
||||||
|
2) Check the log file, which might contain helpful information.
|
||||||
|
It is located in the cache directory, usually in `~/.cache/cpdctrl-gui`.
|
||||||
|
3) Ask superuser
|
||||||
|
|
||||||
|
### 2) The Arduino/LEDD1B does not work
|
||||||
1) Make sure the Thorlabs LED is plugged in, turned on (turn the dial to a non-zero value) and that the switch is in the **TRIG**er position
|
1) Make sure the Thorlabs LED is plugged in, turned on (turn the dial to a non-zero value) and that the switch is in the **TRIG**er position
|
||||||
2) Reset the Arduino by pressing the white button on the top and restart the program
|
2) Reset the Arduino by pressing the white button on the top and restart the program
|
||||||
3) Unplug and replug the Arduino and restart the program
|
3) Unplug and replug the Arduino and restart the program
|
||||||
4) Re-upload the code (see section "Arduino Code")
|
4) Re-upload the code (see section "Technical Information/Arduino Code")
|
||||||
|
|
||||||
|
### 3) Pressure does not drop enough
|
||||||
|
Usual values:
|
||||||
|
|
||||||
|
- Before turbo tump:
|
||||||
|
- 1E-3: ~5 minutes
|
||||||
|
- 2E-2: ~25 minutes
|
||||||
|
- With turbo tump:
|
||||||
|
- 1E-4: **TODO**
|
||||||
|
- 1E-5: **TODO**
|
||||||
|
- 1E-6: after 1-2 days
|
||||||
|
|
||||||
|
1) Make sure the housing sits properly on the baseplate, and no cable is sticking out (check the backside!)
|
||||||
|
2) Inspect for leaks using isopropanol
|
||||||
|
3) Perhaps the grease has to be re-applied, ask superuser
|
||||||
|
@ -1,54 +1,10 @@
|
|||||||
# CPD User Guide
|
## Quickstart Guide
|
||||||
## Changing the sample
|
1. Start the `cpdctrl-gui` software (cpd icon)
|
||||||
1. Stop the turbopump:
|
2. Click "Power On" to power on devices connected to the USB Switch
|
||||||
1. Press the stop button on the control panel
|
3. Make sure all devices are turned on
|
||||||
2. Close the valve between pump and chamber (on top of the turbo pump), by screwing it into the pump
|
4. If not connected already, click "CPD devices" and "LED devices" to connect the measurement device and LED controller
|
||||||
2. Ventilate the chamber
|
5. Set the LED to a constant brightness value or load a LED script file
|
||||||
1. Open the gate valve
|
6. Set the measurement name
|
||||||
2. Carefully open the needle valve (usually less than 1/8 turn)
|
7. Click Start
|
||||||
3. When the chamber can be moved, close both valves again
|
8. In the "LED Script" tab, you can see the current LED state during measurement
|
||||||
3. Take off the chamber
|
9. When done, power off the devices
|
||||||
1. Make some space to put the chamber (mind the grease on the bottom edge)
|
|
||||||
2. Carefully lift the chamber. Good places to grab it are the tubing on the left and the right bottom edge.
|
|
||||||
***Caution***: The tube connecting the chamber with the pump will exert a force to the right!
|
|
||||||
**Make sure you do not crash the chamber into the sample tray!**
|
|
||||||
4. Move the sample away from the gold mesh
|
|
||||||
1. Move the tray fully down by turning the upper micrometer screw *counterclockwise*
|
|
||||||
2. Move the tray fully to the left using the larger micrometer screw
|
|
||||||
3. Loosen the screw on the clamp holding the sample in place and remove the sample
|
|
||||||
5. Prepare the new sample
|
|
||||||
1. Put the sample on the tray and place the clamp on it.
|
|
||||||
2. Tighten the clamp screw (you might need to grab the nut on the bottom side)
|
|
||||||
3. Move the tray in x-position using the large micrometer screw.
|
|
||||||
The gold mesh should be well aligned with the sample surface
|
|
||||||
4. Carefully move the tray up by turning the small micrometer screw *clockwise*.
|
|
||||||
Leave about half a millimeter of space between the sample and the gold mesh
|
|
||||||
**Do not crash the sample into the gold mesh!**
|
|
||||||
6. Put the chamber back on
|
|
||||||
1. Put the chamber back on and align it on the two posts on the bottom and right
|
|
||||||
2. Check the backside to make sure that no cables are sticking out
|
|
||||||
7. Vacuum the chamber
|
|
||||||
1. Make sure the nitrogen venting valves are closed
|
|
||||||
2. **Make sure the turbopump is not spinning anymore**
|
|
||||||
3. When the turbopump has fully stopped, very slowly open the valve on top of the pump
|
|
||||||
4. Fully open the valve **TODO: hier noch irgendwelche checks?**
|
|
||||||
5. **Wait** until the vacuum is in the lower $10^{-2}$ range (read the value from the sensor attached to the turbopump)
|
|
||||||
6. Only when this target pressure is reached, turn on the turbopump (control panel on top)
|
|
||||||
7. Wait until the desired vacuum for measurement is reached (usually $10^{-5}$)
|
|
||||||
- $10^{-4}$: **TODO**
|
|
||||||
- $10^{-5}$: **TODO**
|
|
||||||
- $10^{-6}$: after 1-2 days
|
|
||||||
|
|
||||||
If the pressure does not drop enough: Use isopropanol to inspect for leaks
|
|
||||||
|
|
||||||
## CPD Controller Calibration
|
|
||||||
**TODO**
|
|
||||||
|
|
||||||
This process is not always necessary.
|
|
||||||
1. Set CPD Controller to Manual, Offset=Gold Work Function, Filter off (so you can see what happens immediately), Oscillator to ~2
|
|
||||||
2. Move the micrometer screw close until a signal appears
|
|
||||||
3. Set Automatic offset so that the output is 0V
|
|
||||||
Calibration: Fresh graphite probe
|
|
||||||
|
|
||||||
## cpdctrl-gui Software
|
|
||||||
**TODO**
|
|
@ -5,9 +5,10 @@ from PyQt6.QtCore import Qt, QTimer, QFileSystemWatcher
|
|||||||
from PyQt6.QtWidgets import QMainWindow, QWidget, QHBoxLayout, QLabel, QStatusBar, QFileDialog, \
|
from PyQt6.QtWidgets import QMainWindow, QWidget, QHBoxLayout, QLabel, QStatusBar, QFileDialog, \
|
||||||
QVBoxLayout
|
QVBoxLayout
|
||||||
from PyQt6.QtWidgets import QTabWidget
|
from PyQt6.QtWidgets import QTabWidget
|
||||||
from PyQt6.QtGui import QIcon, QPixmap, QAction, QKeySequence, QDragEnterEvent
|
from PyQt6.QtGui import QIcon, QAction, QKeySequence, QDragEnterEvent
|
||||||
from PyQt6.QtWidgets import QDialog, QDialogButtonBox, QMessageBox
|
from PyQt6.QtWidgets import QDialog, QDialogButtonBox, QMessageBox
|
||||||
|
|
||||||
|
from .widgets.help import HelpMenu
|
||||||
from ..resources import get_resource_path
|
from ..resources import get_resource_path
|
||||||
from .widgets.menubar import MenuBar
|
from .widgets.menubar import MenuBar
|
||||||
from .widgets.toolbar import ToolBar
|
from .widgets.toolbar import ToolBar
|
||||||
@ -15,8 +16,9 @@ from .widgets.metadata_input import MetadataInput
|
|||||||
from cpdctrl_gui.ui.widgets.settings import MeasurementSettings, AppSettings
|
from cpdctrl_gui.ui.widgets.settings import MeasurementSettings, AppSettings
|
||||||
from .widgets.plot import Plot
|
from .widgets.plot import Plot
|
||||||
from .widgets.device_select import ListChoice
|
from .widgets.device_select import ListChoice
|
||||||
from .widgets.about import MarkdownView
|
from .widgets.about import About
|
||||||
from .widgets.led_script import LedScriptViewer
|
from .widgets.led_script import LedScriptViewer
|
||||||
|
from .widgets.help import HelpMenu
|
||||||
# from .widgets.treeview import TreeView
|
# from .widgets.treeview import TreeView
|
||||||
|
|
||||||
import time
|
import time
|
||||||
@ -33,10 +35,12 @@ from ..utility.config import AppConfig
|
|||||||
|
|
||||||
import cpdctrl.voltage_measurement_device as vmd
|
import cpdctrl.voltage_measurement_device as vmd
|
||||||
import cpdctrl.led_control_device as ledd
|
import cpdctrl.led_control_device as ledd
|
||||||
|
from cpdctrl.power_switch_device.impl.cleware_switch import ClewareSwitch # TODO: this needs to be refactored at some point
|
||||||
from cpdctrl.led_script import LedScript, InvalidScript
|
from cpdctrl.led_script import LedScript, InvalidScript
|
||||||
from cpdctrl.utility.data import DataCollector
|
from cpdctrl.utility.data import DataCollector
|
||||||
from cpdctrl.measurement import measure
|
from cpdctrl.measurement import measure
|
||||||
|
|
||||||
|
|
||||||
class MainWindow(QMainWindow):
|
class MainWindow(QMainWindow):
|
||||||
"""
|
"""
|
||||||
The main window of the app.
|
The main window of the app.
|
||||||
@ -69,6 +73,7 @@ class MainWindow(QMainWindow):
|
|||||||
self.a_open_about.setIcon(QIcon.fromTheme(get_resource_path("icons/icon.svg")))
|
self.a_open_about.setIcon(QIcon.fromTheme(get_resource_path("icons/icon.svg")))
|
||||||
self.a_open_about.triggered.connect(lambda: self.app_open_about())
|
self.a_open_about.triggered.connect(lambda: self.app_open_about())
|
||||||
|
|
||||||
|
|
||||||
self.create_toolbars()
|
self.create_toolbars()
|
||||||
self.setMenuBar(MenuBar(self)) # must come after toolbars
|
self.setMenuBar(MenuBar(self)) # must come after toolbars
|
||||||
self.setStatusBar(QStatusBar(self))
|
self.setStatusBar(QStatusBar(self))
|
||||||
@ -125,12 +130,19 @@ class MainWindow(QMainWindow):
|
|||||||
self.verbose = True
|
self.verbose = True
|
||||||
|
|
||||||
# devices
|
# devices
|
||||||
|
exe = AppConfig.MAIN_CFG.get_or("power_switch_exe", "")
|
||||||
|
self.power_switch = None
|
||||||
|
if os.path.isfile(exe):
|
||||||
|
self.power_switch = ClewareSwitch(exe)
|
||||||
self.vmdev = None
|
self.vmdev = None
|
||||||
self.leddev = None
|
self.leddev = None
|
||||||
self.vmdev_autoconnect()
|
if AppConfig.MAIN_CFG.get_or("voltage_measurement_device_auto_reconnect", False):
|
||||||
self.leddev_autoconnect()
|
self.vmdev_autoconnect()
|
||||||
|
if AppConfig.MAIN_CFG.get_or("led_device_auto_reconnect", False):
|
||||||
|
self.leddev_autoconnect()
|
||||||
|
|
||||||
# Measurement
|
# Measurement
|
||||||
|
self.idle_timer = None
|
||||||
self.measurement_timer = None
|
self.measurement_timer = None
|
||||||
self.led_script = None
|
self.led_script = None
|
||||||
self.led_script_watcher = None
|
self.led_script_watcher = None
|
||||||
@ -143,6 +155,8 @@ class MainWindow(QMainWindow):
|
|||||||
self.menuBar().m_file.addAction(self.a_open_about)
|
self.menuBar().m_file.addAction(self.a_open_about)
|
||||||
self.menuBar().m_file.addAction(self.a_open_help)
|
self.menuBar().m_file.addAction(self.a_open_help)
|
||||||
|
|
||||||
|
self.idle_start()
|
||||||
|
|
||||||
|
|
||||||
def set_status(self, msg):
|
def set_status(self, msg):
|
||||||
self.statusBar().showMessage(msg)
|
self.statusBar().showMessage(msg)
|
||||||
@ -163,6 +177,9 @@ class MainWindow(QMainWindow):
|
|||||||
self.topbar.add_button("meas_save", "Save", QIcon.fromTheme(QIcon.ThemeIcon.DocumentSaveAs), self.measurement_save)
|
self.topbar.add_button("meas_save", "Save", QIcon.fromTheme(QIcon.ThemeIcon.DocumentSaveAs), self.measurement_save)
|
||||||
self.topbar.add_button("meas_load", "Load", QIcon.fromTheme(QIcon.ThemeIcon.DocumentOpen), self.measurement_load_dialog)
|
self.topbar.add_button("meas_load", "Load", QIcon.fromTheme(QIcon.ThemeIcon.DocumentOpen), self.measurement_load_dialog)
|
||||||
self.topbar.add_separator()
|
self.topbar.add_separator()
|
||||||
|
self.topbar.add_button("power_on", "Power On", get_resource_path("icons/power_on.svg"), self.power_on)
|
||||||
|
self.topbar.add_button("power_off", "Power Off", get_resource_path("icons/power_off.svg"), self.power_off)
|
||||||
|
self.topbar.add_separator()
|
||||||
self.topbar.add_button("app_settings", "Settings", QIcon.fromTheme(QIcon.ThemeIcon.DocumentProperties), self.app_open_settings)
|
self.topbar.add_button("app_settings", "Settings", QIcon.fromTheme(QIcon.ThemeIcon.DocumentProperties), self.app_open_settings)
|
||||||
self.topbar.addAction(self.a_open_help)
|
self.topbar.addAction(self.a_open_help)
|
||||||
self.topbar.addAction(self.a_open_about)
|
self.topbar.addAction(self.a_open_about)
|
||||||
@ -186,27 +203,40 @@ class MainWindow(QMainWindow):
|
|||||||
# return TreeView(self)
|
# return TreeView(self)
|
||||||
|
|
||||||
# LED DEVICE MANAGEMENT
|
# LED DEVICE MANAGEMENT
|
||||||
def leddev_connect(self, leddev_type, leddev_name):
|
def leddev_connect(self, leddev_type, leddev_name, fail_silently=False):
|
||||||
log.info(f"Connecting to LED device {leddev_name} ({leddev_type})")
|
log.info(f"Connecting to LED device {leddev_name} ({leddev_type})")
|
||||||
self.leddev = ledd.connect_device(leddev_type, leddev_name)
|
try:
|
||||||
|
self.leddev = ledd.connect_device(leddev_type, leddev_name)
|
||||||
|
except Exception as e:
|
||||||
|
if fail_silently:
|
||||||
|
raise e
|
||||||
|
else:
|
||||||
|
QMessageBox.critical(self, "Connection failed", f"Failed to connect to '{leddev_name}', the following error occurred: \n{e}")
|
||||||
|
log.error(f"Failed to connect to '{leddev_name}', the following error occured: \n{e}")
|
||||||
|
return
|
||||||
AppConfig.MAIN_CFG.set("led_device_last.type", leddev_type)
|
AppConfig.MAIN_CFG.set("led_device_last.type", leddev_type)
|
||||||
AppConfig.MAIN_CFG.set("led_device_last.name", leddev_name)
|
AppConfig.MAIN_CFG.set("led_device_last.name", leddev_name)
|
||||||
|
self.leddev_connected()
|
||||||
|
|
||||||
|
def leddev_connected(self):
|
||||||
# Update the settings widget value
|
# Update the settings widget value
|
||||||
self.w_measurement_settings.set_value("device_led_controller", str(self.leddev))
|
self.w_measurement_settings.set_value("device_led_controller", self.leddev)
|
||||||
led_name = self.leddev.get_led_name()
|
led_name = None
|
||||||
if not led_name: led_name = "Unknown"
|
if self.leddev is not None:
|
||||||
|
led_name = self.leddev.get_led_name()
|
||||||
|
if not led_name:
|
||||||
|
led_name = "Unknown"
|
||||||
self.w_measurement_settings.set_value("device_led", led_name)
|
self.w_measurement_settings.set_value("device_led", led_name)
|
||||||
|
|
||||||
def leddev_autoconnect(self):
|
def leddev_autoconnect(self):
|
||||||
if AppConfig.MAIN_CFG.get_or("led_device_auto_reconnect", False):
|
try:
|
||||||
try:
|
leddev_type = AppConfig.MAIN_CFG.get("led_device_last.type")
|
||||||
leddev_type = AppConfig.MAIN_CFG.get("led_device_last.type")
|
leddev_name = AppConfig.MAIN_CFG.get("led_device_last.name")
|
||||||
leddev_name = AppConfig.MAIN_CFG.get("led_device_last.name")
|
self.leddev_connect(leddev_type, leddev_name, fail_silently=True)
|
||||||
self.leddev_connect(leddev_type, leddev_name)
|
except KeyError:
|
||||||
except KeyError:
|
pass
|
||||||
pass
|
except Exception as e:
|
||||||
except Exception as e:
|
log.error(f"Failed to auto-connect to LED device: {e}")
|
||||||
log.error(f"Failed to auto-connect to LED device: {e}")
|
|
||||||
|
|
||||||
def leddev_connect_from_dialog(self):
|
def leddev_connect_from_dialog(self):
|
||||||
"""
|
"""
|
||||||
@ -219,25 +249,46 @@ class MainWindow(QMainWindow):
|
|||||||
leddev_type, leddev_name = device_dialog.get_selected()
|
leddev_type, leddev_name = device_dialog.get_selected()
|
||||||
self.leddev_connect(leddev_type, leddev_name)
|
self.leddev_connect(leddev_type, leddev_name)
|
||||||
|
|
||||||
|
def leddev_test_connection(self, silent=False) -> bool:
|
||||||
|
if self.leddev is not None:
|
||||||
|
try:
|
||||||
|
self.leddev.test_connection()
|
||||||
|
return True
|
||||||
|
except ConnectionError as e:
|
||||||
|
if not silent: QMessageBox.warning(self, "LED Controller Disconnected", f"Lost connection to the LED controller '{self.leddev}'")
|
||||||
|
self.leddev = None
|
||||||
|
self.leddev_connected()
|
||||||
|
return False
|
||||||
|
|
||||||
# VOLTAGE DEVICE MANAGEMENT
|
# VOLTAGE DEVICE MANAGEMENT
|
||||||
def vmdev_connect(self, vmdev_type, vmdev_name):
|
def vmdev_connect(self, vmdev_type, vmdev_name, fail_silently=False):
|
||||||
log.info(f"Connecting to voltage measurement device {vmdev_name} ({vmdev_type})")
|
log.info(f"Connecting to voltage measurement device {vmdev_name} ({vmdev_type})")
|
||||||
self.vmdev = vmd.connect_device(vmdev_type, vmdev_name)
|
try:
|
||||||
|
self.vmdev = vmd.connect_device(vmdev_type, vmdev_name)
|
||||||
|
except Exception as e:
|
||||||
|
if fail_silently:
|
||||||
|
raise e
|
||||||
|
else:
|
||||||
|
QMessageBox.critical(self, "Connection failed", f"Failed to connect to '{vmdev_name}', the following error occured: \n{e}")
|
||||||
|
log.error(f"Failed to connect to '{vmdev_name}', the following error occured: \n{e}")
|
||||||
|
return
|
||||||
AppConfig.MAIN_CFG.set("voltage_measurement_device_last.type", vmdev_type)
|
AppConfig.MAIN_CFG.set("voltage_measurement_device_last.type", vmdev_type)
|
||||||
AppConfig.MAIN_CFG.set("voltage_measurement_device_last.name", vmdev_name)
|
AppConfig.MAIN_CFG.set("voltage_measurement_device_last.name", vmdev_name)
|
||||||
|
self.vmdev_connected()
|
||||||
|
|
||||||
|
def vmdev_connected(self):
|
||||||
# Update the settings widget value
|
# Update the settings widget value
|
||||||
self.w_measurement_settings.set_value("device_voltage_measurement", str(self.vmdev))
|
self.w_measurement_settings.set_value("device_voltage_measurement", self.vmdev)
|
||||||
|
|
||||||
def vmdev_autoconnect(self):
|
def vmdev_autoconnect(self):
|
||||||
if AppConfig.MAIN_CFG.get_or("voltage_measurement_device_auto_reconnect", False):
|
try:
|
||||||
try:
|
vmdev_type = AppConfig.MAIN_CFG.get("voltage_measurement_device_last.type")
|
||||||
vmdev_type = AppConfig.MAIN_CFG.get("voltage_measurement_device_last.type")
|
vmdev_name = AppConfig.MAIN_CFG.get("voltage_measurement_device_last.name")
|
||||||
vmdev_name = AppConfig.MAIN_CFG.get("voltage_measurement_device_last.name")
|
self.vmdev_connect(vmdev_type, vmdev_name, fail_silently=True)
|
||||||
self.vmdev_connect(vmdev_type, vmdev_name)
|
except KeyError:
|
||||||
except KeyError:
|
pass
|
||||||
pass
|
except Exception as e:
|
||||||
except Exception as e:
|
log.error(f"Failed to auto-connect to voltage measurement device: {e}")
|
||||||
log.error(f"Failed to auto-connect to voltage measurement device: {e}")
|
|
||||||
def vmdev_connect_from_dialog(self):
|
def vmdev_connect_from_dialog(self):
|
||||||
"""
|
"""
|
||||||
Open a dialog that lets the user choose a voltage measurement device,
|
Open a dialog that lets the user choose a voltage measurement device,
|
||||||
@ -249,17 +300,51 @@ class MainWindow(QMainWindow):
|
|||||||
vmdev_type, vmdev_name = device_dialog.get_selected()
|
vmdev_type, vmdev_name = device_dialog.get_selected()
|
||||||
self.vmdev_connect(vmdev_type, vmdev_name)
|
self.vmdev_connect(vmdev_type, vmdev_name)
|
||||||
|
|
||||||
|
def vmdev_test_connection(self, silent=False) -> bool:
|
||||||
|
if self.vmdev is not None:
|
||||||
|
try:
|
||||||
|
self.vmdev.test_connection()
|
||||||
|
return True
|
||||||
|
except ConnectionError as e:
|
||||||
|
if not silent: QMessageBox.warning(self, "Voltage Measurement Device Disconnected", f"Lost connection to the voltage measurement device '{self.vmdev}'")
|
||||||
|
self.vmdev = None
|
||||||
|
self.vmdev_connected()
|
||||||
|
return False
|
||||||
|
|
||||||
|
# IDLE - NOT IN MEASUREMENT
|
||||||
|
# check if devices stayed connected
|
||||||
|
def idle_start(self):
|
||||||
|
if self.idle_timer is None:
|
||||||
|
self.idle_timer = QTimer(self)
|
||||||
|
self.idle_timer.timeout.connect(self.idle_update)
|
||||||
|
self.idle_timer.start(AppConfig.MAIN_CFG.get_or("idle_update_interval_s", 10)*1000)
|
||||||
|
else:
|
||||||
|
log.debug("Not starting idle timer - it is already running")
|
||||||
|
|
||||||
|
def idle_update(self):
|
||||||
|
self.vmdev_test_connection()
|
||||||
|
self.leddev_test_connection()
|
||||||
|
|
||||||
|
def idle_stop(self):
|
||||||
|
if self.idle_timer is not None:
|
||||||
|
self.idle_timer.stop()
|
||||||
|
self.idle_timer = None
|
||||||
|
else:
|
||||||
|
log.debug("Not stopping idle timer - it is not running")
|
||||||
|
|
||||||
|
|
||||||
# MEASUREMENT
|
# MEASUREMENT
|
||||||
def measure_start(self):
|
def measure_start(self):
|
||||||
if self.vmdev is None:
|
if self.vmdev is None:
|
||||||
self.vmdev_connect_from_dialog()
|
self.vmdev_connect_from_dialog()
|
||||||
if self.vmdev is None:
|
if self.vmdev is None:
|
||||||
raise ValueError("No measurement device selected")
|
raise RuntimeError("No measurement device selected")
|
||||||
|
|
||||||
if self.leddev is None:
|
if self.leddev is None:
|
||||||
self.leddev_connect_from_dialog()
|
self.leddev_connect_from_dialog()
|
||||||
if self.leddev is None:
|
if self.leddev is None:
|
||||||
raise ValueError("No led control device selected")
|
raise RuntimeError("No led control device selected")
|
||||||
|
self.idle_stop()
|
||||||
|
|
||||||
name = self.w_measurement_settings.get_value("name")
|
name = self.w_measurement_settings.get_value("name")
|
||||||
script = self.w_measurement_settings.get_value("led_script")
|
script = self.w_measurement_settings.get_value("led_script")
|
||||||
@ -324,6 +409,7 @@ class MainWindow(QMainWindow):
|
|||||||
self.data_queue,
|
self.data_queue,
|
||||||
auto_add_metadata,
|
auto_add_metadata,
|
||||||
))
|
))
|
||||||
|
# todo: error handling
|
||||||
self.proc_measure.start()
|
self.proc_measure.start()
|
||||||
self.measurement_timer = QTimer(self)
|
self.measurement_timer = QTimer(self)
|
||||||
self.measurement_timer.timeout.connect(self.measure_update)
|
self.measurement_timer.timeout.connect(self.measure_update)
|
||||||
@ -338,9 +424,9 @@ class MainWindow(QMainWindow):
|
|||||||
log.info("Stopping measurement")
|
log.info("Stopping measurement")
|
||||||
if not self.measurement_is_running():
|
if not self.measurement_is_running():
|
||||||
raise RuntimeError("measure_stop: Measurement is not running")
|
raise RuntimeError("measure_stop: Measurement is not running")
|
||||||
|
self.measurement_timer.stop()
|
||||||
self.set_status("Stopping measurement")
|
self.set_status("Stopping measurement")
|
||||||
self.command_queue.put("stop")
|
self.command_queue.put("stop")
|
||||||
self.measurement_timer.stop()
|
|
||||||
self.proc_measure.join()
|
self.proc_measure.join()
|
||||||
self.set_status("Saving data...")
|
self.set_status("Saving data...")
|
||||||
self.data_collector.save_csv_in_dir()
|
self.data_collector.save_csv_in_dir()
|
||||||
@ -371,6 +457,7 @@ class MainWindow(QMainWindow):
|
|||||||
self.w_measurement_settings.setEnabled(True)
|
self.w_measurement_settings.setEnabled(True)
|
||||||
self.w_metadata.setEnabled(True)
|
self.w_metadata.setEnabled(True)
|
||||||
self.set_status("Ready")
|
self.set_status("Ready")
|
||||||
|
self.idle_start()
|
||||||
|
|
||||||
def measure_update(self):
|
def measure_update(self):
|
||||||
self.w_led_script_viewer.update_time(time.time())
|
self.w_led_script_viewer.update_time(time.time())
|
||||||
@ -385,6 +472,11 @@ class MainWindow(QMainWindow):
|
|||||||
self.w_plot.update_plot(tval, vval, led_val)
|
self.w_plot.update_plot(tval, vval, led_val)
|
||||||
else: # measurement might have stopped after max N or script end
|
else: # measurement might have stopped after max N or script end
|
||||||
self.measure_stop()
|
self.measure_stop()
|
||||||
|
# this should only be run when the measurement is stopped automatically
|
||||||
|
# and is therefore not part of measure_stop()
|
||||||
|
if AppConfig.MEAS_CFG.get_or("power_off_script_end", False):
|
||||||
|
self.power_off()
|
||||||
|
QMessageBox.information(self, "Powered Off", "The measurement has stopped, power switch has been turned off.")
|
||||||
|
|
||||||
def measurement_is_running(self):
|
def measurement_is_running(self):
|
||||||
return self.proc_measure is not None
|
return self.proc_measure is not None
|
||||||
@ -519,6 +611,64 @@ class MainWindow(QMainWindow):
|
|||||||
metadata = self.w_metadata.get_dict()
|
metadata = self.w_metadata.get_dict()
|
||||||
self.command_queue.put(("metadata", metadata))
|
self.command_queue.put(("metadata", metadata))
|
||||||
|
|
||||||
|
def power_on(self):
|
||||||
|
"""
|
||||||
|
Send power on command to the power switch.
|
||||||
|
If configured, wait for a while and then try to reconnect to unconnected devices.
|
||||||
|
"""
|
||||||
|
if self.power_switch is None:
|
||||||
|
raise RuntimeError("No power switch configured")
|
||||||
|
log.info("Powering on")
|
||||||
|
self.power_switch.on()
|
||||||
|
# TODO: change to zero
|
||||||
|
timeout = AppConfig.MAIN_CFG.get_or("power_switch_autoconnect_devices_timeout_s", 20)
|
||||||
|
if not timeout > 0: return
|
||||||
|
if self.vmdev and self.leddev: return
|
||||||
|
dialog = QDialog()
|
||||||
|
dialog.setWindowTitle("Autoconnect devices")
|
||||||
|
buttons = QDialogButtonBox(QDialogButtonBox.StandardButton.Cancel)
|
||||||
|
buttons.accepted.connect(dialog.accept)
|
||||||
|
buttons.rejected.connect(dialog.reject)
|
||||||
|
dialog.setLayout(QVBoxLayout())
|
||||||
|
dialog.l_time = QLabel(f"{timeout} s")
|
||||||
|
dialog.time_left = timeout
|
||||||
|
dialog.layout().addWidget(QLabel(f"Trying to reconnect to devices in"))
|
||||||
|
dialog.layout().addWidget(dialog.l_time)
|
||||||
|
dialog.layout().addWidget(buttons)
|
||||||
|
dialog.time_left = timeout
|
||||||
|
def update_time_left(dialog):
|
||||||
|
dialog.time_left -= 1
|
||||||
|
dialog.l_time.setText(f"{dialog.time_left} s")
|
||||||
|
if dialog.time_left <= 0:
|
||||||
|
dialog.accept()
|
||||||
|
dialog.update_time_left = update_time_left
|
||||||
|
dialog.timer = QTimer()
|
||||||
|
dialog.timer.timeout.connect(lambda: dialog.update_time_left(dialog))
|
||||||
|
dialog.timer.start(1000)
|
||||||
|
ret = dialog.exec()
|
||||||
|
dialog.timer.stop()
|
||||||
|
if ret == QDialog.DialogCode.Accepted:
|
||||||
|
log.info("Trying to autoconnect last devices")
|
||||||
|
if self.vmdev is None:
|
||||||
|
self.vmdev_autoconnect()
|
||||||
|
if self.leddev is None:
|
||||||
|
self.leddev_autoconnect()
|
||||||
|
else:
|
||||||
|
pass
|
||||||
|
|
||||||
|
def power_off(self):
|
||||||
|
if self.power_switch is None:
|
||||||
|
raise RuntimeError("No power switch configured")
|
||||||
|
log.info("Powering off")
|
||||||
|
self.idle_stop() # dont show device disconnected errors
|
||||||
|
self.power_switch.off()
|
||||||
|
silent = True
|
||||||
|
time.sleep(0.2)
|
||||||
|
self.leddev_test_connection(silent=silent)
|
||||||
|
self.vmdev_test_connection(silent=silent)
|
||||||
|
self.idle_start()
|
||||||
|
|
||||||
|
|
||||||
def app_exit(self) -> None:
|
def app_exit(self) -> None:
|
||||||
"""
|
"""
|
||||||
Closes the application.
|
Closes the application.
|
||||||
@ -531,17 +681,7 @@ class MainWindow(QMainWindow):
|
|||||||
buttons = QDialogButtonBox(QDialogButtonBox.StandardButton.Ok)
|
buttons = QDialogButtonBox(QDialogButtonBox.StandardButton.Ok)
|
||||||
buttons.accepted.connect(dialog.accept)
|
buttons.accepted.connect(dialog.accept)
|
||||||
dialog.setLayout(QVBoxLayout())
|
dialog.setLayout(QVBoxLayout())
|
||||||
# show the logo via a pixmap in a label
|
dialog.layout().addWidget(About())
|
||||||
img_path = get_resource_path("icons/logo.svg")
|
|
||||||
pixmap = QPixmap(img_path)
|
|
||||||
pixmap = pixmap.scaled(128, 128, Qt.AspectRatioMode.KeepAspectRatio, Qt.TransformationMode.SmoothTransformation)
|
|
||||||
# qt cant find the file
|
|
||||||
label = QLabel()
|
|
||||||
label.setPixmap(pixmap)
|
|
||||||
label.setAlignment(Qt.AlignmentFlag.AlignCenter) # center the image
|
|
||||||
dialog.layout().addWidget(label)
|
|
||||||
# show about.md
|
|
||||||
dialog.layout().addWidget(MarkdownView("about.md"))
|
|
||||||
dialog.layout().addWidget(buttons)
|
dialog.layout().addWidget(buttons)
|
||||||
dialog.exec()
|
dialog.exec()
|
||||||
|
|
||||||
@ -551,9 +691,7 @@ class MainWindow(QMainWindow):
|
|||||||
buttons = QDialogButtonBox(QDialogButtonBox.StandardButton.Ok)
|
buttons = QDialogButtonBox(QDialogButtonBox.StandardButton.Ok)
|
||||||
buttons.accepted.connect(dialog.accept)
|
buttons.accepted.connect(dialog.accept)
|
||||||
dialog.setLayout(QVBoxLayout())
|
dialog.setLayout(QVBoxLayout())
|
||||||
# show help.md
|
dialog.layout().addWidget(HelpMenu())
|
||||||
#dialog.layout().addWidget(MarkdownView("troubleshooting.md"))
|
|
||||||
dialog.layout().addWidget(MarkdownView("user_guide.md"))
|
|
||||||
dialog.layout().addWidget(buttons)
|
dialog.layout().addWidget(buttons)
|
||||||
# set larger window size
|
# set larger window size
|
||||||
dialog.resize(800, 600)
|
dialog.resize(800, 600)
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
from PyQt6.QtWidgets import QTextBrowser
|
from PyQt6.QtWidgets import QTextBrowser, QWidget, QLabel, QVBoxLayout
|
||||||
from PyQt6.QtGui import QDesktopServices
|
from PyQt6.QtCore import Qt
|
||||||
|
from PyQt6.QtGui import QDesktopServices, QPixmap
|
||||||
|
|
||||||
from ...resources import get_resource_path
|
from ...resources import get_resource_path
|
||||||
|
|
||||||
@ -8,19 +9,45 @@ import logging
|
|||||||
log = logging.getLogger(__name__)
|
log = logging.getLogger(__name__)
|
||||||
|
|
||||||
class MarkdownView(QTextBrowser):
|
class MarkdownView(QTextBrowser):
|
||||||
def __init__(self, path):
|
def __init__(self, parent=None):
|
||||||
super().__init__()
|
super().__init__(parent)
|
||||||
self.setReadOnly(True)
|
self.setReadOnly(True)
|
||||||
self.filepath = get_resource_path(path)
|
|
||||||
try:
|
|
||||||
with open(self.filepath, "r") as file:
|
|
||||||
content = file.read()
|
|
||||||
self.setMarkdown(content)
|
|
||||||
except FileNotFoundError:
|
|
||||||
log.error(f"File not found: {self.filepath}")
|
|
||||||
self.setMarkdown(f"## File not found\n`{self.filepath}`")
|
|
||||||
|
|
||||||
# open links with the OS web browser
|
# open links with the OS web browser
|
||||||
self.anchorClicked.connect(QDesktopServices.openUrl)
|
self.anchorClicked.connect(QDesktopServices.openUrl)
|
||||||
# dont follow links
|
# dont follow links
|
||||||
self.setOpenLinks(False)
|
self.setOpenLinks(False)
|
||||||
|
|
||||||
|
class About(QWidget):
|
||||||
|
"""
|
||||||
|
Small about text with logo
|
||||||
|
"""
|
||||||
|
def __init__(self, parent=None):
|
||||||
|
super().__init__(parent)
|
||||||
|
self.setLayout(QVBoxLayout())
|
||||||
|
# show the logo via a pixmap in a label
|
||||||
|
img_path = get_resource_path("icons/logo.svg")
|
||||||
|
pixmap = QPixmap(img_path)
|
||||||
|
pixmap = pixmap.scaled(128, 128, Qt.AspectRatioMode.KeepAspectRatio, Qt.TransformationMode.SmoothTransformation)
|
||||||
|
# qt cant find the file
|
||||||
|
w_label = QLabel()
|
||||||
|
w_label.setPixmap(pixmap)
|
||||||
|
w_label.setAlignment(Qt.AlignmentFlag.AlignCenter) # center the image
|
||||||
|
self.layout().addWidget(w_label)
|
||||||
|
# show about.md
|
||||||
|
w_md_view = MarkdownView()
|
||||||
|
filepath = get_resource_path("small_about.md")
|
||||||
|
try:
|
||||||
|
with open(filepath, "r") as file:
|
||||||
|
content = file.read()
|
||||||
|
try:
|
||||||
|
from importlib.metadata import version
|
||||||
|
cpdversion = version('cpdctrl_gui')
|
||||||
|
content += f"\n- Version: {cpdversion}"
|
||||||
|
except Exception as e:
|
||||||
|
log.info(f"Failed to get cpdctrl_gui version: {e}")
|
||||||
|
# content += f"\n- Version: Unknown"
|
||||||
|
w_md_view.setMarkdown(content)
|
||||||
|
except FileNotFoundError:
|
||||||
|
log.error(f"File not found: {filepath}")
|
||||||
|
w_md_view.setMarkdown(f"## File not found\n`{filepath}`")
|
||||||
|
self.layout().addWidget(w_md_view)
|
||||||
|
49
cpdctrl_gui/ui/widgets/help.py
Normal file
49
cpdctrl_gui/ui/widgets/help.py
Normal file
@ -0,0 +1,49 @@
|
|||||||
|
from PyQt6.QtWidgets import QWidget, QTextBrowser, QHBoxLayout, QListWidget, QListWidgetItem
|
||||||
|
from PyQt6.QtGui import QDesktopServices
|
||||||
|
from ...resources import get_resource_path
|
||||||
|
from .about import MarkdownView
|
||||||
|
|
||||||
|
import logging
|
||||||
|
log = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
# Ordered list of help files to use
|
||||||
|
GUI_FILES = [
|
||||||
|
("about.md", "About"),
|
||||||
|
("sample_changing.md", "Changing the Sample"),
|
||||||
|
("measurement_settings.md", "Measurement Settings"),
|
||||||
|
("led_control.md", "LED Control"),
|
||||||
|
("troubleshooting.md", "Troubleshooting"),
|
||||||
|
("technical_information.md", "Technical Information"),
|
||||||
|
]
|
||||||
|
|
||||||
|
class HelpMenu(QWidget):
|
||||||
|
def __init__(self, parent=None):
|
||||||
|
super().__init__(parent)
|
||||||
|
self.setLayout(QHBoxLayout())
|
||||||
|
self.w_list = QListWidget()
|
||||||
|
self.w_list.setMaximumWidth(200)
|
||||||
|
self.layout().addWidget(self.w_list)
|
||||||
|
self.w_viewer = MarkdownView()
|
||||||
|
self.layout().addWidget(self.w_viewer)
|
||||||
|
files = [(get_resource_path(f), name) for (f,name) in GUI_FILES]
|
||||||
|
for i, (f, name) in enumerate(files):
|
||||||
|
w_item = QListWidgetItem(name)
|
||||||
|
w_item.filepath = f
|
||||||
|
self.w_list.addItem(w_item)
|
||||||
|
if i == 0:
|
||||||
|
self.set_view(w_item)
|
||||||
|
self.w_list.setCurrentRow(0)
|
||||||
|
self.w_list.itemClicked.connect(self.set_view)
|
||||||
|
|
||||||
|
def set_view(self, list_item):
|
||||||
|
try:
|
||||||
|
filepath = list_item.filepath
|
||||||
|
try:
|
||||||
|
with open(filepath, "r") as file:
|
||||||
|
content = file.read()
|
||||||
|
self.w_viewer.setMarkdown(content)
|
||||||
|
except FileNotFoundError:
|
||||||
|
log.error(f"File not found: {filepath}")
|
||||||
|
self.w_viewer.setMarkdown(f"## File not found\n`{filepath}`")
|
||||||
|
except AttributeError:
|
||||||
|
log.error(f"Invalid list item")
|
@ -9,23 +9,50 @@ class AppSettings(QWidget):
|
|||||||
self.setLayout(QVBoxLayout())
|
self.setLayout(QVBoxLayout())
|
||||||
self.w_form = SettingsForm(AppConfig.MAIN_CFG)
|
self.w_form = SettingsForm(AppConfig.MAIN_CFG)
|
||||||
self.layout().addWidget(self.w_form)
|
self.layout().addWidget(self.w_form)
|
||||||
|
self.w_form.add_group("devices", "Devices")
|
||||||
|
self.w_form.add_group("interface", "Interface")
|
||||||
|
self.w_form.add_group("led_script", "LED Script")
|
||||||
|
self.w_form.add_group("power_switch", "Power Switch")
|
||||||
|
|
||||||
w_cache_dir = FileSelection(filemode=QFileDialog.FileMode.Directory)
|
w_cache_dir = FileSelection(filemode=QFileDialog.FileMode.Directory)
|
||||||
self.w_form.add_form_row("dir_cache", "Cache Directory", "~/.cache/cpdctrl", w_cache_dir, "Directory to store temporary data")
|
self.w_form.add_form_row("dir_cache", "Cache Directory", "~/.cache/cpdctrl", w_cache_dir, "Directory to store temporary data")
|
||||||
self.w_form.add_form_row("led_device_auto_reconnect", "Autoconnect to last LED Controller", True, QCheckBox(), "Automatically connect to the last used LED Controller")
|
# devices
|
||||||
self.w_form.add_form_row("voltage_measurement_device_auto_reconnect", "Autoconnect to last Voltage Measurement Device", True, QCheckBox(), "Automatically connect to the last used Voltage Measurement Device")
|
self.w_form.add_form_row("led_device_auto_reconnect", "Autoconnect to last LED Controller", True, QCheckBox(), "Automatically connect to the last used LED Controller", group="devices")
|
||||||
|
self.w_form.add_form_row("voltage_measurement_device_auto_reconnect", "Autoconnect to last Voltage Measurement Device", True, QCheckBox(), "Automatically connect to the last used Voltage Measurement Device", group="devices")
|
||||||
|
w_idle_dt = QSpinBox()
|
||||||
|
w_idle_dt.setMinimum(10)
|
||||||
|
w_idle_dt.setMaximum(200000)
|
||||||
|
w_idle_dt.setSingleStep(10)
|
||||||
|
self.w_form.add_form_row("idle_update_interval_s", "Device Connection Check Interval (s)", 30, w_idle_dt, "How often to check whether the devices are still connected.\nApplies only when not in a measurement.", group="devices")
|
||||||
|
|
||||||
|
# interface
|
||||||
w_plot_n = QSpinBox()
|
w_plot_n = QSpinBox()
|
||||||
w_plot_n.setMinimum(1000)
|
w_plot_n.setMinimum(1000)
|
||||||
w_plot_n.setMaximum(200000)
|
w_plot_n.setMaximum(200000)
|
||||||
w_plot_n.setSingleStep(1000)
|
w_plot_n.setSingleStep(1000)
|
||||||
self.w_form.add_form_row("plot_max_data_points", "Max. Datapoints in the Plot", 20000, w_plot_n, "Maximum number of datapoints in the live plot.\nThis value is limited to ensure performance is not degraded in long measurements")
|
self.w_form.add_form_row("plot_max_data_points", "Max. Datapoints in the Plot", 20000, w_plot_n, "Maximum number of datapoints in the live plot.\nThis value is limited to ensure performance is not degraded in long measurements", group="interface")
|
||||||
w_plot_dt = QSpinBox()
|
w_plot_dt = QSpinBox()
|
||||||
w_plot_dt.setMinimum(10)
|
w_plot_dt.setMinimum(10)
|
||||||
w_plot_dt.setMaximum(200000)
|
w_plot_dt.setMaximum(200000)
|
||||||
w_plot_dt.setSingleStep(100)
|
w_plot_dt.setSingleStep(100)
|
||||||
self.w_form.add_form_row("plot_update_interval_ms", "Plot Update Interval (ms)", 200, w_plot_dt, "Number of milliseconds to wait before updating the live plot")
|
self.w_form.add_form_row("plot_update_interval_ms", "Plot Update Interval (ms)", 200, w_plot_dt, "Number of milliseconds to wait before updating the live plot", group="interface")
|
||||||
self.w_form.add_form_row("led_script_watch_file", "Watch Led Script File", False, QCheckBox(), "Watch the LED script file for changes and reload it automatically")
|
self.w_form.add_form_row("error_show_dialog", "Show Dialog on Error", True, QCheckBox(), "Whether to show a dialog window when uncaught errors occur\nLeave this on.", group="interface")
|
||||||
self.w_form.add_form_row("led_script_save_copy", "Copy Led Script to Cache Dir", True, QCheckBox(), "Save the final version of the led script in the cache directory")
|
self.w_form.add_form_row("error_stack_trace", "Stacktrace in errors", False, QCheckBox(), "Whether to show the call stack in unexpected error messages", group="interface")
|
||||||
|
|
||||||
|
# led_script
|
||||||
|
self.w_form.add_form_row("led_script_watch_file", "Watch Led Script File", False, QCheckBox(), "Watch the LED script file for changes and reload it automatically", group="led_script")
|
||||||
|
self.w_form.add_form_row("led_script_save_copy", "Copy Led Script to Cache Dir", True, QCheckBox(), "Save the final version of the led script in the cache directory", group="led_script")
|
||||||
|
|
||||||
self.w_form.add_form_row("metadata_auto_update", "Auto-Update Metadata", True, QCheckBox(), "Allow metadata updates while the program is running")
|
self.w_form.add_form_row("metadata_auto_update", "Auto-Update Metadata", True, QCheckBox(), "Allow metadata updates while the program is running")
|
||||||
self.w_form.add_form_row("metadata_auto_add", "Auto-Add Metadata", True, QCheckBox(), "Automatically add measurement metadata to the data file.\nThis includes: device names, measurement mode, measurement interval, start and stop times, led script")
|
self.w_form.add_form_row("metadata_auto_add", "Auto-Add Metadata", True, QCheckBox(), "Automatically add measurement metadata to the data file.\nThis includes: device names, measurement mode, measurement interval, start and stop times, led script")
|
||||||
|
|
||||||
|
# power_switch
|
||||||
|
w_usb_switch_exe = FileSelection(filemode=QFileDialog.FileMode.ExistingFile)
|
||||||
|
self.w_form.add_form_row("power_switch_exe", "Power Switch Executable", "", w_usb_switch_exe, "Path to the USBSwitchCmd executable for the Cleware USB switch\nRequires a relaunch to take effect", group="power_switch")
|
||||||
|
w_power_on_reconnect_dt = QSpinBox()
|
||||||
|
w_power_on_reconnect_dt.setMinimum(0)
|
||||||
|
w_power_on_reconnect_dt.setMaximum(60)
|
||||||
|
w_power_on_reconnect_dt.setSingleStep(1)
|
||||||
|
self.w_form.add_form_row("power_switch_autoconnect_devices_timeout_s", "Autoconnect devices after (s)", 15, w_power_on_reconnect_dt, "After switching on, wait x seconds before trying to reconnect to the last devices\nSet to 0 to disable the auto-connection attempt.", group="power_switch")
|
||||||
|
|
||||||
|
self.w_form.update_alignment()
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
from PyQt6.QtGui import QIcon
|
from PyQt6.QtGui import QIcon
|
||||||
from PyQt6.QtWidgets import QWidget, QLabel, QFormLayout, QSpinBox, QDoubleSpinBox, QLineEdit, QHBoxLayout, QPushButton, QFileDialog, QCheckBox
|
from PyQt6.QtWidgets import QWidget, QLabel, QFormLayout, QSpinBox, QDoubleSpinBox, QLineEdit, QHBoxLayout, QPushButton, \
|
||||||
from PyQt6.QtCore import pyqtSignal
|
QFileDialog, QCheckBox, QVBoxLayout, QGroupBox
|
||||||
|
from PyQt6.QtCore import pyqtSignal, Qt
|
||||||
from cpdctrl.utility.config_file import ConfigFile
|
from cpdctrl.utility.config_file import ConfigFile
|
||||||
|
|
||||||
|
|
||||||
@ -47,14 +48,35 @@ class SettingsForm(QWidget):
|
|||||||
"""
|
"""
|
||||||
def __init__(self, config_file: ConfigFile, parent=None):
|
def __init__(self, config_file: ConfigFile, parent=None):
|
||||||
super().__init__(parent)
|
super().__init__(parent)
|
||||||
self.setLayout(QFormLayout())
|
self.setLayout(QVBoxLayout())
|
||||||
self.ws_form = {}
|
self.ws_form: dict[str, QWidget] = {}
|
||||||
|
self.ls_form: dict[str, QLabel] = {}
|
||||||
|
self.ws_groups: dict[str, QFormLayout] = {}
|
||||||
self.config_file = config_file
|
self.config_file = config_file
|
||||||
|
w_default = QWidget()
|
||||||
|
w_default.setLayout(QFormLayout())
|
||||||
|
self.ws_groups["default"] = w_default.layout()
|
||||||
|
self.layout().addWidget(w_default)
|
||||||
|
|
||||||
def __contains__(self, item):
|
def __contains__(self, item):
|
||||||
return item in self.ws_form
|
return item in self.ws_form
|
||||||
|
|
||||||
def add_form_row(self, key: str, label: str, default_value, widget: QWidget, tooltip: str = None):
|
def add_group(self, key, name):
|
||||||
|
"""
|
||||||
|
Add a group box
|
||||||
|
Parameters
|
||||||
|
----------
|
||||||
|
key
|
||||||
|
The key to be used with add_form_row
|
||||||
|
name
|
||||||
|
The name to display
|
||||||
|
"""
|
||||||
|
w_group = QGroupBox(parent=None, title=name)
|
||||||
|
w_group.setLayout(QFormLayout())
|
||||||
|
self.layout().addWidget(w_group)
|
||||||
|
self.ws_groups[key] = w_group.layout()
|
||||||
|
|
||||||
|
def add_form_row(self, key: str, label: str, default_value, widget: QWidget, tooltip: str = None, group:str="default"):
|
||||||
"""
|
"""
|
||||||
Add a row to the form. Uses the value from the config file corresponding to <key> or the default value.
|
Add a row to the form. Uses the value from the config file corresponding to <key> or the default value.
|
||||||
Parameters
|
Parameters
|
||||||
@ -62,15 +84,21 @@ class SettingsForm(QWidget):
|
|||||||
key
|
key
|
||||||
label: str
|
label: str
|
||||||
Label for the form widget
|
Label for the form widget
|
||||||
default_value
|
default_value:
|
||||||
widget
|
The default value to use for the widget
|
||||||
|
widget: QWidget
|
||||||
Widget to add to the form
|
Widget to add to the form
|
||||||
tooltip
|
tooltip: str
|
||||||
|
Tooltip for the widget
|
||||||
|
group: str
|
||||||
|
Group to add the row to.
|
||||||
|
|
||||||
Returns
|
Returns
|
||||||
-------
|
-------
|
||||||
|
|
||||||
"""
|
"""
|
||||||
|
if not group in self.ws_groups:
|
||||||
|
raise ValueError(f"Can not add form row '{key}': Group '{group}' does not exist.")
|
||||||
if tooltip: widget.setToolTip(tooltip)
|
if tooltip: widget.setToolTip(tooltip)
|
||||||
value = self.config_file.get_or(key, default_value)
|
value = self.config_file.get_or(key, default_value)
|
||||||
# set the value depending on the type of the widget
|
# set the value depending on the type of the widget
|
||||||
@ -88,8 +116,10 @@ class SettingsForm(QWidget):
|
|||||||
widget.textChanged.connect(lambda v: self.value_updated(key, v))
|
widget.textChanged.connect(lambda v: self.value_updated(key, v))
|
||||||
else:
|
else:
|
||||||
raise ValueError(f"Unknown widget type: {type(widget)}")
|
raise ValueError(f"Unknown widget type: {type(widget)}")
|
||||||
self.layout().addRow(QLabel(label), widget)
|
l_label = QLabel(label)
|
||||||
|
self.ws_groups[group].addRow(l_label, widget)
|
||||||
self.ws_form[key] = widget
|
self.ws_form[key] = widget
|
||||||
|
self.ls_form[key] = l_label
|
||||||
|
|
||||||
|
|
||||||
def value_updated(self, key, value):
|
def value_updated(self, key, value):
|
||||||
@ -152,3 +182,18 @@ class SettingsForm(QWidget):
|
|||||||
raise ValueError(f"Unknown widget type: {type(self.ws_form[key])}")
|
raise ValueError(f"Unknown widget type: {type(self.ws_form[key])}")
|
||||||
else:
|
else:
|
||||||
raise ValueError(f"Unknown key: {key}")
|
raise ValueError(f"Unknown key: {key}")
|
||||||
|
|
||||||
|
def update_alignment(self):
|
||||||
|
"""
|
||||||
|
Give all labels the min width of the widest label,
|
||||||
|
so that all forms are aligned even when in different group boxes
|
||||||
|
"""
|
||||||
|
max_width = 0
|
||||||
|
for _, l in self.ls_form.items():
|
||||||
|
w = l.minimumSizeHint().width()
|
||||||
|
if w > max_width:
|
||||||
|
max_width = w
|
||||||
|
for _, l in self.ls_form.items():
|
||||||
|
l.setMinimumWidth(max_width)
|
||||||
|
|
||||||
|
|
||||||
|
@ -1,9 +1,12 @@
|
|||||||
|
import os.path
|
||||||
|
|
||||||
from PyQt6.QtCore import pyqtSignal
|
from PyQt6.QtCore import pyqtSignal
|
||||||
from PyQt6.QtWidgets import QWidget, QRadioButton, QVBoxLayout, QHBoxLayout, QPushButton, QSpinBox, QFileDialog, QLabel
|
from PyQt6.QtWidgets import QWidget, QRadioButton, QVBoxLayout, QHBoxLayout, QPushButton, QSpinBox, QFileDialog, QLabel
|
||||||
from PyQt6.QtWidgets import QFormLayout, QDoubleSpinBox, QCheckBox, QLineEdit, QGroupBox
|
from PyQt6.QtWidgets import QFormLayout, QDoubleSpinBox, QCheckBox, QLineEdit, QGroupBox
|
||||||
|
|
||||||
from os import path
|
from os import path
|
||||||
|
|
||||||
|
from cpdctrl.led_script import LedScript, InvalidScript
|
||||||
from cpdctrl_gui.utility.config import AppConfig
|
from cpdctrl_gui.utility.config import AppConfig
|
||||||
from .base import SettingsForm
|
from .base import SettingsForm
|
||||||
|
|
||||||
@ -28,7 +31,13 @@ class DeviceSelection(QGroupBox):
|
|||||||
def set_value(self, key, value):
|
def set_value(self, key, value):
|
||||||
key = key.replace("device_", "")
|
key = key.replace("device_", "")
|
||||||
if key in self.devices_widgets:
|
if key in self.devices_widgets:
|
||||||
self.devices_widgets[key].setText(value)
|
if value is None:
|
||||||
|
text = "N.C."
|
||||||
|
elif type(value) != str:
|
||||||
|
text = str(value)
|
||||||
|
else:
|
||||||
|
text = value
|
||||||
|
self.devices_widgets[key].setText(text)
|
||||||
else:
|
else:
|
||||||
raise KeyError(f"Unknown device '{key}'")
|
raise KeyError(f"Unknown device '{key}'")
|
||||||
|
|
||||||
@ -41,7 +50,7 @@ class ScriptSelection(QGroupBox):
|
|||||||
|
|
||||||
# Radio buttons
|
# Radio buttons
|
||||||
self.radio_script_file = QRadioButton("Script file")
|
self.radio_script_file = QRadioButton("Script file")
|
||||||
self.radio_constant_value = QRadioButton("Constant value")
|
self.radio_constant_value = QRadioButton("Constant brightness (%)")
|
||||||
self.radio_script_file.toggled.connect(self.on_radio_button_toggled)
|
self.radio_script_file.toggled.connect(self.on_radio_button_toggled)
|
||||||
self.radio_constant_value.toggled.connect(self.on_radio_button_toggled)
|
self.radio_constant_value.toggled.connect(self.on_radio_button_toggled)
|
||||||
|
|
||||||
@ -50,6 +59,15 @@ class ScriptSelection(QGroupBox):
|
|||||||
self.btn_load_script.clicked.connect(self.load_file)
|
self.btn_load_script.clicked.connect(self.load_file)
|
||||||
self.w_script_file = QLineEdit()
|
self.w_script_file = QLineEdit()
|
||||||
self.w_script_file.setEnabled(False)
|
self.w_script_file.setEnabled(False)
|
||||||
|
last_file = AppConfig.MAIN_CFG.get_or("tmp_last_script_file", "")
|
||||||
|
# only use last file if exists and is valid
|
||||||
|
if os.path.isfile(last_file):
|
||||||
|
try:
|
||||||
|
LedScript.parse_script(last_file)
|
||||||
|
except InvalidScript:
|
||||||
|
last_file = ""
|
||||||
|
self.w_script_file.setText(last_file)
|
||||||
|
self.file_path = last_file
|
||||||
|
|
||||||
# QSpinBox for constant value
|
# QSpinBox for constant value
|
||||||
self.w_constant_value = QSpinBox()
|
self.w_constant_value = QSpinBox()
|
||||||
@ -78,12 +96,14 @@ class ScriptSelection(QGroupBox):
|
|||||||
self.on_radio_button_toggled()
|
self.on_radio_button_toggled()
|
||||||
|
|
||||||
self.layout.addStretch(1)
|
self.layout.addStretch(1)
|
||||||
self.file_path = None
|
|
||||||
|
|
||||||
def on_radio_button_toggled(self):
|
def on_radio_button_toggled(self):
|
||||||
if self.radio_script_file.isChecked():
|
if self.radio_script_file.isChecked():
|
||||||
self.btn_load_script.setEnabled(True)
|
self.btn_load_script.setEnabled(True)
|
||||||
self.w_constant_value.setEnabled(False)
|
self.w_constant_value.setEnabled(False)
|
||||||
|
# if a file is set, load it when the button is toggled
|
||||||
|
if self.file_path:
|
||||||
|
self.loadScript.emit()
|
||||||
else:
|
else:
|
||||||
self.btn_load_script.setEnabled(False)
|
self.btn_load_script.setEnabled(False)
|
||||||
self.w_constant_value.setEnabled(True)
|
self.w_constant_value.setEnabled(True)
|
||||||
@ -95,6 +115,7 @@ class ScriptSelection(QGroupBox):
|
|||||||
if file_path:
|
if file_path:
|
||||||
dir_name = path.dirname(file_path)
|
dir_name = path.dirname(file_path)
|
||||||
AppConfig.MAIN_CFG.set("tmp_last_script_dir", dir_name)
|
AppConfig.MAIN_CFG.set("tmp_last_script_dir", dir_name)
|
||||||
|
AppConfig.MAIN_CFG.set("tmp_last_script_file", file_path)
|
||||||
self.file_path = file_path
|
self.file_path = file_path
|
||||||
self.w_script_file.setText(self.file_path)
|
self.w_script_file.setText(self.file_path)
|
||||||
# signal the change
|
# signal the change
|
||||||
@ -144,8 +165,15 @@ class MeasurementSettings(QWidget):
|
|||||||
w_box_max_measurements = QSpinBox(self)
|
w_box_max_measurements = QSpinBox(self)
|
||||||
w_box_max_measurements.setMaximum(2147483647) # max int32
|
w_box_max_measurements.setMaximum(2147483647) # max int32
|
||||||
w_box_max_measurements.setMinimum(0) # 0 for infinite measurements
|
w_box_max_measurements.setMinimum(0) # 0 for infinite measurements
|
||||||
self.w_form.add_form_row("max_measurements", "Max Measurements", 0, w_box_max_measurements, "Number of measurements to take. Set to 0 for infinite measurements")
|
self.w_form.add_form_row("max_measurements", "Max Measurement Points", 0, w_box_max_measurements, "Number of measurements to take. Set to 0 for infinite measurements")
|
||||||
self.w_form.add_form_row("stop_on_script_end", "Stop on Script End", False, QCheckBox(self), "Stop measurement when LED script ends")
|
w_stop_script_end = QCheckBox(self)
|
||||||
|
self.w_form.add_form_row("stop_on_script_end", "Stop on Script End", False, w_stop_script_end, "Stop measurement when LED script ends")
|
||||||
|
self.w_poweroff_script_end = QCheckBox(self)
|
||||||
|
# do after adding to form, since the actual value will have been loaded
|
||||||
|
w_stop_script_end.stateChanged.connect(self._update_poweroff_checkbox)
|
||||||
|
w_box_max_measurements.valueChanged.connect(self._update_poweroff_checkbox)
|
||||||
|
self._update_poweroff_checkbox()
|
||||||
|
self.w_form.add_form_row("power_off_script_end", "Power Off on Script End", False, self.w_poweroff_script_end, "Turn off the USB power switch when the measurement ends")
|
||||||
self.w_form.add_form_row("use_buffer", "Use Buffer", False, QCheckBox(self), "If available, use the voltage device buffer for more accurate measurement timings.\nLeads to a lower accuracy of LED update timings, up to 1*interval")
|
self.w_form.add_form_row("use_buffer", "Use Buffer", False, QCheckBox(self), "If available, use the voltage device buffer for more accurate measurement timings.\nLeads to a lower accuracy of LED update timings, up to 1*interval")
|
||||||
w_box_flush_after = QSpinBox(self)
|
w_box_flush_after = QSpinBox(self)
|
||||||
w_box_flush_after.setMaximum(2147483647) # max int32
|
w_box_flush_after.setMaximum(2147483647) # max int32
|
||||||
@ -153,6 +181,18 @@ class MeasurementSettings(QWidget):
|
|||||||
self.w_form.add_form_row("flush_after", "Flush-Data Interval", 0, w_box_flush_after, "Number of measurements to take before writing the data to an intermediate file")
|
self.w_form.add_form_row("flush_after", "Flush-Data Interval", 0, w_box_flush_after, "Number of measurements to take before writing the data to an intermediate file")
|
||||||
self.l_vbox.addStretch(1)
|
self.l_vbox.addStretch(1)
|
||||||
|
|
||||||
|
def _update_poweroff_checkbox(self):
|
||||||
|
"""
|
||||||
|
Enable the power-off-on-end checkbox only if the measurement
|
||||||
|
will stopped automatically
|
||||||
|
"""
|
||||||
|
max_meas = self.w_form.get_value("max_measurements")
|
||||||
|
stop_script_end = self.w_form.get_value("stop_on_script_end")
|
||||||
|
if max_meas > 0 or stop_script_end == True:
|
||||||
|
self.w_poweroff_script_end.setEnabled(True)
|
||||||
|
else:
|
||||||
|
self.w_poweroff_script_end.setEnabled(False)
|
||||||
|
|
||||||
def set_value(self, key, value):
|
def set_value(self, key, value):
|
||||||
if key in self.w_form:
|
if key in self.w_form:
|
||||||
self.w_form.set_value(key, value)
|
self.w_form.set_value(key, value)
|
||||||
|
@ -2,9 +2,9 @@
|
|||||||
requires = ["setuptools"]
|
requires = ["setuptools"]
|
||||||
|
|
||||||
[project]
|
[project]
|
||||||
name = "cpdctrl-gui"
|
name = "cpdctrl_gui"
|
||||||
version = "0.1.0"
|
version = "1.0.0"
|
||||||
description = "GUI Utility for CPD measurements with a Keitley 2700 SMU and an Arduino-controlled light source"
|
description = "GUI Utility for CPD measurements with a Keitley 2700 SMU and a controlled light source"
|
||||||
requires-python = ">=3.10"
|
requires-python = ">=3.10"
|
||||||
readme = "README.md"
|
readme = "README.md"
|
||||||
license = {file = "LICENSE"}
|
license = {file = "LICENSE"}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user