Commit 7b2e6e96 authored by Karol Hennessy's avatar Karol Hennessy

Merge branch 'test_UI_new_state_machines_merge' into 'master'

Test ui new state machines merge

See merge request !13
parents ff1f0719 05db8768
Pipeline #1707 canceled with stages
......@@ -11,3 +11,161 @@ __pycache__
*.sqlite
*.csv
raspberry-dataserver/foo
env/
.cache
.pylintrc
.pre-commit-config.yaml
.env
ansible/playbooks/hosts
.idea
scratch_*
*/*/startup_config.json
NativeUI/configs/startup_config.json
# python virtual env created using ansible
.hev_env
# Created by https://www.toptal.com/developers/gitignore/api/python
# Edit at https://www.toptal.com/developers/gitignore?templates=python
### Python ###
# Byte-compiled / optimized / DLL files
__pycache__/
*.py[cod]
*$py.class
# C extensions
*.so
# Distribution / packaging
.Python
build/
develop-eggs/
dist/
downloads/
eggs/
.eggs/
lib/
lib64/
parts/
sdist/
var/
wheels/
pip-wheel-metadata/
share/python-wheels/
*.egg-info/
.installed.cfg
*.egg
MANIFEST
# PyInstaller
# Usually these files are written by a python script from a template
# before PyInstaller builds the exe, so as to inject date/other infos into it.
*.manifest
*.spec
# Installer logs
pip-log.txt
pip-delete-this-directory.txt
# Unit test / coverage reports
htmlcov/
.tox/
.nox/
.coverage
.coverage.*
.cache
nosetests.xml
coverage.xml
*.cover
*.py,cover
.hypothesis/
.pytest_cache/
pytestdebug.log
# Translations
*.mo
*.pot
# Django stuff:
*.log
local_settings.py
db.sqlite3
db.sqlite3-journal
# Flask stuff:
instance/
.webassets-cache
# Scrapy stuff:
.scrapy
# Sphinx documentation
docs/_build/
doc/_build/
# PyBuilder
target/
# Jupyter Notebook
.ipynb_checkpoints
# IPython
profile_default/
ipython_config.py
# pyenv
.python-version
# pipenv
# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
# However, in case of collaboration, if having platform-specific dependencies or dependencies
# having no cross-platform support, pipenv may install dependencies that don't work, or not
# install all needed dependencies.
#Pipfile.lock
# PEP 582; used by e.g. github.com/David-OConnor/pyflow
__pypackages__/
# Celery stuff
celerybeat-schedule
celerybeat.pid
# SageMath parsed files
*.sage.py
# Environments
.env
.venv
env/
venv/
ENV/
env.bak/
venv.bak/
pythonenv*
# Spyder project settings
.spyderproject
.spyproject
# Rope project settings
.ropeproject
# mkdocs documentation
/site
# mypy
.mypy_cache/
.dmypy.json
dmypy.json
# Pyre type checker
.pyre/
# pytype static type analyzer
.pytype/
# profiling data
.prof
# End of https://www.toptal.com/developers/gitignore/api/python
stages:
- build
- test
build:raspi4-qmake:
before_script:
- pwd
- echo 'test'
- groupadd pi
- useradd -u 1000 -g pi -m -p raspberry pi
- usermod -aG sudo pi
- mkdir -p /home/pi/Downloads
- apt -y autoremove
- apt -y update
- apt -y upgrade
- apt -y install python3.7 software-properties-common python3-pip git-all raspi-config
- python3 --version
- pip3 install ansible
- ansible --version
# install_packages:
# stage: install
# script:
# - apt -y autoremove
# - apt -y update
# - apt -y upgrade
# - apt -y install python3.7 software-properties-common python3-pip git-all raspi-config
# - python3 --version
# - pip3 install ansible
# - ansible --version
ui_installation:
stage: build
image: etalian/qt-raspi4
before_script:
- mkdir -p "${CI_PROJECT_DIR}/binaries"
script:
- cd "${CI_PROJECT_DIR}/hev-display"
- /raspi/qt5/bin/qmake
- make
- mkdir /tmp/${CI_PROJECT_NAME} && cd "$_"
- cmake --config Release
-DCMAKE_TOOLCHAIN_FILE=/raspi/gcc-linaro-arm-linux-gnueabihf-raspbian-x64.cmake
-DCPACK_PACKAGE_SUFFIX=-pi4 -DCPACK_SYSTEM_NAME=raspbian10 -DCPACK_DEBIAN_PACKAGE_ARCHITECTURE=armhf
"${CI_PROJECT_DIR}/hev-display"
- make
- cpack -G DEB && cp -v *.deb "${CI_PROJECT_DIR}/binaries"
artifacts:
name: "${CI_JOB_NAME}-${CI_COMMIT_REF_SLUG}~git${CI_COMMIT_SHORT_SHA}"
paths:
- hev-display
- binaries/
- ls -a
- pwd
- cd /home/pi/hev
- ./setup.sh CI
ui_test:
stage: test
script:
- echo "Tests will be here."
\ No newline at end of file
# See https://pre-commit.com for more information
# See https://pre-commit.com/hooks.html for more hooks
exclude: ^[\S]*{{cookiecutter[\S]*
repos:
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v3.1.0
hooks:
- id: trailing-whitespace
- id: end-of-file-fixer
- id: check-yaml
- id: check-added-large-files
- id: check-json
# - id: pretty-format-json
- id: requirements-txt-fixer
- repo: https://github.com/psf/black
rev: 19.3b0
hooks:
- id: black
language_version: python3.7
# - repo: https://github.com/timothycrosley/isort
# rev: 5.0.8
# hooks:
# - id: isort
- repo: https://github.com/pycqa/pylint
rev: pylint-2.6.0
hooks:
- id: pylint
args:
- --rcfile=pylint.cfg
#!/usr/bin/env python3
import logging
"""
NativeUI.py
Command-line arguments:
-d, --debug : set the level of debug output.Include once for INFO, twice for DEBUG
-w, --windowed : run the user interface in windowed mode.
-r, --resolution : set the window size in pixels. E.g. -r 1920x1080
--no-startup : start the UI without going through the calibration startup sequence.
-l, --language : set the initial language for the UI (can later be set within the
interface). Defaults to English.
"""
__author__ = ["Benjamin Mummery", "Dónal Murray", "Tiago Sarmento"]
__credits__ = ["Benjamin Mummery", "Dónal Murray", "Tim Powell", "Tiago Sarmento"]
__license__ = "GPL"
__version__ = "0.1.2"
__maintainer__ = "Benjamin Mummery"
__email__ = "benjamin.mummery@stfc.ac.uk"
__status__ = "Prototype"
import argparse
import json
import logging
import os
import re
import sys
import numpy as np
from PySide2.QtCore import Slot
from PySide2.QtWidgets import QMainWindow, QApplication, QHBoxLayout, QVBoxLayout
from threading import Lock
import git
from global_widgets.global_send_popup import confirmPopup, SetConfirmPopup
from widget_library.ok_cancel_buttons_widget import (
OkButtonWidget,
OkSendButtonWidget,
CancelButtonWidget,
)
from hevclient import HEVClient
from hev_main import MainView
from PySide2.QtCore import Slot, QTimer, Qt
from PySide2.QtGui import QColor, QFont, QPalette
from ui_layout import Layout
from ui_widgets import Widgets
from PySide2.QtWidgets import (
QApplication,
QMainWindow,
QWidget,
QDialog,
QStackedWidget,
)
# from handler_library.alarm_handler import AlarmHandler
from handler_library.battery_handler import BatteryHandler
from handler_library.data_handler import DataHandler
from handler_library.measurement_handler import MeasurementHandler
# from handler_library.personal_handler import PersonalHandler
from widget_library.expert_handler import ExpertHandler
from mode_widgets.personal_handler import PersonalHandler
from mode_widgets.mode_handler import ModeHandler
from mode_widgets.clinical_handler import ClinicalHandler
from alarm_widgets.alarm_handler import AlarmHandler
# from handler_library.readback_handler import ReadbackHandler
from global_widgets.global_typeval_popup import TypeValuePopup, AbstractTypeValPopup
from widget_library.numpad_widget import NumberpadWidget, AlphapadWidget
logging.basicConfig(
level=logging.INFO,
format="%(asctime)s - %(levelname)s - %(message)s \n (%(pathname)s, %(lineno)s)",
)
logging.basicConfig(level=logging.WARNING, format='%(asctime)s - %(levelname)s - %(message)s')
class NativeUI(HEVClient, QMainWindow):
"""Main application with client logic"""
def __init__(self, *args, **kwargs):
super(NativeUI, self).__init__(*args, **kwargs)
self.setWindowTitle("HEV NativeUI")
self.main_view = MainView()
self.setCentralWidget(self.main_view)
"""
Main application with client logic
Subclassed HEVClient for client logic, and QMainWindow for display.
Extends its base classes with the following methods:
- __define_connections - connects widget signals and slots to allow the UI to
function. Called once during initialisation.
- __find icons - locate the directory containing icons used for UI buttons and
displays. Called once during initialisation.
- __find_configs - locate the directory containing config files used by the UI. Called
once during initialisation.
- set_current_mode - TODO: deprecated, remove.
- get_updates - Overrides the placeholder get_updates method of HEVClient. Passes
payloads to handlers. Called whenever a new payload is read in.
- change_page (Slot) - switches the page shown in the page_stack. Called whent the
page buttons are pressed or whenever a popup needs to show a specific page.
- q_send_cmd (Slot) - send a command to the MCU.
- q_ack_alarm (Slot) - acknowledge a recieved alarm.
- q_send_personal (Slot) - send personal information to the MCU.
Also adds the following keyword arguments:
- resolution
- skip_startup
- language
"""
def __init__(
self,
*args,
resolution: list = [1920, 1080],
skip_startup: bool = False,
language: str = "english",
**kwargs,
):
super().__init__(*args, **kwargs)
# store variable to change editability of screen - implemented in screen locking
self.enableState = True
# Set the resolution of the display window
self.screen_width = resolution[0]
self.screen_height = resolution[1]
# Set up available modes
self.modeList = ["PC/AC", "PC/AC-PRVC", "PC-PSV", "CPAP"]
self.currentMode = self.modeList[0]
# Import settings from config files.
# Colorblind friendly ref: https://i.stack.imgur.com/zX6EV.png
config_path = self.__find_configs()
self.localisation_files = self.__find_localisation_files(config_path)
with open(os.path.join(config_path, "colors.json")) as infile:
self.colors = json.load(infile)
for key in self.colors:
self.colors[key] = QColor.fromRgb(*self.colors[key])
# Import the specified language config file
language_config = os.path.join(config_path, "text_%s.json" % language)
if not language_config in self.localisation_files:
logging.error(
"No config file for %s language (expected file at %s), defaulting to %s",
(language, language_config, self.localisation_files[0]),
)
language_config = self.localisation_files[0]
with open(language_config) as infile:
self.text = json.load(infile)
# Reorder localisation_files so that the language used is the first index
self.localisation_files.insert(
0,
self.localisation_files.pop(self.localisation_files.index(language_config)),
)
# Set up fonts based on the screen resolution. text_font and value_font are 20
# and 40px respectively for 1920*1080.
self.text_font = QFont("Sans Serif", int(resolution[0] / 96))
self.value_font = QFont("Sans Serif", int(2 * resolution[0] / 96))
# Set the popup size based on the screen resolution. alarm_popup_width is 400
# for 1920*1080
self.alarm_popup_width = int(resolution[0] / 4.8)
# Import icons
self.icons = {
"button_main_page": "user-md-solid",
"button_alarms_page": "exclamation-triangle-solid",
"button_modes_page": "fan-solid",
"button_settings_page": "sliders-h-solid",
"lock_screen": "lock-solid",
}
self.iconext = "png"
self.iconpath = self.__find_icons(self.iconext)
for key in self.icons:
self.icons[key] = os.path.join(
self.iconpath, self.icons[key] + "." + self.iconext
)
# Appearance settings
palette = self.palette()
palette.setColor(QPalette.Window, self.colors["page_background"])
self.alt_palette = self.palette()
# Set up the handlers
self.battery_handler = BatteryHandler()
self.data_handler = DataHandler(plot_history_length=1000)
self.measurement_handler = MeasurementHandler()
self.personal_handler = PersonalHandler(self)
self.mode_handler = ModeHandler(self) # , self.mode_confirm_popup)
self.expert_handler = ExpertHandler(self)
self.clinical_handler = ClinicalHandler(self)
self.alarm_handler = AlarmHandler(self)
self.__payload_handlers = [
self.battery_handler,
self.data_handler,
self.measurement_handler,
self.personal_handler,
self.mode_handler,
self.expert_handler,
self.clinical_handler,
self.alarm_handler,
]
self.messageCommandPopup = SetConfirmPopup(self)
self.typeValPopupNum = AbstractTypeValPopup(
self, "numeric"
) # TypeValuePopup(self, NumberpadWidget(self))
self.typeValPopupAlpha = AbstractTypeValPopup(
self, "alpha"
) # TypeValuePopup(self, AlphapadWidget(self))
# Create all of the widgets and place them in the layout.
self.widgets = Widgets(self)
self.layouts = Layout(self, self.widgets)
# Setup, main_display, and popups created in a stack so we can only show one at
# a time.
self.messageCommandPopup = SetConfirmPopup(self)
self.confirmPopup = confirmPopup(self, self)
self.confirmPopup.show()
self.main_display = QWidget(self)
self.main_display.setLayout(self.layouts.global_layout())
self.startupWidget = QDialog(self)
self.startupWidget.setLayout(self.layouts.startup_layout())
self.startupWidget.setPalette(palette)
self.startupWidget.setAutoFillBackground(True)
self.display_stack = QStackedWidget(self)
for widget in [
self.typeValPopupNum,
self.typeValPopupAlpha,
self.messageCommandPopup,
# self.confirmPopup,
self.main_display,
self.startupWidget,
]:
self.display_stack.addWidget(widget)
self.setCentralWidget(self.display_stack)
# Set up status bar and window title (the title is only shown in windowed mode).
self.statusBar().showMessage("Waiting for data")
self.data = {}
self.target = {}
self.readback = {}
self.cycle = {}
self.battery = {}
self.plots = np.zeros((500, 4))
self.plots[:, 0] = np.arange(500) # fill timestamp with 0-499
def start_client(self):
"""runs in other thread - works as long as super goes last and nothing
else is blocking. If something more than a one-shot process is needed
then use async
"""
# call for all the targets and personal details
# when starting the web app so we always have some in the db
self.send_cmd("GET_TARGETS", "PC_AC")
self.send_cmd("GET_TARGETS", "PC_AC_PRVC")
self.send_cmd("GET_TARGETS", "PC_PSV")
self.send_cmd("GET_TARGETS", "TEST")
self.send_cmd("GENERAL", "GET_PERSONAL")
super().start_client()
def get_updates(self, payload):
"""callback from the polling function, payload is data from socket """
# Store data in dictionary of lists
self.statusBar().setStyleSheet("color:" + self.colors["page_foreground"].name())
self.setWindowTitle(self.text["ui_window_title"].format(version=__version__))
self.setPalette(palette)
self.setAutoFillBackground(True)
self.widgets.version_display_widget.update_UI_version(__version__)
self.widgets.version_display_widget.update_UI_hash(self.__get_hash())
# Connect widgets
self.__define_connections()
# Update page buttons to match the shown view
if skip_startup:
self.display_stack.setCurrentWidget(self.main_display)
else:
self.display_stack.setCurrentWidget(self.startupWidget)
self.widgets.page_buttons.buttons[0].on_press()
def __find_localisation_files(self, config_path: str) -> list:
"""
List the availale localisation files.
"""
files_list = [
os.path.join(config_path, file)
for file in ["text_english.json", "text_portuguese.json"]
]
for file in files_list:
assert os.path.isfile(file)
return files_list
def __get_hash(self) -> str:
"""
Get the hash of the current commit.
"""
repo = git.Repo(search_parent_directories=True)
return repo.head.object.hexsha
def __define_connections(self) -> int:
"""
Connect the signals and slots necessary for the UI to function.
Connections defined here:
battery_handler.UpdateBatteryDisplay -> battery_display.update_Status
UpdateBatteryDisplay is emitted by the battery handler in response to a
battery payload.
personal_handler.UpdatePersonalDisplay -> personal_display.update_status
UpdatePersonalDisplay is emitted by the personal handler in response to a
change in the personal information.
HistoryButtonPressed -> normal_plots, detailed_plots
History buttons control the amount of time shown on the x axes of the
value-time plots on the main page. HistoryButtonPressed is emitted by the
history button when pressed.
PageButtonPressed -> change_page
Page buttons control which page is shown in the page_stack.
PageButtonPressed is emitted by the page button when pressed.
ToggleButtonPressed -> charts_widget.show_line
ToggleButtonPressed -> charts_widget.hide_line
Lines in the charts_widget are shown or hidden depending on whether the
relevent toggle button is in the checked or unchecked state.
ToggleButtonPressed is emitted by the toggle button when pressed.
"""
# Startup connections
self.widgets.calibration.button.pressed.connect(
lambda i=self.widgets.calibration: self.widgets.startup_handler.handle_calibrationPress(
i
)
)
self.widgets.leak_test.button.pressed.connect(
lambda i=self.widgets.leak_test: self.widgets.startup_handler.handle_calibrationPress(
i
)
)
self.widgets.maintenance.button.pressed.connect(
lambda i=self.widgets.maintenance: self.widgets.startup_handler.handle_calibrationPress(
i
)
)
for key, radio in self.widgets.startup_handler.settingsRadioDict.items():
radio.toggled.connect(
lambda i, j=key: self.widgets.startup_handler.handle_settings_radiobutton(
i, j
)
)
for radio in self.widgets.startup_handler.modeRadioDict.values():
radio.toggled.connect(
lambda i, j=radio: self.widgets.startup_handler.handle_mode_radiobutton(
i, j
)
)
# Startup next and skip buttons should move from the startup widget to the main
# display
self.widgets.nextButton.pressed.connect(
lambda: self.display_stack.setCurrentWidget(self.main_display)
)
self.widgets.skipButton.pressed.connect(
lambda: self.display_stack.setCurrentWidget(self.main_display)
)
self.widgets.lock_button.PageButtonPressed.connect(self.toggle_editability)
# Startup next button should send the ventilator start command.
self.widgets.nextButton.pressed.connect(
lambda: self.q_send_cmd("GENERAL", "START")
)
# Battery Display should update when we get battery info
self.battery_handler.UpdateBatteryDisplay.connect(
self.widgets.battery_display.update_status
)
# Personal Display should update when personal info is changed.
self.personal_handler.UpdatePersonalDisplay.connect(
self.widgets.personal_display.update_status
)
# Measurement Widgets should update when the data is changed.
for widget in (
self.widgets.normal_measurements.widget_list
+ self.widgets.detailed_measurements.widget_list
):
self.measurement_handler.UpdateMeasurements.connect(widget.set_value)
# When plot data is updated, plots should update
for plot_widget in [
self.widgets.normal_plots.update_plot_data,
self.widgets.detailed_plots.update_plot_data,
self.widgets.circle_plots.update_plot_data,
self.widgets.charts_widget.update_plot_data,
]:
self.data_handler.UpdatePlots.connect(plot_widget)
# Plots should update when we press the history buttons
for button in self.widgets.history_buttons.buttons:
for widget in [self.widgets.normal_plots, self.widgets.detailed_plots]:
button.HistoryButtonPressed.connect(widget.update_plot_time_range)
# The shown page should change when we press the page buttons
for button in self.widgets.page_buttons.buttons:
button.PageButtonPressed.connect(self.change_page)
# Start button should raise the startup widget.
self.widgets.ventilator_start_stop_buttons_widget.button_start.pressed.connect(
lambda: self.display_stack.setCurrentWidget(self.startupWidget)
)
self.typeValPopupNum.okButton.pressed.connect(
self.typeValPopupNum.handle_ok_press
)
self.typeValPopupAlpha.okButton.pressed.connect(
self.typeValPopupNum.handle_ok_press
)
##### Mode:
# When mode is switched from mode page, various other locations must respond
for widget in self.mode_handler.spinDict.values():
self.mode_handler.UpdateModes.connect(widget.update_value)
widget.simpleSpin.manualChanged.connect(
lambda i=widget: self.clinical_handler.setpoint_changed(i)
)
widget.simpleSpin.manualChanged.connect(
lambda i=widget: self.mode_handler.propagate_modevalchange(i)
)
for widget in self.mode_handler.mainSpinDict.values():
self.mode_handler.UpdateModes.connect(widget.update_value)
widget.simpleSpin.manualChanged.connect(
lambda i=widget: self.clinical_handler.setpoint_changed(i)
)
widget.simpleSpin.manualChanged.connect(
lambda i=widget: self.mode_handler.propagate_modevalchange(i)
)
# widget.simpleSpin.manualChanged.connect(lambda i=widget: self.mode_handler.mode_value(i))
self.mode_handler.modeSwitched.connect(lambda i: self.set_current_mode(i))
self.mode_handler.modeSwitched.connect(
lambda i: self.widgets.tab_modeswitch.update_mode(i)
)
self.mode_handler.modeSwitched.connect(self.mode_handler.refresh_button_colour)
# when mode is switched from modeSwitch button, other locations must respond
self.widgets.tab_modeswitch.modeSwitched.connect(
lambda i: self.set_current_mode(i)
)
self.widgets.tab_modeswitch.modeSwitched.connect(
self.mode_handler.refresh_button_colour
)
# mode_handler should respond to manual spin box changes
for key, spin_widget in self.mode_handler.spinDict.items():
spin_widget.simpleSpin.manualChanged.connect(
lambda i=key: self.mode_handler.handle_manual_change(i)
)
for key, spin_widget in self.mode_handler.mainSpinDict.items():
spin_widget.simpleSpin.manualChanged.connect(
lambda i=key: self.mode_handler.handle_manual_change(i)
)
spin_widget.simpleSpin.programmaticallyChanged.connect(
lambda i=key: self.mode_handler.handle_manual_change(i)
)
# mode_handler should respond to user selection of radio button
for key, radio_widget in self.mode_handler.radioDict.items():
radio_widget.toggled.connect(
lambda i, j=key: self.mode_handler.handle_radio_toggled(i, j)
)
# mode_handler should respond to ok, send, or cancel presses
for key, button_widget in self.mode_handler.buttonDict.items():
if isinstance(button_widget, (OkButtonWidget, OkSendButtonWidget)):
button_widget.pressed.connect(
lambda i=key: self.mode_handler.handle_okbutton_click(i)
)
button_widget.pressed.connect(self.clinical_handler.commandSent)
elif isinstance(button_widget, CancelButtonWidget):
buttonMode = self.mode_handler.get_mode(key)
button_widget.pressed.connect(
lambda i=buttonMode: self.mode_handler.handle_cancel_pressed(i)
)
button_widget.pressed.connect(
lambda i=buttonMode: self.clinical_handler.handle_cancel_pressed(i)
)
for key, button_widget in self.mode_handler.mainButtonDict.items():
if isinstance(button_widget, (OkButtonWidget)):
button_widget.clicked.connect(
self.mode_handler.handle_mainokbutton_click
)
button_widget.pressed.connect(self.clinical_handler.commandSent)
elif isinstance(button_widget, CancelButtonWidget):
# mode = self.mode_handler.get_mode(key)
button_widget.clicked.connect(self.mode_handler.commandSent)
button_widget.pressed.connect(self.clinical_handler.commandSent)
for key, spin_widget in self.clinical_handler.limSpinDict.items():
spin_widget.simpleSpin.manualChanged.connect(
lambda i=key: self.clinical_handler.handle_manual_change(i)
)
for key, spin_widget in self.clinical_handler.setSpinDict.items():
spin_widget.simpleSpin.manualChanged.connect(
lambda i=spin_widget: self.clinical_handler.setpoint_changed(i)
)
spin_widget.simpleSpin.manualChanged.connect(
lambda i=key: self.clinical_handler.handle_manual_change(i)
)
spin_widget.simpleSpin.manualChanged.connect(
lambda i=spin_widget: self.mode_handler.propagate_modevalchange(i)
)
for key, button_widget in self.clinical_handler.buttonDict.items():
if isinstance(button_widget, (OkButtonWidget)):
button_widget.clicked.connect(
self.clinical_handler.handle_okbutton_click
)
elif isinstance(button_widget, CancelButtonWidget):
button_widget.clicked.connect(
self.clinical_handler.handle_cancelbutton_click
)
for widget in self.clinical_handler.setSpinDict.values():
self.clinical_handler.UpdateClinical.connect(widget.update_value)
self.mode_handler.OpenPopup.connect(self.messageCommandPopup.populatePopup)
self.mode_handler.OpenPopup.connect(
lambda: self.display_stack.setCurrentWidget(self.messageCommandPopup)
)
self.messageCommandPopup.ModeSend.connect(self.mode_handler.sendCommands)
self.expert_handler.OpenPopup.connect(self.messageCommandPopup.populatePopup)
self.expert_handler.OpenPopup.connect(
lambda: self.display_stack.setCurrentWidget(self.messageCommandPopup)
)
self.messageCommandPopup.ExpertSend.connect(self.expert_handler.sendCommands)
self.clinical_handler.OpenPopup.connect(self.messageCommandPopup.populatePopup)
self.messageCommandPopup.ClinicalSend.connect(
self.clinical_handler.sendCommands
)
self.messageCommandPopup.cancelButton.pressed.connect(
lambda: self.display_stack.setCurrentWidget(self.main_display)
)
self.messageCommandPopup.okButton.pressed.connect(
lambda: self.display_stack.setCurrentWidget(self.main_display)
)
##### Expert Settings:
self.widgets.expert_password_widget.okButton.pressed.connect(
self.widgets.expert_password_widget.submit_password
)
self.widgets.expert_password_widget.correctPassword.connect(
lambda: self.widgets.expert_passlock_stack.setCurrentIndex(1)
)
# Expert handler should respond to manual value changes
for key, spin_widget in self.expert_handler.spinDict.items():
spin_widget.simpleSpin.manualChanged.connect(
lambda i=key: self.expert_handler.handle_manual_change(i)
)
# mode_handler should respond to ok, send, or cancel presses
for key, button_widget in self.expert_handler.buttonDict.items():
if isinstance(button_widget, OkButtonWidget) or isinstance(
button_widget, OkSendButtonWidget
):
button_widget.pressed.connect(
lambda i=key: self.expert_handler.handle_okbutton_click(i)
)
elif isinstance(button_widget, CancelButtonWidget):
button_widget.pressed.connect(self.expert_handler.commandSent)
for widget in self.expert_handler.spinDict.values():
self.expert_handler.UpdateExpert.connect(widget.update_value)
# Lines displayed on the charts page should update when the corresponding
# buttons are toggled.
for button in self.widgets.chart_buttons_widget.buttons:
button.ToggleButtonPressed.connect(self.widgets.charts_widget.show_line)
button.ToggleButtonReleased.connect(self.widgets.charts_widget.hide_line)
button.on_press() # Ensure states of the plots match states of buttons.
button.toggle()
# Plot data and measurements should update on a timer
self.timer = QTimer()
self.timer.setInterval(16) # just faster than 60Hz
self.timer.timeout.connect(self.data_handler.send_update_plots_signal)
# self.timer.timeout.connect(self.widgets.alarm_handler.update_alarms)
# self.timer.timeout.connect(self.mode_handler.update_values)
# self.timer.timeout.connect(self.expert_handler.update_values)
self.timer.start()
# Localisation needs to update widgets
for widget in [
self.widgets.normal_measurements,
self.widgets.detailed_measurements,
self.widgets.normal_plots,
self.widgets.detailed_plots,
self.widgets.circle_plots,
self.widgets.ventilator_start_stop_buttons_widget,
self.widgets.charts_widget,
self.widgets.plot_stack,
self.widgets.alarms_page,
self.widgets.settings_page,
self.widgets.modes_page,
self.widgets.modes_stack,
self.widgets.startup_stack,
self.widgets.mode_settings_stack,
self.widgets.mode_settings_stack_startup,
# self.widgets.spin_buttons,
# self.widgets.mode_personal_tab,
]:
self.widgets.localisation_button.SetLocalisation.connect(
widget.localise_text
)
self.alarm_handler.UpdateAlarm.connect(self.alarm_handler.handle_newAlarm)
self.alarm_handler.NewAlarm.connect(self.widgets.alarm_popup.addAlarm)
self.alarm_handler.NewAlarm.connect(self.widgets.alarm_list.addAlarm)
self.alarm_handler.NewAlarm.connect(self.widgets.alarm_table.addAlarmRow)
self.alarm_handler.RemoveAlarm.connect(self.widgets.alarm_popup.removeAlarm)
self.alarm_handler.RemoveAlarm.connect(self.widgets.alarm_list.removeAlarm)
# Localisation needs to update widgets
self.widgets.localisation_button.SetLocalisation.connect(
self.widgets.normal_measurements.localise_text
)
return 0
def __find_icons(self, iconext: str) -> str:
"""
Locate the icons directory and return its path.
Assumes that the cwd is in a git repo, and that the path of the icons folder
relative to the root of the repo is "hev-display/assets/png/".
"""
# Find the root of the git repo
rootdir = git.Repo(os.getcwd(), search_parent_directories=True).git.rev_parse(
"--show-toplevel"
)
icondir = os.path.join(rootdir, "hev-display", "assets", iconext)
if not os.path.isdir(icondir):
raise FileNotFoundError("Could not find icon directory at %s" % icondir)
return icondir
def __find_configs(self) -> str:
"""
Locate the config files directory and return its path.
Assumes that the cwd is in a git repo, and that the path of the icons folder
relative to the root of the repo is "NativeUI/configs".
"""
# Find the root of the git repo
rootdir = git.Repo(os.getcwd(), search_parent_directories=True).git.rev_parse(
"--show-toplevel"
)
configdir = os.path.join(rootdir, "NativeUI", "configs")
if not os.path.isdir(configdir):
raise FileNotFoundError("Could not find icon directory at %s" % configdir)
return configdir
def set_current_mode(self, mode):
"""
Set the current mode
TODO: move to mode handler
"""
print("setting native ui mode")
print(mode)
self.currentMode = mode
def get_updates(self, payload: dict) -> int:
"""
Callback from the polling function, payload is data from socket.
Passes the payload to each of the handlers in self.__payload_handlers. If no
handlers return 0, indicating that the payload is not dealt with by any handler,
log a warning.
"""
self.statusBar().showMessage(f"{payload}")
try:
if (payload["type"] == "DATA"):
self.data = payload["DATA"]
# remove first entry and append plot data to end
self.plots = np.append(np.delete(self.plots, 0, 0), [[self.data["timestamp"], self.data["pressure_patient"], self.data["flow"], self.data["volume"]]], axis=0)
except KeyError:
logging.warning(f"Invalid payload: {payload}")
logging.debug("recieved payload of type %s", payload["type"])
payload_registered = False
for handler in self.__payload_handlers:
if handler.set_db(payload) == 0:
payload_registered = True
if not payload_registered:
logging.warning("Handlers: Invalid payload type: %s", payload["type"])
logging.debug("Content of invalid payload:\n%s", payload)
return 0
def toggle_editability(self):
"""Set all widgets disabled to lock screen"""
self.enableState = not self.enableState
if self.enableState:
self.alt_palette.setColor(QPalette.Window, self.colors["page_background"])
else:
self.alt_palette.setColor(QPalette.Window, self.colors["page_foreground"])
self.setPalette(self.alt_palette)
for attribute in dir(self.widgets):
widg = self.widgets.get_widget(attribute)
if isinstance(widg, QWidget):
widg.setEnabled(self.enableState)
self.widgets.lock_button.setEnabled(True)
@Slot(str)
def change_page(self, page_to_show: str) -> int:
"""
Change the page shown in page_stack.
"""
self.widgets.page_stack.setCurrentWidget(getattr(self.widgets, page_to_show))
self.widgets.expert_passlock_stack.setCurrentIndex(
0
) # reset password lock on expert settings
return 0
@Slot(str, str, float)
def q_send_cmd(self, cmdtype: str, cmd: str, param: float = None) -> None:
"""send command to hevserver via socket"""
self.send_cmd(cmdtype=cmdtype, cmd=cmd, param=param)
def q_send_cmd(self, cmdtype: str, cmd: str, param: float = None) -> int:
"""
Send command to hevserver via socket.
"""
logging.debug("to MCU: cmd: %s", cmd)
check = self.send_cmd(cmdtype=cmdtype, cmd=cmd, param=param)
if check:
self.confirmPopup.addConfirmation(cmdtype + " " + cmd)
return 0
else:
return 1
@Slot(str)
def q_ack_alarm(self, alarm: str):
"""acknowledge an alarm in the hevserver"""
def q_ack_alarm(self, alarm: str) -> int:
"""
Acknowledge an alarm in the hevserver
"""
logging.debug("To MCU: Acknowledging alarm: %s", alarm)
self.ack_alarm(alarm=alarm)
return 0
@Slot(str)
def q_send_personal(self, personal: str):
"""send personal details to hevserver"""
def q_send_personal(self, personal: str) -> int:
"""
Send personal details to the hevserver.
"""
logging.debug("to MCU: Setting personal data: %s", personal)
self.send_personal(personal=personal)
return 0
if __name__ == "__main__":
# parse args and setup logging
parser = argparse.ArgumentParser(description='Plotting script for the HEV lab setup')
parser.add_argument('-d', '--debug', action='count', default=0, help='Show debug output')
def parse_command_line_arguments() -> argparse.Namespace:
"""
Returns the parsed command line arguments.
"""
parser = argparse.ArgumentParser(
description="Plotting script for the HEV lab setup"
)
parser.add_argument(
"-d", "--debug", action="count", default=0, help="Show debug output"
)
parser.add_argument(
"-w",
"--windowed",
action="store_true",
default=False,
help="Run the UI in wondowed mode",
)
parser.add_argument(
"-r", "--resolution", action="store", dest="resolution", type=str
)
parser.add_argument(
"--no-startup",
action="store_true",
default=False,
help="Run the UI without the startup sequence",
)
parser.add_argument(
"-l",
"--language",
action="store",
dest="language",
default="English",
type=str,
help="""
Set the language for the UI from the following list:\n
Language Recognised Values\n
English e, E, english, English\n
Portugues p, P, portugues, Portugues
""",
)
return parser.parse_args()
args = parser.parse_args()
if args.debug == 0:
def set_logging_level(debug_level: int) -> int:
"""
Set the level of logging output according to the value of debug_level:
0 = Warning
1 = Info
2 = Debug
"""
if debug_level == 0:
logging.getLogger().setLevel(logging.WARNING)
elif args.debug == 1:
elif debug_level == 1:
logging.getLogger().setLevel(logging.INFO)
else:
logging.getLogger().setLevel(logging.DEBUG)
return 0
def interpret_language(input_string: str) -> str:
"""
Convert the value given for the language CLA to one that NativeUI can interpret.
"""
default_language = "English"
if input_string is None:
return default_language
keys_english = ["e", "E", "english", "English"]
keys_portugues = ["p", "P", "portugues", "Portugues", "portuguese", "Portuguese"]
if input_string in keys_english:
return "english"
if input_string in keys_portugues:
return "portuguese"
logging.error(
"Unrecognised language value %s, defaulting to %s",
(input_string, default_language),
)
return default_language
def interpret_resolution(input_string: str) -> list:
"""
Convert a string to a pair of numbers specifying the window size.
Given a string of the form "[int][*][int]" where [*] is and non-numerical character,
returns a list [int, int]. If the provided string is None or cannot be interpreted,
returns the default window size [1920, 1080].
"""
default_size = [1920, 1080]
if input_string is None:
return default_size
dimensions = [val for val in re.findall("\d*", input_string) if val != ""]
if len(dimensions) != 2:
logging.warning("Unsupported resolution argument: %s", input_string)
return default_size
try:
dimensions = [int(val) for val in dimensions]
except ValueError:
logging.warning(
"Resolution argument '%s' could not be interpreted as numerical values."
"Values must be integer numbers of pixels on x and y respectively,"
"e.g. 1920x1080.",
input_string,
)
return default_size
return dimensions
def set_window_size(window, resolution: str = None, windowed: bool = False) -> int:
"""
Set the size and position of the window.
By default the window will be borderless, 1920x1080 pixels, and positioned at 0,0.
If the "windowed" argument is True, the window will be bordered. Uses
interpret_resolution to extract size parameters from the "resolution" string. If the
string cannot be interpreted, or the resolution argument is None, uses
interpret_resolution's default size.
"""
window_size = interpret_resolution(resolution)
window.setFixedSize(*window_size)
if not windowed:
window.move(0, 0)
window.setWindowFlags(Qt.FramelessWindowHint)
return 0
if __name__ == "__main__":
# parse args and setup logging
command_line_args = parse_command_line_arguments()
set_logging_level(command_line_args.debug)
# setup pyqtplot widget
app = QApplication(sys.argv)
dep = NativeUI()
dep = NativeUI(
resolution=interpret_resolution(command_line_args.resolution),
skip_startup=command_line_args.no_startup,
language=interpret_language(command_line_args.language),
)
set_window_size(
dep,
resolution=command_line_args.resolution,
windowed=command_line_args.windowed,
)
dep.show()
app.exec_()
#!/usr/bin/env python3
"""
tab_alarms.py
"""
__author__ = ["Benjamin Mummery", "Tiago Sarmento"]
__credits__ = ["Benjamin Mummery", "Dónal Murray", "Tim Powell", "Tiago Sarmento"]
__license__ = "GPL"
__version__ = "0.0.1"
__maintainer__ = "Tiago Sarmento"
__email__ = "tiago.sarmento@stfc.ac.uk"
__status__ = "Prototype"
import sys
from datetime import datetime
from PySide2 import QtCore, QtGui, QtWidgets
from handler_library.handler import PayloadHandler
import logging
class AlarmHandler(PayloadHandler):
UpdateAlarm = QtCore.Signal(dict)
NewAlarm = QtCore.Signal(QtWidgets.QWidget)
RemoveAlarm = QtCore.Signal(QtWidgets.QWidget)
def __init__(self, NativeUI, *args, **kwargs):
super().__init__(['DATA', 'ALARM'],*args, **kwargs)
self.NativeUI = NativeUI
self.alarmDict = {}
self.alarm_list = []
self.oldAlarms = []
def acknowledge_pressed(self):
self.popup.clearAlarms()
self.list.acknowledge_all()
def _set_alarm_list(self, alarm_list:list):
self.alarm_list = alarm_list
def active_payload(self, *args) -> int:
#alarm_data = self.get_db()
#outdict = {}
full_payload = args[0]
#print(full_payload['alarms'])
currentAlarms = full_payload['alarms']#self.NativeUI.ongoingAlarms # instead of getting database at a particular frequency, this should be triggered when a new alarm arrives
self.alarm_list = currentAlarms
#self._set__alarm_list(currentAlarms)
if self.oldAlarms != currentAlarms:
if len(self.oldAlarms) != len(currentAlarms):
self.oldAlarms = currentAlarms
self.UpdateAlarm.emit(currentAlarms)
def handle_newAlarm(self, currentAlarms): # if this is combined with active_payload an error arises
for alarm in currentAlarms:
alarmCode = alarm["alarm_code"]
if alarmCode in self.alarmDict:
self.alarmDict[alarmCode].resetTimer()
self.alarmDict[alarmCode].calculateDuration()
else:
newAbstractAlarm = AbstractAlarm(self.NativeUI, alarm)
self.alarmDict[alarmCode] = newAbstractAlarm
self.NewAlarm.emit(newAbstractAlarm)
newAbstractAlarm.alarmExpired.connect(
lambda i=newAbstractAlarm: self.handleAlarmExpiry(i)
)
def handleAlarmExpiry(self, abstractAlarm):
abstractAlarm.freezeTimer()
abstractAlarm.recordFinishTime()
self.RemoveAlarm.emit(abstractAlarm)
self.alarmDict.pop(abstractAlarm.alarmPayload["alarm_code"])
# abstractAlarm is deleted by itself
class AbstractAlarm(QtCore.QObject):
alarmExpired = QtCore.Signal()
def __init__(self, NativeUI, alarmPayload, *args, **kwargs):
super(AbstractAlarm, self).__init__(*args, **kwargs)
self.NativeUI = NativeUI
self.alarmPayload = alarmPayload
self.startTime = datetime.now()
self.duration = datetime.now() - self.startTime
self.finishTime = -1
self.timer = QtCore.QTimer()
self.timer.setInterval(2000) # just faster than 60Hz
self.timer.timeout.connect(self.timeoutDelete)
self.timer.start()
def timeoutDelete(self):
# """Check alarm still exists in ongoingAlarms object. If present do nothing, otherwise delete."""
self.alarmExpired.emit()
self.setParent(None) # delete self
return 0
def resetTimer(self):
self.timer.start()
return 0
def freezeTimer(self):
self.timer.stop()
return 0
def recordFinishTime(self):
self.finishTime = datetime.now()
self.duration = self.finishTime - self.startTime
def calculateDuration(self):
self.duration = datetime.now() - self.startTime
#!/usr/bin/env python3
"""
alarm_list.py
"""
__author__ = ["Benjamin Mummery", "Tiago Sarmento"]
__credits__ = ["Benjamin Mummery", "Dónal Murray", "Tim Powell", "Tiago Sarmento"]
__license__ = "GPL"
__version__ = "0.0.1"
__maintainer__ = "Tiago Sarmento"
__email__ = "tiago.sarmento@stfc.ac.uk"
__status__ = "Prototype"
import sys
import os
from PySide2 import QtCore, QtGui, QtWidgets
from datetime import datetime
class AlarmList(QtWidgets.QListWidget):
def __init__(self, NativeUI, *args, **kwargs):
super(AlarmList, self).__init__(*args, **kwargs)
self.labelList = []
self.setSizePolicy(
QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Expanding
)
self.setStyleSheet("background-color:white;")
self.setFont(NativeUI.text_font)
iconpath_bell = os.path.join(NativeUI.iconpath, "bell-solid.png")
iconpath_bellReg = os.path.join(NativeUI.iconpath, "bell-regular.png")
self.solidBell = QtGui.QIcon(iconpath_bell)
self.regularBell = QtGui.QIcon(iconpath_bellReg)
newItem = QtWidgets.QListWidgetItem(" ")
self.addItem(newItem)
def acknowledge_all(self):
for x in range(self.count() - 1):
self.item(x).setText("acknowledgedAlarm")
self.item(x).setIcon(self.regularBell)
def addAlarm(self, abstractAlarm):
timestamp = str(abstractAlarm.startTime)[:-3]
newItem = QtWidgets.QListWidgetItem(
self.solidBell,
timestamp
+ ": "
+ abstractAlarm.alarmPayload["alarm_type"]
+ " - "
+ abstractAlarm.alarmPayload["alarm_code"],
)
self.insertItem(0, newItem) # add to the top
# self.labelList
def removeAlarm(self, abstractAlarm):
for x in range(self.count() - 1):
if abstractAlarm.alarmPayload["alarm_code"] in self.item(x).text():
self.takeItem(x)
if __name__ == "__main__":
app = QtWidgets.QApplication(sys.argv)
widg = alarmList()
widg.show()
sys.exit(app.exec_())
#!/usr/bin/env python3
"""
alarm_popup.py
"""
__author__ = ["Benjamin Mummery", "Tiago Sarmento"]
__credits__ = ["Benjamin Mummery", "Dónal Murray", "Tim Powell", "Tiago Sarmento"]
__license__ = "GPL"
__version__ = "0.0.1"
__maintainer__ = "Tiago Sarmento"
__email__ = "tiago.sarmento@stfc.ac.uk"
__status__ = "Prototype"
import os
from PySide2 import QtCore, QtGui, QtWidgets
from datetime import datetime
class AlarmWidget(QtWidgets.QWidget):
"""Object containing information particular to one alarm.
Created when alarm received from microcontroller, timeout after alarm signal stops.
Is contained within alarmPopup"""
def __init__(self, NativeUI, abstractAlarm, alarmCarrier, *args, **kwargs):
super(AlarmWidget, self).__init__(*args, **kwargs)
popup_height = int(NativeUI.alarm_popup_width / 10.0)
self.NativeUI = NativeUI
self.alarmCarrier = alarmCarrier # Needs to refer to its containing object
self.layout = QtWidgets.QHBoxLayout()
self.layout.setSpacing(0)
self.layout.setMargin(0)
self.alarmPayload = abstractAlarm.alarmPayload
iconLabel = QtWidgets.QLabel()
iconpath_check = os.path.join(
self.NativeUI.iconpath, "exclamation-triangle-solid.png"
)
pixmap = QtGui.QPixmap(iconpath_check).scaledToHeight(popup_height)
iconLabel.setPixmap(pixmap)
self.layout.addWidget(iconLabel)
self.textLabel = QtWidgets.QLabel()
alarmLevel = self.alarmPayload["alarm_type"] # .replace('PRIORITY_', '')
self.textLabel.setText(
self.alarmPayload["alarm_code"] + " - (" + alarmLevel + ")"
)
self.textLabel.setFixedWidth(NativeUI.alarm_popup_width)
self.textLabel.setAlignment(QtCore.Qt.AlignCenter)
self.textLabel.setFont(NativeUI.text_font)
# self.textLabel.setStyleSheet("font-size: " + NativeUI.text_size + ";")
self.layout.addWidget(self.textLabel)
self.setFixedHeight(popup_height)
self.setLayout(self.layout)
if self.alarmPayload["alarm_type"] == "PRIORITY_HIGH":
self.setStyleSheet("background-color:red;")
elif self.alarmPayload["alarm_type"] == "PRIORITY_MEDIUM":
self.setStyleSheet("background-color:orange;")
self.setFixedSize(NativeUI.alarm_popup_width + popup_height, popup_height)
# self.timer = QtCore.QTimer()
# self.timer.setInterval(500) # just faster than 60Hz
# self.timer.timeout.connect(self.checkAlarm)
# self.timer.start()
self.installEventFilter(self)
def eventFilter(self, source, event):
if event.type() == QtCore.QEvent.MouseButtonPress:
self.NativeUI.widgets.page_buttons.alarms_button.click()
return False
def get_priority(self):
return self.alarmPayload["alarm_type"]
def setFont(self, font) -> int:
"""
Set the font for textLabel.
"""
self.textLabel.setFont(font)
return 0
# def checkAlarm(self):
# """Check alarm still exists in ongoingAlarms object. If present do nothing, otherwise delete."""
# self.ongoingAlarms = self.NativeUI.ongoingAlarms
# for alarm in self.ongoingAlarms:
# if self.alarmPayload["alarm_code"] == alarm["alarm_code"]:
# return
# self.alarmCarrier.alarmDict.pop(self.alarmPayload["alarm_code"])
# self.setParent(None) # delete self
# return 0
class AlarmPopup(QtWidgets.QDialog):
"""Container class for alarm widgets. Handles ordering and positioning of alarms.
Needs to adjust its size whenever a widget is deleted"""
def __init__(self, NativeUI, *args, **kwargs):
super(AlarmPopup, self).__init__(*args, **kwargs)
self.setParent(NativeUI) # ensures popup closes when main UI does
self.alarmDict = {}
self.NativeUI = NativeUI
self.extraAlarms = AlarmExtrasWidget(NativeUI, self)
self.layout = QtWidgets.QVBoxLayout()
self.layout.setSpacing(0)
self.layout.setMargin(0)
self.setLayout(self.layout)
self.location_on_window()
self.setWindowFlags(
QtCore.Qt.FramelessWindowHint
| QtCore.Qt.Dialog
| QtCore.Qt.WindowStaysOnTopHint
) # no window title
self.shadow = QtWidgets.QGraphicsDropShadowEffect()
self.shadow.setBlurRadius(20)
self.shadow.setXOffset(10)
self.shadow.setYOffset(10)
self.timer = QtCore.QTimer()
self.timer.setInterval(100) # just faster than 60Hz
self.timer.timeout.connect(self.adjustSize)
self.timer.start()
self.show()
def clearAlarms(self):
"""Wipe all alarms out and clear dictionary"""
for i in reversed(range(self.layout.count())):
self.layout.itemAt(i).widget().setParent(None)
self.adjustSize()
self.setLayout(self.layout)
self.alarmDict = {}
return 0
def addAlarm(self, abstractAlarm):
"""Creates a new alarmWidget and adds it to the container"""
self.alarmDict[abstractAlarm.alarmPayload["alarm_code"]] = AlarmWidget(
self.NativeUI, abstractAlarm, self
)
self.refresh_alarm_ordering()
# self.layout.addWidget(self.alarmDict[abstractAlarm.alarmPayload["alarm_code"]])
return 0
def removeAlarm(self, abstractAlarm):
"""Creates a new alarmWidget and adds it to the container"""
self.alarmDict[abstractAlarm.alarmPayload["alarm_code"]].setParent(None)
self.alarmDict.pop(abstractAlarm.alarmPayload["alarm_code"])
self.refresh_alarm_ordering()
return 0
def refresh_alarm_ordering(self):
self.layout.removeWidget(self.extraAlarms)
for key in self.alarmDict:
self.layout.removeWidget(self.alarmDict[key])
for key in self.alarmDict:
if self.alarmDict[key].get_priority() == "PRIORITY_HIGH":
if self.layout.count() == 4:
self.extraAlarms.update_text(
1 + len(self.alarmDict) - self.layout.count()
)
self.layout.addWidget(self.extraAlarms)
break
self.layout.addWidget(self.alarmDict[key])
if self.layout.count() < 5:
for key in self.alarmDict:
if self.layout.count() == 3:
self.extraAlarms.update_text(
len(self.alarmDict) - self.layout.count()
)
self.layout.addWidget(self.extraAlarms)
break
if self.alarmDict[key].get_priority() == "PRIORITY_LOW":
self.layout.addWidget(self.alarmDict[key])
# def resetTimer(self, alarmPayload):
# self.alarmDict[alarmPayload["alarm_code"]].timer.start()
def location_on_window(self):
"""Position the popup as defined here"""
screen = QtWidgets.QDesktopWidget().screenGeometry()
x = screen.width() - screen.width() / 2
y = 0 # screen.height() - widget.height()
self.move(x, y)
return 0
class AlarmExtrasWidget(QtWidgets.QWidget):
"""Object containing information particular to one alarm.
Created when alarm received from microcontroller, timeout after alarm signal stops.
Is contained within alarmPopup"""
def __init__(self, NativeUI, alarmCarrier, *args, **kwargs):
super(AlarmExtrasWidget, self).__init__(*args, **kwargs)
popup_height = int(NativeUI.alarm_popup_width / 10.0)
self.NativeUI = NativeUI
self.alarmCarrier = alarmCarrier # Needs to refer to its containing object
self.layout = QtWidgets.QHBoxLayout()
self.layout.setSpacing(0)
self.layout.setMargin(0)
# self.alarmPayload = abstractAlarm.alarmPayload
iconLabel = QtWidgets.QLabel()
iconpath_check = os.path.join(
self.NativeUI.iconpath, "exclamation-triangle-solid.png"
)
pixmap = QtGui.QPixmap(iconpath_check).scaledToHeight(popup_height)
iconLabel.setPixmap(pixmap)
self.layout.addWidget(iconLabel)
self.textLabel = QtWidgets.QLabel()
self.textLabel.setText("1 More Alarms")
self.textLabel.setFixedWidth(NativeUI.alarm_popup_width)
self.textLabel.setAlignment(QtCore.Qt.AlignCenter)
self.textLabel.setFont(NativeUI.text_font)
# self.textLabel.setStyleSheet("font-size: " + NativeUI.text_size + ";")
self.layout.addWidget(self.textLabel)
self.setFixedHeight(popup_height)
self.setLayout(self.layout)
self.setStyleSheet("background-color:red;")
# self.priority = "PRIORITY_LOW"
self.installEventFilter(self)
self.setFixedSize(NativeUI.alarm_popup_width + popup_height, popup_height)
def update_text(self, num):
self.textLabel.setText(str(num) + " More Alarms")
def eventFilter(self, source, event):
if event.type() == QtCore.QEvent.MouseButtonPress:
self.NativeUI.widgets.page_buttons.alarms_button.click()
return False
def get_priority(self):
return self.alarmPayload["alarm_type"]
#!/usr/bin/env python3
"""
alarm_list.py
"""
__author__ = ["Benjamin Mummery", "Tiago Sarmento"]
__credits__ = ["Benjamin Mummery", "Dónal Murray", "Tim Powell", "Tiago Sarmento"]
__license__ = "GPL"
__version__ = "0.0.1"
__maintainer__ = "Tiago Sarmento"
__email__ = "tiago.sarmento@stfc.ac.uk"
__status__ = "Prototype"
import sys
import os
from PySide2 import QtCore, QtGui, QtWidgets
from datetime import datetime
class AlarmTable(QtWidgets.QTableWidget):
"""Table containing all of the alarms since power up are contained. Easily sorted"""
def __init__(self, NativeUI, *args, **kwargs):
super(AlarmTable, self).__init__(*args, **kwargs)
self.setSizePolicy(
QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Expanding
)
self.setStyleSheet("background-color:white;")
self.setFont(NativeUI.text_font)
self.nrows = 0
self.setColumnCount(4)
self.setSortingEnabled(True)
if self.nrows == 0:
self.setHorizontalHeaderLabels(
["Timestamp", "Priority Level", "Alarm Code", "Duration"]
)
self.payloadKeys = ["alarm_type", "alarm_code"]
self.resizeColumnsToContents()
self.alarmCellDict = {}
self.timer = QtCore.QTimer()
self.timer.setInterval(100)
# self.timer.timeout.connect(self.updateDuration)
self.timer.start()
# def addAlarm(self, abstractAlarm):
# timestamp = str(datetime.now())[:-3]
# newItem = QtWidgets.QListWidgetItem(
# self.solidBell,
# timestamp
# + ": "
# + abstractAlarm.alarmPayload["alarm_type"]
# + " - "
# + abstractAlarm.alarmPayload["alarm_code"],
# )
# self.insertItem(0, newItem) # add to the top
# def removeAlarm(self, abstractAlarm):
# for x in range(self.count() - 1):
# if abstractAlarm.alarmPayload["alarm_code"] in self.item(x).text():
# self.takeItem(x)
def addAlarmRow(self, abstractAlarm):
"""Add a new row 1 cell at a time. Goes through alarm payload to fill information"""
self.setSortingEnabled(False)
self.setRowCount(self.nrows + 1)
newItem = QtWidgets.QTableWidgetItem(str(abstractAlarm.startTime)[:-3])
self.setItem(self.nrows, 0, newItem)
newItem = QtWidgets.QTableWidgetItem(abstractAlarm.alarmPayload["alarm_type"])
self.setItem(self.nrows, 1, newItem)
newItem = QtWidgets.QTableWidgetItem(abstractAlarm.alarmPayload["alarm_code"])
self.setItem(self.nrows, 2, newItem)
newItem = QtWidgets.QTableWidgetItem(" ")
self.alarmCellDict[self.nrows] = newItem
self.setItem(self.nrows, 3, self.alarmCellDict[self.nrows])
# abstractAlarm.alarmExpired.connect(lambda i = self.alarmCellDict[self.nrows], j = abstractAlarm: self.update_duration(i,j))
self.timer.timeout.connect(
lambda i=self.alarmCellDict[self.nrows], j=abstractAlarm: self.update_duration(
i, j
)
)
#if self.nrows == 1:
self.resizeColumnsToContents()
self.nrows = self.nrows + 1
self.setSortingEnabled(True)
def update_duration(self, cellItem, abstractAlarm):
cellItem.setText(str(abstractAlarm.duration))
#!/usr/bin/env python3
"""
tab_alarms.py
"""
__author__ = ["Benjamin Mummery", "Tiago Sarmento"]
__credits__ = ["Benjamin Mummery", "Dónal Murray", "Tim Powell", "Tiago Sarmento"]
__license__ = "GPL"
__version__ = "0.0.1"
__maintainer__ = "Tiago Sarmento"
__email__ = "tiago.sarmento@stfc.ac.uk"
__status__ = "Prototype"
import sys
#from alarm_widgets.alarm_popup import alarmPopup, abstractAlarm
from alarm_widgets.alarm_table import alarmTable
from PySide2 import QtCore, QtGui, QtWidgets
class TabAlarmTable(QtWidgets.QWidget):
def __init__(self, NativeUI, *args, **kwargs):
super(TabAlarmTable, self).__init__(*args, **kwargs)
self.NativeUI = NativeUI
self.table = alarmTable(NativeUI)
vlayout = QtWidgets.QVBoxLayout()
vlayout.addWidget(self.table)
self.acknowledgeButton = QtWidgets.QPushButton('table button')
#self.acknowledgeButton.pressed.connect(self.acknowledge_pressed)
vlayout.addWidget(self.acknowledgeButton)
self.setLayout(vlayout)
\ No newline at end of file
#!/usr/bin/env python3
"""
tab_alarms.py
"""
__author__ = ["Benjamin Mummery", "Tiago Sarmento"]
__credits__ = ["Benjamin Mummery", "Dónal Murray", "Tim Powell", "Tiago Sarmento"]
__license__ = "GPL"
__version__ = "0.0.1"
__maintainer__ = "Tiago Sarmento"
__email__ = "tiago.sarmento@stfc.ac.uk"
__status__ = "Prototype"
import sys
from alarm_widgets.alarm_popup import alarmPopup, abstractAlarm
from alarm_widgets.alarm_list import alarmList
from PySide2 import QtCore, QtGui, QtWidgets
class TabAlarm(QtWidgets.QWidget):
def __init__(self, NativeUI, *args, **kwargs):
super(TabAlarm, self).__init__(*args, **kwargs)
self.NativeUI = NativeUI
# self.alarmDict = {}
self.popup = alarmPopup(NativeUI, self)
self.popup.show()
self.list = alarmList(NativeUI)
vlayout = QtWidgets.QVBoxLayout()
vlayout.addWidget(self.list)
self.acknowledgeButton = QtWidgets.QPushButton()
self.acknowledgeButton.pressed.connect(self.acknowledge_pressed)
vlayout.addWidget(self.acknowledgeButton)
self.setLayout(vlayout)
self.alarmDict = {}
# fdd
# self.timer = QtCore.QTimer()
# self.timer.setInterval(160)
# self.timer.timeout.connect(self.updateAlarms)
# self.timer.start()
def acknowledge_pressed(self):
self.popup.clearAlarms()
self.list.acknowledge_all()
def update_alarms(self):
newAlarmPayload = self.NativeUI.get_db("alarms")
if newAlarmPayload == {}:
return
if newAlarmPayload["alarm_code"] in self.alarmDict:
a = 1
self.alarmDict[newAlarmPayload["alarm_code"]].resetTimer()
self.alarmDict[newAlarmPayload["alarm_code"]].calculateDuration()
else:
newAbstractAlarm = abstractAlarm(self.NativeUI, newAlarmPayload)
self.alarmDict[newAlarmPayload["alarm_code"]] = newAbstractAlarm
newAbstractAlarm.alarmExpired.connect(
lambda i=newAbstractAlarm: self.handleAlarmExpiry(i)
)
self.popup.addAlarm(newAbstractAlarm)
self.list.addAlarm(newAbstractAlarm)
self.NativeUI.widgets.alarm_table_tab.table.addAlarmRow(newAbstractAlarm)
def handleAlarmExpiry(self, abstractAlarm):
abstractAlarm.freezeTimer()
self.popup.removeAlarm(abstractAlarm)
self.list.removeAlarm(abstractAlarm)
self.alarmDict.pop(abstractAlarm.alarmPayload["alarm_code"])
abstractAlarm.recordFinishTime()
#!/usr/bin/env python3
"""
tab_clinical.py
"""
__author__ = ["Benjamin Mummery", "Tiago Sarmento"]
__credits__ = ["Benjamin Mummery", "Dónal Murray", "Tim Powell", "Tiago Sarmento"]
__license__ = "GPL"
__version__ = "0.0.1"
__maintainer__ = "Tiago Sarmento"
__email__ = "tiago.sarmento@stfc.ac.uk"
__status__ = "Prototype"
from PySide2 import QtWidgets, QtGui, QtCore
from global_widgets.template_set_values import TemplateSetValues
class TabClinical(TemplateSetValues):
def __init__(self, *args, **kwargs):
super(TabClinical, self).__init__(*args, **kwargs)
clinicalList = [
["APNEA", "ms", ""],
["Check Pressure Patient", "ms", ""],
["High FIO2", "ms", ""],
["High Pressure_Low", " ", ""],
["High Respiratory Rate", " ", ""],
["High VTE", " ", ""],
["Low VTE", " ", ""],
["High VTI", " ", ""],
["Low VTI", " ", ""],
["Low FIO2", " ", ""],
["Occlusion_Low", " ", ""],
["High PEEP", " ", ""],
["Low PEEP", " ", ""],
]
self.addSpinDblCol(clinicalList)
self.addButtons()
self.finaliseLayout()
{
"settings":[
[["APNEA", "ms", "APNEA", "SET_THRESHOLD_MIN", "APNEA", 5, 20, 10, 1, 0]],
[["Check Pressure Patient", "ms", "CHECK_P_PATIENT", "SET_THRESHOLD_MIN", "CHECK_P_PATIENT"],["Check Pressure Patient", "ms", "CHECK_P_PATIENT", "SET_THRESHOLD_MAX", "CHECK_P_PATIENT"]],
[["FIO2", "%", "HIGH_FIO2", "SET_THRESHOLD_MIN", "HIGH_FIO2", -10, 0, -5, 1, 0],["Percentage O2", "", "fiO2_percent", "SET_TARGET_CURRENT", "FIO2_PERCENT", 0, 100, 51, 1, 0],["FIO2", "%", "HIGH_FIO2", "SET_THRESHOLD_MAX", "HIGH_FIO2", 0, 10, 5, 1, 0]],
[["Pressure", " ", "HIGH_PRESSURE", "SET_THRESHOLD_MIN", "HIGH_PRESSURE"],["Inhale Pressure","","inspiratory_pressure","SET_TARGET_CURRENT","INSPIRATORY_PRESSURE", 10, 50, 17, 1, 0],["Pressure", " ", "HIGH_PRESSURE", "SET_THRESHOLD_MAX", "HIGH_PRESSURE"],["Pressure", " ", "HIGH_PRESSURE", "SET_THRESHOLD_MAX", "HIGH_PRESSURE"]],
[["Respiratory Rate", " ", "HIGH_RR", "SET_THRESHOLD_MIN", "HIGH_RR", -10, 0, -5, 0.1, 1],["Respiratory Rate","/min","respiratory_rate","SET_TARGET_CURRENT","RESPIRATORY_RATE", 10, 20, 15, 0.1, 1],["Respiratory Rate", " ", "HIGH_RR", "SET_THRESHOLD_MAX", "HIGH_RR", 0, 10, 5, 0.1, 1]],
[["VTE", " ", "HIGH_VTE", "SET_THRESHOLD_MIN", "HIGH_VTE", -10, 0, -5, 1, 0],["Inhale Volume", "", "volume", "SET_TARGET_CURRENT", "VOLUME", 200, 800, 400, 20, 0],["VTE", " ", "HIGH_VTE", "SET_THRESHOLD_MAX", "HIGH_VTE",0, 10, 5, 1, 0]],
[["VTI", " ", "HIGH_VTI", "SET_THRESHOLD_MIN", "HIGH_VTI", -10, 0, -5, 1, 0],["VTI", " ", "HIGH_VTI", "SET_THRESHOLD_MAX", "HIGH_VTI",0, 10, 5, 1, 0]],
[["Occlusion", " ", "OCCLUSION","SET_THRESHOLD_MIN", "OCCLUSION", 5, 20, 15, 1, 0]],
[["PEEP", " ", "HIGH_PEEP","SET_THRESHOLD_MIN", "HIGH_PEEP", -2, 0, -2, 1, 0],["PEEP","cm h2o","peep","SET_TARGET_CURRENT","PEEP", 0, 100, 15, 0.1, 1],["PEEP", " ", "HIGH_PEEP","SET_THRESHOLD_MAX", "HIGH_PEEP",0, 2, 2, 1, 0]]
],
"SingleThresholds": ["APNEA", "Occlusion"],
"AbsoluteLimits": ["Percentage O2", "FIO2", "PEEP"]
}
\ No newline at end of file
{
"settings":[
["APNEA", "ms", "APNEA", "SET_THRESHOLD_MIN", "APNEA"],
["Check Pressure Patient", "ms", "CHECK_P_PATIENT", "SET_THRESHOLD_MIN", "CHECK_P_PATIENT"],
["FIO2", "ms", "HIGH_FIO2", "SET_THRESHOLD_MIN", "HIGH_FIO2"],
["High Pressure", " ", "HIGH_PRESSURE", "SET_THRESHOLD_MIN", "HIGH_PRESSURE"],
["High Respiratory Rate", " ", "HIGH_RR", "SET_THRESHOLD_MIN", "HIGH_RR"],
["High VTE", " ", "HIGH_VTE", "SET_THRESHOLD_MAX", "HIGH_VTE"],
["Low VTE", " ", "LOW_VTE", "SET_THRESHOLD_MIN", "LOW_VTE"],
["High VTI", " ", "HIGH_VTI", "SET_THRESHOLD_MAX", "HIGH_VTI"],
["Low VTI", " ", "LOW_VTI", "SET_THRESHOLD_MIN", "LOW_VTI"],
["Low FIO2", " ", "LOW_FIO2", "SET_THRESHOLD_MIN", "LOW_FIO2"],
["Occlusion", " ", "OCCLUSION","SET_THRESHOLD_MIN", "OCCLUSION"],
["PEEP", " ", "HIGH_PEEP","SET_THRESHOLD_MAX", "HIGH_PEEP"],
["Low PEEP", " ", "LOW_PEEP","SET_THRESHOLD_MIN", "LOW_PEEP"] ],
"HighLowLimits": ["High Pressure", "Occlusion"]
}
\ No newline at end of file
{
"page_background": [30, 30, 30],
"page_foreground": [200, 200, 200],
"button_background_enabled":[50, 50, 50],
"button_background_disabled":[100, 100, 100],
"button_foreground_enabled":[200, 200, 200],
"button_foreground_disabled":[30, 30, 30],
"button_background_highlight":[30,93,248],
"button_foreground_highlight":[200,200,200],
"label_background":[0, 0, 0],
"label_foreground":[200, 200, 200],
"display_background":[200, 200, 200],
"display_foreground":[0, 0, 0],
"display_foreground_changed":[0, 200, 0],
"display_foreground_red":[200, 0, 0],
"plot_pressure":[0, 114, 178],
"plot_volume":[0, 158, 115],
"plot_flow":[240, 228, 66],
"plot_pressure_flow":[230, 159, 0],
"plot_flow_volume":[204, 121, 167],
"plot_volume_pressure":[86, 180, 233],
"highligh":[30,93,248],
"baby_blue":[144, 231, 211],
"red":[200, 0, 0],
"green":[0, 200, 0]
}
{
"Buffers": [
[
"Calibration",
"ms",
"duration_calibration",
"SET_DURATION",
"CALIBRATION",
0,
1000,
50,
0
],
["Purge", "ms", "duration_buff_purge", "SET_DURATION", "BUFF_PURGE"],
["Flush", "ms", "duration_buff_flush", "SET_DURATION", "BUFF_FLUSH"],
[
"Pre-fill",
"ms",
"duration_buff_prefill",
"SET_DURATION",
"BUFF_PREFILL"
],
["Fill", "ms", "duration_buff_prefill", "SET_DURATION", "BUFF_FILL"],
[
"Pre-inhale",
"ms",
"duration_buff_pre_inhale",
"SET_DURATION",
"BUFF_PRE_INHALE"
]
],
"PID": [
["KP", "", "kp", "SET_PID", "KP"],
["KI", "", "ki", "SET_PID", "KI"],
["KD", "", "kd", "SET_PID", "KD"],
["PID Gain", "", "pid_gain", "SET_PID", "PID_GAIN"],
[
"Max. PP",
"",
"max_patient_pressure",
"SET_PID",
"MAX_PATIENT_PRESSURE"
]
],
"Valves": [
["Air in", "", "valve_air_in"],
["O2 in", "", "valve_o2_in"],
["Inhale", "", "valve_inhale"],
["Exhale", "", "valve_exhale"],
["Purge valve", "", "valve_purge"],
["Inhale Opening", "%", "valve_inhale_percent"],
["Exhale Opening", "%", "valve_exhale_percent"]
],
"Breathing": [
["Inhale", "ms", "duration_inhale", "SET_DURATION", "INHALE"],
["Pause", "ms", "duration_pause", "SET_DURATION", "PAUSE"],
["Exhale fill", "ms", "duration_exhale", "SET_DURATION", "EXHALE_FILL"],
["Exhale", "ms", "duration_exhale", "SET_DURATION", "EXHALE"],
["I:E Ratio", "", "inhale_exhale_ratio"]
]
}
\ No newline at end of file
{"settings":[
["Respiratory Rate","/min","respiratory_rate","SET_TARGET_","RESPIRATORY_RATE", 0, 20, 15, 0.1, 1],
["PEEP","cm h2o","peep","SET_TARGET_","PEEP", 0, 100, 15, 0.1, 1],
["Inhale Time", "s", "inhale_time", "SET_TARGET_", "INHALE_TIME", 0, 20, 1, 0.1, 1],
["IE Ratio", "", "ie_ratio", "SET_TARGET_", "IE_RATIO", 0, 1, 0.338, 0.001, 3],
["Inhale Trigger Sensitivity","","inhale_trigger_threshold","SET_TARGET_","INHALE_TRIGGER_THRESHOLD", 0, 20, 5, 0.2, 1],
["Exhale Trigger Sensitivity","","exhale_trigger_threshold","SET_TARGET_","EXHALE_TRIGGER_THRESHOLD", 0, 50, 25, 0.2, 1],
["Inhale Pressure","","inspiratory_pressure","SET_TARGET_","INSPIRATORY_PRESSURE", 10, 50, 17, 1, 0],
["Inhale Volume", "", "volume", "SET_TARGET_", "VOLUME", 200, 800, 400, 20, 0],
["Percentage O2", "", "fiO2_percent", "SET_TARGET_", "FIO2_PERCENT", 20, 100, 51, 0.1, 1]],
"radioSettings": ["Inhale Time", "IE Ratio"],
"enableDict":{"PC/AC":[1, 1,0, 1, 1, 0, 1, 0, 1], "PC/AC-PRVC":[1, 1,1, 0, 1, 0, 1, 1, 1], "PC-PSV":[1, 1,1, 0, 1, 0, 1, 0, 1], "CPAP":[1, 1,0, 1, 1, 0, 1, 0, 1]},
"mainPageSettings": ["Inhale Pressure", "Respiratory Rate", "Inhale Time", "IE Ratio", "Percentage O2", "PEEP" ]
}
\ No newline at end of file
{"settings":[
["Name", "/min", "name", "SET_PERSONAL", "NAME", "Goedkoop Van Tilator"],
["Patient ID", "s", "patient_id", "SET_PERSONAL", "PATIENT_ID", "11235813FIB"],
["Age", "years", "age", "SET_PERSONAL", "AGE", 0, 130, 25, 1, 0],
["Sex", "", "sex", "SET_PERSONAL", "SEX", "X"],
["Weight", "kg", "weight", "SET_PERSONAL", "WEIGHT", 20, 250, 60, 1, 0],
["Height", "cms", "height", "SET_PERSONAL", "HEIGHT",20, 250, 160, 1, 0]
],
"textBoxes": ["Name", "Patient ID", "Sex"]
}
\ No newline at end of file
{"calibration": {"label": "calibration", "last_performed": 1622034526, "cmd_code": "calib_rate"}, "leak_test": {"label": "Leak Test", "last_performed": 1622034527, "cmd_code": "leak_test"}, "maintenance": {"label": "maintenance", "last_performed": 1622034528, "cmd_code": "main_tenance"}}
\ No newline at end of file
{
"language_name": "English",
"start_button": "START",
"stop_button": "STOP",
"standby_button": "STANDBY",
"PC/AC": "PC/AC",
"PC/AC-PRVC": "PC/AC-PRVC",
"PC-PSV": "PC-PSV",
"CPAP": "CPAP",
"plot_axis_label_pressure": "Pressure [cmH<sub>2</sub>O]",
"plot_axis_label_flow": "Flow [L/min]",
"plot_axis_label_volume": "Volume [mL]",
"plot_axis_label_time": "Time [s]",
"plot_line_label_pressure": "Airway Pressure",
"plot_line_label_flow": "Flow",
"plot_line_label_volume": "Volume",
"plot_line_label_pressure_flow": "Airway Pressure - Flow",
"plot_line_label_flow_volume": "Flow - Volume",
"plot_line_label_volume_pressure": "Volume - Airway Pressure",
"layout_label_measurements": "Measurements",
"button_label_main_normal": "Normal",
"button_label_main_detailed": "Detailed",
"button_label_alarms_list": "List of Alarms",
"button_label_alarms_table": "Alarm Table",
"button_label_alarms_clinical": "Clinical Limits",
"button_label_settings_expert": "Expert",
"button_label_settings_charts": "Charts",
"button_label_settings_info": "System Info",
"button_label_modes_mode": "Mode Settings",
"button_label_modes_personal": "Personal Settings",
"button_label_modes_summary": "Summary",
"ui_window_title": "HEV NativeUI v{version}",
"measurement_label_plateau_pressure": "P<sub>PLATEAU</sub> [cmH<sub>2</sub>O]",
"measurement_label_respiratory_rate": "RR",
"measurement_label_fio2_percent": "FIO<sub>2</sub> [%]",
"measurement_label_exhaled_tidal_volume": "VTE [mL]",
"measurement_label_exhaled_minute_volume": "MVE [<sup>L</sup>/<sub>min</sub>]",
"measurement_label_peep": "PEEP [cmH<sub>2</sub>O]",
"measurement_label_inhale_exhale_ratio": "I:E",
"measurement_label_mean_airway_pressure": "P<sub>MEAN</sub> [cmH<sub>2</sub>O]",
"measurement_label_inhaled_tidal_volume": "VTI [mL]",
"measurement_label_inhaled_minute_volume": "MVI [L/min]",
"measurement_label_peak_inspiratory_pressure": "P<sub>PEAK</sub> [cmH<sub>2</sub>O]",
"spin_box_label_Inhale_Pressure": "Inhale Pressure",
"spin_box_label_Respiratory_Rate": "Respiratory Rate",
"spin_box_label_Inhale_Time": "Inhale Time",
"spin_box_label_IE_Ratio": "IE Ratio",
"spin_box_label_Percentage_O2": "Percentage O2",
"personal_tab_name": "Name",
"personal_tab_patientid": "Patient ID",
"personal_tab_age": "Age",
"personal_tab_sex": "Sex",
"personal_tab_weight": "Weight",
"personal_tab_height": "Height"
}
{
"language_name": "Portugues",
"start_button": "INICIAR",
"stop_button": "PARAR",
"standby_button": "ESPERAR",
"PC/AC": "PC/AC",
"PC/AC-PRVC": "PC/AC-PRVC",
"PC-PSV": "PC-PSV",
"CPAP": "CPAP",
"plot_axis_label_pressure": "Pressao [cmH<sub>2</sub>O]",
"plot_axis_label_flow": "Fluxo [L/min]",
"plot_axis_label_volume": "Volume [mL]",
"plot_axis_label_time": "Tempo [s]",
"plot_line_label_pressure": "Pressao de Ar",
"plot_line_label_flow": "Fluxo",
"plot_line_label_volume": "Volume",
"plot_line_label_pressure_flow": "Pressao de Ar - Fluxo",
"plot_line_label_flow_volume": "Fluxo - Volume",
"plot_line_label_volume_pressure": "Volume - Pressao de Ar",
"layout_label_measurements": "Medicoes",
"button_label_main_normal": "Normal",
"button_label_main_detailed": "Detalhado",
"button_label_alarms_list": "-",
"button_label_alarms_table": "-",
"button_label_alarms_clinical": "-",
"button_label_settings_expert": "-",
"button_label_settings_charts": "-",
"button_label_settings_info": "-",
"button_label_modes_mode": "-",
"button_label_modes_personal": "-",
"button_label_modes_summary": "-",
"ui_window_title": "HEV NativeUI v{version}",
"measurement_label_plateau_pressure": "P<sub>Plato</sub> [cmH<sub>2</sub>O]",
"measurement_label_respiratory_rate": "FREQ<sub>RESP</sub>",
"measurement_label_fio2_percent": "FIO<sub>2</sub> [%]",
"measurement_label_exhaled_tidal_volume": "VOL<sub>EXAL</sub> [mL]",
"measurement_label_exhaled_minute_volume": "MVE [<sup>L</sup>/<sub>min</sub>]",
"measurement_label_peep": "PEEP [cmH<sub>2</sub>O]",
"measurement_label_inhale_exhale_ratio": "I:E",
"measurement_label_mean_airway_pressure": "P<sub>MEDIO</sub> [cmH<sub>2</sub>O]",
"measurement_label_inhaled_tidal_volume": "VTI [mL]",
"measurement_label_inhaled_minute_volume": "MVI [L/min]",
"measurement_label_peak_inspiratory_pressure": "P<sub>PICO</sub> [cmH<sub>2</sub>O]",
"spin_box_label_Inhale_Pressure": "Pressao inspiracao",
"spin_box_label_Respiratory_Rate": "Frequencia Respiratoria",
"spin_box_label_Inhale_Time": "Tempo Inspiracao",
"spin_box_label_IE_Ratio": "Razao I/E",
"spin_box_label_Percentage_O2": "Porcetagem O<sub>2</sub>",
"personal_tab_name": "Nome",
"personal_tab_patientid": "ID do paciente",
"personal_tab_age": "Idade",
"personal_tab_sex": "Sexo",
"personal_tab_weight": "Peso",
"personal_tab_height": "Altura"
}
#!/usr/bin/env python3
"""
global_select_button.py
"""
__author__ = ["Benjamin Mummery", "Tiago Sarmento"]
__credits__ = ["Benjamin Mummery", "Dónal Murray", "Tim Powell", "Tiago Sarmento"]
__license__ = "GPL"
__version__ = "0.0.1"
__maintainer__ = "Tiago Sarmento"
__email__ = "tiago.sarmento@stfc.ac.uk"
__status__ = "Prototype"
from PySide2 import QtWidgets, QtGui, QtCore
class selectorButton(QtWidgets.QPushButton):
"""A button styled with two colour options, to use for tab selection"""
def __init__(self, NativeUI, *args, **kwargs):
super(selectorButton, self).__init__(*args, **kwargs)
self.setFont(NativeUI.text_font)
style = (
"QPushButton[selected='0']{"
" color: " + NativeUI.colors["page_foreground"].name() + ";"
" background-color: "
+ NativeUI.colors["button_background_enabled"].name()
+ ";"
" border:none"
"}"
"QPushButton[selected='1']{"
" color: " + NativeUI.colors["page_background"].name() + ";"
" background-color:"
+ NativeUI.colors["button_foreground_disabled"].name()
+ ";"
" border:none"
"}"
)
self.setStyleSheet(style)
self.setProperty("selected", "0")
#!/usr/bin/env python3
"""
global_send_popup.py
"""
__author__ = ["Benjamin Mummery", "Tiago Sarmento"]
__credits__ = ["Benjamin Mummery", "Dónal Murray", "Tim Powell", "Tiago Sarmento"]
__license__ = "GPL"
__version__ = "0.0.1"
__maintainer__ = "Tiago Sarmento"
__email__ = "tiago.sarmento@stfc.ac.uk"
__status__ = "Prototype"
from PySide2 import QtWidgets, QtGui, QtCore
from widget_library.ok_cancel_buttons_widget import OkButtonWidget, CancelButtonWidget
from widget_library.expert_handler import ExpertHandler
from mode_widgets.personal_handler import PersonalHandler
from mode_widgets.mode_handler import ModeHandler
from mode_widgets.clinical_handler import ClinicalHandler
import logging
# from global_widgets.global_ok_cancel_buttons import okButton, cancelButton
import sys
import os
class SetConfirmPopup(QtWidgets.QDialog):
"""Popup called when user wants to send new values to microcontroller.
This popup shows changes and asks for confirmation"""
# a signal for each handler, so the UI knows which values were updated
ExpertSend = QtCore.Signal()
ModeSend = QtCore.Signal()
PersonalSend = QtCore.Signal()
ClinicalSend = QtCore.Signal()
def __init__(self, NativeUI, *args, **kwargs):
super().__init__(*args, **kwargs)
self.NativeUI = NativeUI
self.handler = None
# list widget displays the changes to be sent to MCU in human readable way
self.listWidget = QtWidgets.QListWidget()
self.listWidget.setVerticalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOff)
self.listWidget.setHorizontalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOff)
buttonHLayout = QtWidgets.QHBoxLayout()
self.okButton = OkButtonWidget(self.NativeUI)
self.okButton.setEnabled(True)
self.okButton.pressed.connect(self.ok_button_pressed)
buttonHLayout.addWidget(self.okButton)
self.cancelButton = CancelButtonWidget(self.NativeUI)
self.cancelButton.setEnabled(True)
buttonHLayout.addWidget(self.cancelButton)
vlayout = QtWidgets.QVBoxLayout()
vlayout.addWidget(self.listWidget)
vlayout.addLayout(buttonHLayout)
self.setLayout(vlayout)
def populatePopup(self, handlerWidget, messageList):
"""One popup is used for all the handlers. It is populated when called by a particular handler"""
self.handler = handlerWidget
self.listWidget.clear()
if messageList == []:
messageList = ["no values were set"]
for item in messageList:
listItem = QtWidgets.QListWidgetItem(item)
listItem.setFlags(QtCore.Qt.NoItemFlags)
self.listWidget.addItem(listItem)
# adjust size according to list contents
self.listWidget.setFixedHeight(
self.listWidget.sizeHintForRow(0) * self.listWidget.count() + 10
)
self.listWidget.setFixedWidth(
self.listWidget.sizeHintForColumn(0) * self.listWidget.count()
)
self.listWidget.update()
self.update()
return 0
def ok_button_pressed(self):
"""Emit signal to connect with handler corresponding to editted values."""
if self.handler is None:
logging.error("Popup ok_button_pressed called before popupatePopup")
return 1
if isinstance(self.handler, ExpertHandler):
self.ExpertSend.emit()
elif isinstance(self.handler, ModeHandler):
self.ModeSend.emit()
elif isinstance(self.handler, PersonalHandler):
self.PersonalSend.emit()
elif isinstance(self.handler, ClinicalHandler):
self.ClinicalSend.emit()
else:
logging.warning("Unrecognised handler type: %s", type(self.handler))
return 0
# def cancel_button_pressed(self):
# """Close popup when cancel button is clicked"""
# print("CANCEL BUTTON PRESSED")
# # self.close()
# return 0
class confirmWidget(QtWidgets.QWidget):
"""A widget displaying an individual command confirmation from the MCU. Is contained in confirmPopup"""
def __init__(self, NativeUI, confirmMessage, *args, **kwargs):
super(confirmWidget, self).__init__(*args, **kwargs)
self.hlayout = QtWidgets.QHBoxLayout()
self.hlayout.setSpacing(0)
self.hlayout.setMargin(0)
self.confirmMessage = confirmMessage
iconLabel = QtWidgets.QLabel()
iconpath_check = os.path.join(NativeUI.iconpath, "exclamation-circle-solid.png")
pixmap = QtGui.QPixmap(iconpath_check).scaledToHeight(40)
iconLabel.setPixmap(pixmap)
self.hlayout.addWidget(iconLabel)
textLabel = QtWidgets.QLabel()
textLabel.setText(self.confirmMessage)
textLabel.setFixedHeight(40)
textLabel.setFixedWidth(400)
textLabel.setAlignment(QtCore.Qt.AlignCenter)
self.hlayout.addWidget(textLabel)
self.setLayout(self.hlayout)
self.setFixedHeight(50)
# create timer to handle timeout
self.timer = QtCore.QTimer()
self.timer.setInterval(10000)
self.timer.timeout.connect(self.confirmTimeout)
self.timer.start()
def confirmTimeout(self):
"""Widget should expire after a defined time"""
self.parent().confirmDict.pop(
self.confirmMessage.replace("/", "_").replace("-", "_") # - and / are not used in dictionary keys
)
self.setParent(None) # delete self
class confirmPopup(QtWidgets.QDialog):
"""Popup when a command is confirmed by microcontroller.
This popup is a frame containing a confirmWidget object for
each successful command."""
def __init__(self, NativeUI, *args, **kwargs):
super(confirmPopup, self).__init__(*args, **kwargs)
self.NativeUI = NativeUI
self.confirmDict = {}
self.vlayout = QtWidgets.QVBoxLayout()
self.vlayout.setSpacing(0)
self.vlayout.setMargin(0)
self.setLayout(self.vlayout)
self.setStyleSheet("background-color:green;")
self.location_on_window()
self.setWindowFlags(
QtCore.Qt.FramelessWindowHint
| QtCore.Qt.Dialog
| QtCore.Qt.WindowStaysOnTopHint
) # no window title
self.timer = QtCore.QTimer()
self.timer.setInterval(500)
self.timer.timeout.connect(self.adjustSize) # container needs to adjust to a new number of confirmWidgets
self.timer.start()
def addConfirmation(self, confirmMessage):
"""Add a confirmation to the popup. Triggered when UI receives a confirmation from the microcontroller"""
self.confirmDict[confirmMessage] = confirmWidget(
self.NativeUI, confirmMessage
) # record in dictionary so it can be accessed and deleted
self.vlayout.addWidget(self.confirmDict[confirmMessage])
return 0
def location_on_window(self):
"""Places confirmWidgets at the top center of the screen"""
screen = QtWidgets.QDesktopWidget().screenGeometry()
# widget = self.geometry()
x = screen.width() - screen.width() / 2
y = 0 # screen.height() - widget.height()
self.move(x, y)
return 0
#!/usr/bin/env python3
"""
global_spinbox.py
"""
__author__ = ["Benjamin Mummery", "Tiago Sarmento"]
__credits__ = ["Benjamin Mummery", "Dónal Murray", "Tim Powell", "Tiago Sarmento"]
__license__ = "GPL"
__version__ = "0.0.1"
__maintainer__ = "Tiago Sarmento"
__email__ = "tiago.sarmento@stfc.ac.uk"
__status__ = "Prototype"
from PySide2 import QtWidgets, QtGui, QtCore
from global_widgets.global_typeval_popup import TypeValuePopup
from CommsCommon import ReadbackFormat
class signallingSpinBox(QtWidgets.QDoubleSpinBox):
"""the base class for all the spinboxes.
Additional functionality:
A popup to edit the value appears when the box is double clicked.
A signal is emitted when the user presses up or down buttons.
"""
manualChanged = QtCore.Signal()
programmaticallyChanged = QtCore.Signal()
def __init__(self, NativeUI, popup, label_text, min, max, initVal, step, decPlaces):
super().__init__()
self.lineEdit().installEventFilter(self)
self.editable = True
self.label_text, self.min, self.max, self.initVal, self.step, self.decPlaces = label_text, min, max, initVal, step, decPlaces
self.setRange(min, max)
self.setSingleStep(step)
self.setDecimals(decPlaces)
self.setValue(initVal)
self.NativeUI = NativeUI
#self.populateVals = [label_text, min, max, initVal, step, decPlaces]
self.popUp = popup# TypeValuePopup(NativeUI, label_text, min, max, initVal, step, decPlaces)
#self.popUp.okButton.clicked.connect(self.okButtonPressed)
#self.popUp.cancelButton.clicked.connect(self.cancelButtonPressed)
def setEditability(self, setBool):
self.editable = setBool
def okButtonPressed(self):
"""Ok button press applies changes in popup to the spin box, closes the popup, and emits a signal"""
val = float(self.popUp.lineEdit.text())
self.setValue(val)
self.popUp.close()
self.manualChanged.emit()
def cancelButtonPressed(self):
"""Cancel button press reverts changes and closes popup"""
self.popUp.lineEdit.setText(self.popUp.lineEdit.saveVal)
self.popUp.close()
def stepBy(self, step):
"""Overrides stepBy to store previous value and emit a signal when called"""
value = self.value()
self.prevValue = value
super(signallingSpinBox, self).stepBy(step)
if self.value() != value:
self.manualChanged.emit()
def set_value(self, value):
self.setValue(value)
self.programmaticallyChanged.emit()
def eventFilter(self, source, event):
"""Overrides event filter to implement response to double click """
if (
source is self.lineEdit()
and event.type() == QtCore.QEvent.MouseButtonDblClick
):
if not self.editable:
return
#self.popUp.lineEdit.setText(str(self.value()))
#self.popUp.lineEdit.setFocus()
self.popUp.populatePopup(self,self.NativeUI.display_stack.currentWidget())
self.NativeUI.display_stack.setCurrentWidget(self.popUp)
#self.popUp.show()
return True
return False
class labelledSpin(QtWidgets.QWidget):
"""Combines signalling spin box with information relevant to its layout and to handle value updates.
It is created by an information array which indicates labels, units, command type and code for value setting,
and the range of permitted values"""
def __init__(self, NativeUI, popup, infoArray, *args, **kwargs):
super(labelledSpin, self).__init__(*args, **kwargs)
self.NativeUI = NativeUI
self.cmd_type, self.cmd_code = "", ""
self.min, self.max, self.step = 0, 10000, 0.3
self.initVal = 1
self.currentDbValue = self.initVal
self.decPlaces = 2
self.label = "default"
if len(infoArray) == 10:
self.label, self.units, self.tag, self.cmd_type, self.cmd_code, self.min, self.max, self.initVal, self.step, self.decPlaces = (
infoArray
)
if len(infoArray) == 9:
self.label, self.units, self.tag, self.cmd_type, self.cmd_code, self.min, self.max, self.step, self.decPlaces = (
infoArray
)
elif len(infoArray) == 5:
self.label, self.units, self.tag, self.cmd_type, self.cmd_code = infoArray
elif len(infoArray) == 3:
self.label, self.units, self.tag = infoArray
self.manuallyUpdated = False
self.layout = QtWidgets.QHBoxLayout()
textStyle = "color:white;"
# if self.label != "":
self.nameLabel = QtWidgets.QLabel(self.label)
self.nameLabel.setStyleSheet(textStyle)
self.nameLabel.setFont(NativeUI.text_font)
self.nameLabel.setAlignment(QtCore.Qt.AlignRight | QtCore.Qt.AlignVCenter)
self.simpleSpin = signallingSpinBox(NativeUI, popup, self.label, self.min, self.max, self.initVal, self.step, self.decPlaces)
# self.simpleSpin.setRange(self.min, self.max)
# self.simpleSpin.setSingleStep(self.step)
# self.simpleSpin.setDecimals(self.decPlaces)
# self.simpleSpin.setValue(self.initVal)
self.simpleSpin.setStyleSheet(
"QDoubleSpinBox{"
" width:100px;" # TODO: unhardcode
"}"
"QDoubleSpinBox[bgColour='1']{"
" background-color:" + NativeUI.colors["page_foreground"].name() + ";"
"}"
"QDoubleSpinBox[bgColour='0']{"
" background-color:" + NativeUI.colors["page_background"].name() + ";"
"}"
"QDoubleSpinBox[textColour='1']{"
" color:" + NativeUI.colors["page_background"].name() + ";"
"}"
"QDoubleSpinBox[textColour='0']{"
" color:" + NativeUI.colors["page_foreground"].name() + ";"
"}"
"QDoubleSpinBox[textColour='2']{"
" color:" + NativeUI.colors["red"].name() + ";"
"}"
"QDoubleSpinBox::up-button{"
"width:20; "
"}"
"QDoubleSpinBox::down-button{"
"width:20px;"
"height:20px; "
"}"
)
self.simpleSpin.setFont(NativeUI.text_font)
self.simpleSpin.setProperty("textColour", "1")
self.simpleSpin.setProperty("bgColour", "1")
self.simpleSpin.setButtonSymbols(
QtWidgets.QAbstractSpinBox.ButtonSymbols.PlusMinus
)
self.simpleSpin.setAlignment(QtCore.Qt.AlignCenter)
if self.cmd_type == "":
self.simpleSpin.setReadOnly(True)
# self.simpleSpin.setProperty("bgColour", "1")
# self.simpleSpin.setProperty("textColour", "2")
self.simpleSpin.setEditability(False)
self.simpleSpin.style().polish(self.simpleSpin)
self.unitLabel = QtWidgets.QLabel(self.units)
self.unitLabel.setStyleSheet(textStyle)
self.unitLabel.setAlignment(QtCore.Qt.AlignLeft | QtCore.Qt.AlignVCenter)
self.widgetList = [self.nameLabel, self.simpleSpin, self.unitLabel]
for widget in self.widgetList:
self.layout.addWidget(widget)
self.setLayout(self.layout)
self.simpleSpin.manualChanged.connect(self.manualStep)
self.simpleSpin.programmaticallyChanged.connect(self.manualStep)
# self.simpleSpin.valueChanged.connect(self.valChange)
def manualStep(self):
"""Handle changes in value. Change colour if different to set value, set updating values."""
if self.manuallyUpdated:
roundVal = round(self.currentDbValue, self.decPlaces)
if self.decPlaces == 0:
roundVal = int(roundVal)
if self.simpleSpin.value() == roundVal:
self.simpleSpin.setProperty("textColour", "1")
self.manuallyUpdated = False
self.simpleSpin.style().polish(self.simpleSpin)
else:
self.simpleSpin.setProperty("textColour", "2")
self.manuallyUpdated = True
self.simpleSpin.style().polish(self.simpleSpin)
return 0
def setEnabled(self, bool):
self.simpleSpin.setEnabled(bool)
self.simpleSpin.setProperty("bgColour", str(int(bool)))
self.simpleSpin.setProperty("textColour", str(int(bool)))
self.simpleSpin.style().polish(self.simpleSpin)
def update_value(self, db):
if self.tag == "":
a = 1 # do nothing
else:
newVal = db[self.tag]
if self.manuallyUpdated:
a = 1 # do nothing
else:
self.currentDbValue = newVal
self.simpleSpin.setValue(newVal)
self.simpleSpin.setProperty("textColour", "1")
self.simpleSpin.style().polish(self.simpleSpin)
def insertWidget(self, widget, position):
self.insertedWidget = widget
self.widgetList.insert(position, widget)
for i in reversed(range(self.layout.count())):
self.layout.itemAt(i).widget().setParent(None)
# newLayout = QtWidgets.QHBoxLayout()
for widget in self.widgetList:
self.layout.addWidget(widget)
self.setLayout(self.layout)
def get_value(self):
return self.simpleSpin.value()
def set_value(self, value):
self.simpleSpin.setValue(value)
self.simpleSpin.setProperty("textColour", "1")
self.simpleSpin.style().polish(self.simpleSpin)
return 0
def set_maximum(self, max):
self.max = max
if self.simpleSpin.value() > self.max:
self.simpleSpin.set_value(max)
self.simpleSpin.setRange(self.min, self.max)
def set_minimum(self, min):
self.min = min
if self.simpleSpin.value() < self.min:
self.simpleSpin.stepBy(self.min - self.simpleSpin.value())
self.simpleSpin.setRange(self.min, self.max)
\ No newline at end of file
#!/usr/bin/env python3
"""
global_typeval_popup.py
"""
__author__ = ["Benjamin Mummery", "Tiago Sarmento"]
__credits__ = ["Benjamin Mummery", "Dónal Murray", "Tim Powell", "Tiago Sarmento"]
__license__ = "GPL"
__version__ = "0.0.1"
__maintainer__ = "Tiago Sarmento"
__email__ = "tiago.sarmento@stfc.ac.uk"
__status__ = "Prototype"
from PySide2 import QtCore, QtGui, QtWidgets
import os
os.environ["QT_IM_MODULE"] = "qtvirtualkeyboard"
from widget_library.ok_cancel_buttons_widget import OkButtonWidget, CancelButtonWidget
from widget_library.numpad_widget import NumberpadWidget, AlphapadWidget
class AbstractTypeValPopup(QtWidgets.QDialog):
"""Popup takes user input to put in spin box. """
okPressed = QtCore.Signal(str)
cancelPressed = QtCore.Signal()
correctPassword = QtCore.Signal()
def __init__(self, NativeUI, characterType, *args, **kwargs):
super().__init__(*args, **kwargs)
self.NativeUI = NativeUI
# self.label_text = label_text
# self.min, self.max, self.initVal, self.step, self.decPlaces = min, max, initVal, step, decPlaces
grid = QtWidgets.QGridLayout()
grid.setSpacing(1)
#self.label_text, self.min, self.max, self.initVal, self.step, self.decPlaces = 'Enter Password', 0, 10000, 0, 0, 0
self.setStyleSheet("border-radius:4px; background-color:black")
self.characterType = characterType
self.label = QtWidgets.QLabel() # self.label_text)
self.label.setFont(NativeUI.text_font)
self.label.setStyleSheet('color: ' + NativeUI.colors["page_foreground"].name())
self.lineEdit = QtWidgets.QLineEdit()
self.lineEdit.setText("4")
self.lineEdit.setStyleSheet(
"QLineEdit{"
" background-color: white;"
" border-radius: 4px;"
"}"
"QLineEdit[colour = '0']{"
" color: green;"
"}"
"QLineEdit[colour = '1']{"
" color: rgb(144, 231, 211);"
"}"
"QLineEdit[colour = '2']{"
" color: red;"
"}"
)
self.lineEdit.setFont(NativeUI.text_font)
self.lineEdit.setProperty("colour", "1")
self.lineEdit.setAlignment(QtCore.Qt.AlignCenter)
self.lineEdit.saveVal = '' # self.lineEdit.text()
hlayout = QtWidgets.QHBoxLayout()
if characterType == 'numeric':
self.lineEdit.setValidator(
QtGui.QDoubleValidator(0.0, 100.0, 5)
) # ensures only doubles can be typed, do
#self.lineEdit.textEdited.connect(self.setTextColour(1))
self.numberpad = NumberpadWidget(NativeUI)
self.numberpad.numberPressed.connect(self.handle_numberpress)
self.increaseButton = OkButtonWidget(NativeUI)
self.increaseButton.clicked.connect(self.increase_button_clicked)
self.increaseButton.setEnabled(True)
self.decreaseButton = CancelButtonWidget(NativeUI)
self.decreaseButton.clicked.connect(self.decrease_button_clicked)
self.decreaseButton.setEnabled(True)
hlayout.addWidget(self.decreaseButton)
hlayout.addWidget(self.lineEdit)
hlayout.addWidget(self.increaseButton)
if characterType == 'alpha':
self.numberpad = AlphapadWidget(NativeUI)
self.numberpad.numberPressed.connect(self.handle_alphapress)
hlayout.addWidget(self.lineEdit)
self.hlayout2 = QtWidgets.QHBoxLayout()
self.okButton = OkButtonWidget(NativeUI)
self.okButton.setEnabled(True)
self.hlayout2.addWidget(self.okButton)
self.cancelButton = CancelButtonWidget(NativeUI)
self.cancelButton.setEnabled(True)
self.hlayout2.addWidget(self.cancelButton)
vlayout = QtWidgets.QVBoxLayout()
vlayout.addWidget(self.label)
vlayout.addLayout(hlayout)
vlayout.addWidget(self.numberpad)
vlayout.addLayout((self.hlayout2))
self.setLayout(vlayout)
self.setWindowFlags(
QtCore.Qt.FramelessWindowHint | QtCore.Qt.WindowStaysOnTopHint
) # no window title
self.password = 'A'
def submit_password(self):
val = self.lineEdit.text()
if val == self.password:
self.correctPassword.emit()
self.lineEdit.setText('') # reset text whether or not password was successful
def handle_ok_press(self):
val = self.lineEdit.text()
if self.characterType == 'numeric':
val = float(val)
self.NativeUI.display_stack.setCurrentWidget(self.return_stack_widget)
if val != self.currentWidg.value():
self.currentWidg.setValue(val)
self.currentWidg.manualChanged.emit()
def populatePopup(self, currentWidg, return_widget):
self.currentWidg = currentWidg
self.return_stack_widget = return_widget
if self.characterType == 'numeric':
self.label_text, self.min, self.max, self.initVal, self.step, self.decPlaces = currentWidg.label_text, currentWidg.min, currentWidg.max, currentWidg.initVal, currentWidg.step, currentWidg.decPlaces
else:
self.label_text = currentWidg.label_text
self.label.setText(self.label_text)
self.lineEdit.setText('')#str(currentWidg.value()))
def handle_numberpress(self, symbol):
"""Handle number pad button press. Put button value in line edit text, and handle inputs
outside accepted range or number of decimal places. Handle backspace"""
oldText = self.lineEdit.text()
if symbol.isnumeric() or (symbol == '.'):
newText = oldText + symbol
if float(newText) > self.max:
newText = str(self.max)
elif float(newText) < self.min:
newText = str(self.min)
elif '.' in newText:
if len(newText.split('.')[1]) > self.decPlaces:
newText = oldText
self.lineEdit.setText(newText)
elif symbol == '<':
self.lineEdit.setText(oldText[0:-1])
def handle_alphapress(self, symbol):
"""Handle number pad button press. Put button value in line edit text, and handle inputs
outside accepted range or number of decimal places. Handle backspace"""
oldText = self.lineEdit.text()
if symbol.isalpha():
newText = oldText + symbol
self.lineEdit.setText(newText)
elif symbol == '<':
self.lineEdit.setText(oldText[0:-1])
def increase_button_clicked(self):
"""Handle increase step button click"""
currentVal = self.get_value()
newVal = round(float(currentVal) + self.step, self.decPlaces)
if newVal >= self.max:
newVal = self.max
self.lineEdit.setText(str(newVal))
return 0
def decrease_button_clicked(self):
"""Handle decrease step button click"""
currentVal = self.get_value()
newVal = round(float(currentVal) - self.step, self.decPlaces)
if newVal <= self.min:
newVal = self.min
self.lineEdit.setText(str(newVal))
return 0
def get_value(self):
return self.lineEdit.text()
class TypeValuePopup():
"""Popup takes user input to put in spin box. """
okPressed = QtCore.Signal(str)
cancelPressed = QtCore.Signal()
def __init__(self, NativeUI, keyboard, *args, **kwargs):
super().__init__(*args, **kwargs)
#self.label_text = label_text
#self.min, self.max, self.initVal, self.step, self.decPlaces = min, max, initVal, step, decPlaces
grid = QtWidgets.QGridLayout()
grid.setSpacing(1)
self.setStyleSheet("border-radius:4px; background-color:black")
self.label = QtWidgets.QLabel()#self.label_text)
self.label.setFont(NativeUI.text_font)
self.label.setStyleSheet('color: ' + NativeUI.colors["page_foreground"].name())
self.lineEdit = QtWidgets.QLineEdit()
self.lineEdit.setText("4")
self.lineEdit.setStyleSheet(
"QLineEdit{"
" background-color: white;"
" border-radius: 4px;"
"}"
"QLineEdit[colour = '0']{"
" color: green;"
"}"
"QLineEdit[colour = '1']{"
" color: rgb(144, 231, 211);"
"}"
"QLineEdit[colour = '2']{"
" color: red;"
"}"
)
self.lineEdit.setFont(NativeUI.text_font)
self.lineEdit.setProperty("colour", "1")
self.lineEdit.setAlignment(QtCore.Qt.AlignCenter)
self.lineEdit.saveVal = ''#self.lineEdit.text()
self.lineEdit.setValidator(
QtGui.QDoubleValidator(0.0, 100.0, 5)
) # ensures only doubles can be typed, do
# self.lineEdit.textEdited.connect(self.setTextColour(1))
self.numberpad = keyboard#NumberpadWidget(NativeUI)
self.numberpad.numberPressed.connect(self.handle_numberpress)
self.increaseButton = OkButtonWidget(NativeUI)
self.increaseButton.clicked.connect(self.increase_button_clicked)
self.increaseButton.setEnabled(True)
self.decreaseButton = CancelButtonWidget(NativeUI)
self.decreaseButton.clicked.connect(self.decrease_button_clicked)
self.decreaseButton.setEnabled(True)
#grid.addWidget(self.lineEdit, 0, 0, 1, 2)
hlayout = QtWidgets.QHBoxLayout()
hlayout.addWidget(self.decreaseButton)
hlayout.addWidget(self.lineEdit)
hlayout.addWidget(self.increaseButton)
hlayout2 = QtWidgets.QHBoxLayout()
self.okButton = OkButtonWidget(NativeUI)
self.okButton.setEnabled(True)
self.okButton.pressed.connect(self.handle_ok_press)
hlayout2.addWidget(self.okButton)
#grid.addWidget(self.okButton, 1, 0)
self.cancelButton = CancelButtonWidget(NativeUI)
self.cancelButton.setEnabled(True)
hlayout2.addWidget(self.cancelButton)
#grid.addWidget(self.cancelButton, 1, 1)
vlayout = QtWidgets.QVBoxLayout()
vlayout.addWidget(self.label)
vlayout.addLayout(hlayout)
vlayout.addWidget(self.numberpad)
vlayout.addLayout((hlayout2))
self.setLayout(vlayout)
self.setWindowFlags(
QtCore.Qt.FramelessWindowHint | QtCore.Qt.WindowStaysOnTopHint
) # no window title
def handle_ok_press(self):
val = self.lineEdit.text()
self.currentWidg.setValue(float(val))
self.close()
self.currentWidg.manualChanged.emit()
def populatePopup(self, currentWidg):
self.currentWidg = currentWidg
self.label_text, self.min, self.max, self.initVal, self.step, self.decPlaces = currentWidg.label_text, currentWidg.min, currentWidg.max, currentWidg.initVal, currentWidg.step, currentWidg.decPlaces
self.label.setText(self.label_text)
self.lineEdit.setText(str(currentWidg.value()))
def handle_numberpress(self, symbol):
"""Handle number pad button press. Put button value in line edit text, and handle inputs
outside accepted range or number of decimal places. Handle backspace"""
oldText = self.lineEdit.text()
if symbol.isnumeric() or (symbol == '.'):
newText = oldText + symbol
if float(newText) > self.max:
newText = str(self.max)
elif float(newText) < self.min:
newText = str(self.min)
elif '.' in newText:
if len(newText.split('.')[1]) > self.decPlaces:
newText = oldText
self.lineEdit.setText(newText)
elif symbol == '<':
self.lineEdit.setText(oldText[0:-1])
def handle_alphapress(self, symbol):
"""Handle number pad button press. Put button value in line edit text, and handle inputs
outside accepted range or number of decimal places. Handle backspace"""
oldText = self.lineEdit.text()
if symbol.isalpha():
newText = oldText + symbol
self.lineEdit.setText(newText)
elif symbol == '<':
self.lineEdit.setText(oldText[0:-1])
def increase_button_clicked(self):
"""Handle increase step button click"""
currentVal = self.get_value()
newVal = round(float(currentVal) + self.step, self.decPlaces)
if newVal >= self.max:
newVal = self.max
self.lineEdit.setText(str(newVal))
return 0
def decrease_button_clicked(self):
"""Handle decrease step button click"""
currentVal = self.get_value()
newVal = round(float(currentVal) - self.step, self.decPlaces)
if newVal <= self.min:
newVal = self.min
self.lineEdit.setText(str(newVal))
return 0
def get_value(self):
return self.lineEdit.text()
#!/usr/bin/env python3
"""
tab_hold_buttons.py
"""
__author__ = ["Benjamin Mummery", "Tiago Sarmento"]
__credits__ = ["Benjamin Mummery", "Dónal Murray", "Tim Powell", "Tiago Sarmento"]
__license__ = "GPL"
__version__ = "0.0.1"
__maintainer__ = "Tiago Sarmento"
__email__ = "tiago.sarmento@stfc.ac.uk"
__status__ = "Prototype"
import logging
from PySide2 import QtCore, QtGui, QtWidgets
from widget_library.ok_cancel_buttons_widget import OkButtonWidget, CancelButtonWidget
# from global_widgets.global_ok_cancel_buttons import okButton, cancelButton
import time
class timerConfirmPopup(QtWidgets.QWidget):
"""Popup when start stop standby buttons are pressed. Counts a certain amount of time before switching on,
showing a progress bar meanwhile"""
def __init__(self, NativeUI, *args, **kwargs):
super(timerConfirmPopup, self).__init__(*args, **kwargs)
self.setAttribute(
QtCore.Qt.WA_ShowWithoutActivating
) # keep the main page activated to maintain button hold
self.setWindowFlags(
QtCore.Qt.WindowStaysOnTopHint | QtCore.Qt.FramelessWindowHint
) # ensures focus is not stolen by alarm or confirmation
self.setStyleSheet(
"background-color: "
+ NativeUI.colors["button_background_enabled"].name()
+ ";"
"color: " + NativeUI.colors["button_foreground_disabled"].name() + ";"
"border:none"
)
self.stack = QtWidgets.QStackedWidget()
# Define progress bar
vlayout = QtWidgets.QVBoxLayout()
self.label = QtWidgets.QLabel("progressing")
vlayout.addWidget(self.label)
self.progressBar = QtWidgets.QProgressBar()
self.progressBar.setMaximum(3000)
self.progressBar.setFormat("")
vlayout.addWidget(self.progressBar)
self.progressWidg = QtWidgets.QWidget()
self.progressWidg.setLayout(vlayout)
self.stack.addWidget(self.progressWidg)
# Define confirmation message and buttons
self.completeLayout = QtWidgets.QVBoxLayout()
self.completeLabel = QtWidgets.QLabel("confirm it")
buttonLayout = QtWidgets.QHBoxLayout()
self.okButton = OkButtonWidget(NativeUI)
buttonLayout.addWidget(self.okButton)
self.cancelButton = CancelButtonWidget(NativeUI)
self.okButton.setEnabled(True)
self.cancelButton.setEnabled(True)
buttonLayout.addWidget(self.cancelButton)
self.completeLayout.addWidget(self.completeLabel)
self.completeLayout.addLayout(buttonLayout)
self.completeWidg = QtWidgets.QWidget()
self.completeWidg.setLayout(self.completeLayout)
self.stack.addWidget(self.completeWidg)
# set layout
stackLayout = QtWidgets.QVBoxLayout()
stackLayout.addWidget(self.stack)
self.setLayout(stackLayout)
# place popup in screen
qtRectangle = self.frameGeometry()
centerPoint = QtWidgets.QDesktopWidget().availableGeometry().center()
qtRectangle.moveCenter(centerPoint)
self.move(qtRectangle.topLeft())
# self.move(QtGui.QApplication.desktop().screen().rect().center() - self.rect().center())
class holdButton(QtWidgets.QPushButton):
"""Subclass push button to count press time and update progress bar. handleClick() is overridden.
Popup with progress bar appears on click, fills as button is held."""
def __init__(self, NativeUI, *args, **kwargs):
super(holdButton, self).__init__(*args, **kwargs)
self.NativeUI = NativeUI
self.timeOut = 3000
self.timeStep = 60
self.setAutoRepeat(True)
self.setAutoRepeatDelay(self.timeStep)
self.setAutoRepeatInterval(self.timeStep)
self.pressTime = 0
self.state = 0
self.complete = False
self.clicked.connect(self.handleClick)
self.popUp = timerConfirmPopup(NativeUI)
self.popUp.okButton.pressed.connect(self.okButtonPressed)
self.popUp.cancelButton.pressed.connect(self.closePopup)
def handleClick(self):
"""Over ride handleClick to count button hold time.
Shows popup, sets progress bar, and updates it"""
self.now = time.time()
if self.state == 0:
# print('pressed')
self.popUp.show()
self.initial = time.time()
if self.isDown():
if self.pressTime == self.timeOut:
self.complete = True
logging.debug(self.now - self.initial)
self.popUp.stack.setCurrentWidget(self.popUp.completeWidg)
self.pressTime = self.pressTime + self.timeStep
self.state = 1
self.popUp.progressBar.setValue(self.pressTime)
self.popUp.progressBar.update()
else:
self.pressTime = 0
logging.debug(
"holdButton.handleClick():\nself.complete: %s\nself.state: %s"
% (self.complete, self.state)
)
if self.state == 1:
logging.debug("holdButton released")
if not self.complete:
self.popUp.close()
self.state = 0
def okButtonPressed(self):
"""Respond to ok button press by sending command corresponding to button type"""
logging.debug(self.text())
self.NativeUI.q_send_cmd(
"GENERAL", self.text()
) # text is stand stop or standby
self.closePopup()
return 0
def closePopup(self):
"""Reset progress bar and widget stack, and close popup"""
self.popUp.progressBar.setValue(0)
self.popUp.stack.setCurrentWidget(self.popUp.progressWidg)
self.popUp.close()
return 0
if __name__ == "__main__":
import sys
app = QtWidgets.QApplication(sys.argv)
widg = holdButton("Standby")
widg.show()
sys.exit(app.exec_())
#!/usr/bin/env python3
"""
tab_modeswitch_button.py
"""
__author__ = ["Benjamin Mummery", "Tiago Sarmento"]
__credits__ = ["Benjamin Mummery", "Dónal Murray", "Tim Powell", "Tiago Sarmento"]
__license__ = "GPL"
__version__ = "0.0.1"
__maintainer__ = "Tiago Sarmento"
__email__ = "tiago.sarmento@stfc.ac.uk"
__status__ = "Prototype"
from PySide2 import QtCore, QtGui, QtWidgets
from widget_library.ok_cancel_buttons_widget import OkButtonWidget, CancelButtonWidget
import json
# from global_widgets.global_ok_cancel_buttons import okButton, cancelButton
class TabModeswitchButton(QtWidgets.QWidget):
modeSwitched = QtCore.Signal(str)
def __init__(self, NativeUI, *args, **kwargs):
super().__init__(*args, **kwargs)
"""Button opens popup for user to switch modes.
The label is updated to show the current operating mode"""
self.NativeUI = NativeUI
layout = QtWidgets.QHBoxLayout(self)
self.label = QtWidgets.QLabel("Mode: ")
self.label.setFont(NativeUI.text_font)
self.label.setStyleSheet(
"background-color:" + NativeUI.colors["page_background"].name() + ";"
"border: none;"
"color:" + NativeUI.colors["page_foreground"].name() + ";"
)
self.switchButton = QtWidgets.QPushButton(self.NativeUI.modeList[0])
layout.addWidget(self.label)
layout.addWidget(self.switchButton)
self.setLayout(layout)
self.mode_popup = False
self.switchButton.pressed.connect(self.switch_button_pressed)
# self.mode_popup.okbutton.pressed.connect(self.changeText)
def update_mode(self, mode):
"""Update button text to show operating mode"""
self.switchButton.setText(mode)
return 0
def switch_button_pressed(self):
"""Button pressed, open popup, ensure correct mode is selected in popup."""
if self.mode_popup == False:
self.mode_popup = modeswitchPopup(self.NativeUI)
self.mode_popup.okbutton.pressed.connect(self.changeText)
else:
self.mode_popup.radioButtons[self.NativeUI.currentMode].click()
self.mode_popup.show()
return 0
def changeText(self):
self.switchButton.setText(self.mode_popup.mode)
self.modeSwitched.emit(self.mode_popup.mode)
def set_size(self, x: int, y: int, spacing=10) -> int:
self.setFixedSize(x, y)
return 0
class modeswitchPopup(QtWidgets.QWidget):
def __init__(self, NativeUI, *args, **kwargs):
"""A popup used to switch modes. Allows the user to compare the values they are setting with current setting
and to navigate to mode setting page to edit those values."""
super(modeswitchPopup, self).__init__(*args, **kwargs)
self.NativeUI = NativeUI
with open("NativeUI/configs/mode_config.json") as json_file:
modeDict = json.load(json_file)
self.settingsList = modeDict['settings']
modeList = self.NativeUI.modeList
vradioLayout = QtWidgets.QVBoxLayout()
groupBox = QtWidgets.QGroupBox()
self.radioButtons = {}
for mode in modeList:
button = QtWidgets.QRadioButton(mode)
goto_button = QtWidgets.QPushButton(mode)
goto_button.pressed.connect(lambda j=mode: self.goto_pressed(j))
hlayout = QtWidgets.QHBoxLayout()
hlayout.addWidget(button)
hlayout.addWidget(goto_button)
self.radioButtons[mode] = button
vradioLayout.addLayout(hlayout)
groupBox.setLayout(vradioLayout)
## Values display
valuesLayout = QtWidgets.QHBoxLayout()
# Title labels:
initLabel = QtWidgets.QLabel(" ")
initVal = QtWidgets.QLabel("Current")
initVal.setAlignment(QtCore.Qt.AlignCenter)
newVal = QtWidgets.QLabel("New")
newVal.setAlignment(QtCore.Qt.AlignCenter)
newVal.setStyleSheet("color: red")
# Populate actual values in loop
self.labelList, self.currentLabelList, self.newLabelList = [], [], []
vlayout1, vlayout2, vlayout3 = (
QtWidgets.QVBoxLayout(),
QtWidgets.QVBoxLayout(),
QtWidgets.QVBoxLayout(),
)
vlayout1.addWidget(initLabel)
vlayout2.addWidget(initVal)
vlayout3.addWidget(newVal)
for settings in self.settingsList:
namelabel = QtWidgets.QLabel(settings[0])
namelabel.setAlignment(QtCore.Qt.AlignRight)
vlayout1.addWidget(namelabel)
currentLabel = QtWidgets.QLabel("0")
currentLabel.setAlignment(QtCore.Qt.AlignCenter)
self.currentLabelList.append(currentLabel)
vlayout2.addWidget(currentLabel)
newLabel = QtWidgets.QLabel("0")
newLabel.setAlignment(QtCore.Qt.AlignCenter)
newLabel.setStyleSheet("color: red")
self.newLabelList.append(newLabel)
vlayout3.addWidget(newLabel)
valuesLayout.addLayout(vlayout1)
valuesLayout.addLayout(vlayout2)
valuesLayout.addLayout(vlayout3)
hlayout = QtWidgets.QHBoxLayout()
hlayout.addWidget(groupBox)
hlayout.addLayout(valuesLayout)
## Ok Cancel Buttons
hbuttonlayout = QtWidgets.QHBoxLayout()
self.okbutton = OkButtonWidget(NativeUI)
self.okbutton.setEnabled(True)
self.okbutton.pressed.connect(self.ok_button_pressed)
self.cancelbutton = CancelButtonWidget(NativeUI)
self.cancelbutton.setEnabled(True)
self.cancelbutton.pressed.connect(self.cancel_button_pressed)
hbuttonlayout.addWidget(self.okbutton)
hbuttonlayout.addWidget(self.cancelbutton)
vlayout = QtWidgets.QVBoxLayout()
vlayout.addLayout(hlayout)
vlayout.addLayout(hbuttonlayout)
for mode in modeList:
button = self.radioButtons[mode]
button.pressed.connect(lambda i=button: self.update_settings_data(i))
if mode == self.NativeUI.currentMode:
button.click()
## Final, general, initiation steps
self.setLayout(vlayout)
self.setWindowFlags(QtCore.Qt.FramelessWindowHint)
# self.radioButtons[self.NativeUI.currentMode].click() # 1st button clicked by default
# self.radioButtons[0].click() # 1st button clicked by default
# self.update_settings_data(radioButtons[0])
self.setStyleSheet(
"background-color:" + NativeUI.colors["page_background"].name() + ";"
"color:" + NativeUI.colors["page_foreground"].name() + ";"
"font: 16pt bold;"
)
# self.radioButtons[self.NativeUI.currentMode].click()
def goto_pressed(self, mode):
"""On button press, show mode page in UI"""
self.NativeUI.widgets.page_stack.setCurrentWidget(
self.NativeUI.widgets.modes_page
)
self.NativeUI.widgets.page_buttons.set_pressed(["modes_button"])
# Switch to the specific mode tab
for button in self.NativeUI.widgets.modes_page.widget_list[
0
].button_list: # mode_settings_tab.buttonWidgets:
if mode in button.text():
self.NativeUI.widgets.modes_page.widget_list[0].setTab(button)
# Close the popup
self.close()
def update_settings_data(self, button):
"""Respond to button press and update labels in modeswitch popup"""
self.spinDict = self.NativeUI.mode_handler.spinDict
self.mode = button.text()
for settings, currentLabel, newLabel in zip(
self.settingsList, self.currentLabelList, self.newLabelList
):
currentVal = self.spinDict[
"spin_" + self.NativeUI.currentMode + "_" + settings[2]
].get_value()
currentLabel.setText(str(round(currentVal, 4)))
setVal = self.spinDict["spin_" + self.mode + "_" + settings[2]].get_value()
newLabel.setText(str(round(setVal, 4)))
def ok_button_pressed(self):
"""Switch to selected mode"""
if self.NativeUI.currentMode == self.mode:
a = 1 # do nothing
else:
self.NativeUI.q_send_cmd(
"SET_MODE", self.mode.replace("/", "_").replace("-", "_")
)
self.NativeUI.currentMode = self.mode
self.close()
return 0
def cancel_button_pressed(self):
"""Close popup without doing anything"""
self.close()
return 0
def update_mode(self, mode):
"""When mode is changed the popup radio buttons should show the new mode"""
self.mode_popup.radioButtons[mode].click()
#!/usr/bin/env python3
"""
tab_start_stop_buttons.py
"""
__author__ = ["Benjamin Mummery", "Tiago Sarmento"]
__credits__ = ["Benjamin Mummery", "Dónal Murray", "Tim Powell", "Tiago Sarmento"]
__license__ = "GPL"
__version__ = "0.0.1"
__maintainer__ = "Tiago Sarmento"
__email__ = "tiago.sarmento@stfc.ac.uk"
__status__ = "Prototype"
import logging
from PySide2 import QtGui, QtWidgets
from PySide2.QtCore import QSize
from global_widgets.tab_hold_buttons import holdButton
class TabStartStopStandbyButtons(QtWidgets.QWidget):
"""
Combine holdButtons into Start, Stop, and Standby buttons in the left bar.
"""
def __init__(self, NativeUI, *args, size: QSize = None, **kwargs):
super(TabStartStopStandbyButtons, self).__init__(*args, **kwargs)
self.NativeUI = NativeUI
if size is not None:
self.__button_size = size
else:
self.__button_size = QSize(100, 20)
layout = QtWidgets.QVBoxLayout()
self.button_start = holdButton(NativeUI) # QtWidgets.QPushButton()
self.button_stop = holdButton(NativeUI) # QtWidgets.QPushButton()
self.button_standby = holdButton(NativeUI) # QtWidgets.QPushButton()
self.__buttons = [self.button_start, self.button_stop, self.button_standby]
self.__buttontext = ["START", "STOP", "STANDBY"]
self.__buttoncommand = [""]
for button, text in zip(self.__buttons, self.__buttontext):
button.setText(text)
button.popUp.completeLabel.setText("Ventilation " + text)
layout.addWidget(button)
button.setStyleSheet(
"background-color:" + NativeUI.colors["background_enabled"].name() + ";"
"border-color:" + NativeUI.colors["page_foreground"].name() + ";"
"color:" + NativeUI.colors["page_foreground"].name() + ";"
"border:none"
)
button.setFont(NativeUI.text_font)
button.setFixedSize(self.__button_size)
self.setLayout(layout)
#!/usr/bin/env python3
"""
template_main_pages.py
"""
__author__ = ["Benjamin Mummery", "Tiago Sarmento"]
__credits__ = ["Benjamin Mummery", "Dónal Murray", "Tim Powell", "Tiago Sarmento"]
__license__ = "GPL"
__version__ = "0.0.1"
__maintainer__ = "Tiago Sarmento"
__email__ = "tiago.sarmento@stfc.ac.uk"
__status__ = "Prototype"
from PySide2 import QtWidgets
# from global_widgets.global_send_popup import SetConfirmPopup
class TemplateMainPages(QtWidgets.QWidget):
def buildPage(self, buttonList, tabsList, *args, **kwargs):
vlayout = QtWidgets.QVBoxLayout()
hButtonLayout = QtWidgets.QHBoxLayout()
self.stack = QtWidgets.QStackedWidget()
for button, tab in zip(buttonList, tabsList):
hButtonLayout.addWidget(button)
self.stack.addWidget(tab)
button.pressed.connect(lambda i=button: self.setTab(i))
self.setTab(buttonList[0])
vlayout.addLayout(hButtonLayout)
vlayout.addWidget(self.stack)
self.setLayout(vlayout)
def setTab(self, buttonWidg):
for button, tab in zip(self.buttonWidgets, self.tabsList):
if button == buttonWidg:
button.setProperty("selected", "1")
self.stack.setCurrentWidget(tab)
else:
button.setProperty("selected", "0")
button.style().unpolish(button)
button.style().polish(button)
#!/usr/bin/env python3
"""
template_set_values.py
"""
__author__ = ["Benjamin Mummery", "Tiago Sarmento"]
__credits__ = ["Benjamin Mummery", "Dónal Murray", "Tim Powell", "Tiago Sarmento"]
__license__ = "GPL"
__version__ = "0.0.1"
__maintainer__ = "Tiago Sarmento"
__email__ = "tiago.sarmento@stfc.ac.uk"
__status__ = "Prototype"
from PySide2 import QtWidgets, QtGui, QtCore
from global_widgets.global_spinbox import labelledSpin
from global_widgets.global_send_popup import SetConfirmPopup
from global_widgets.global_select_button import selectorButton
# from global_widgets.global_ok_cancel_buttons import okButton, cancelButton
from widget_library.ok_cancel_buttons_widget import (
OkButtonWidget,
OkSendButtonWidget,
CancelButtonWidget,
)
from widget_library.line_edit_widget import LabelledLineEditWidget
# from global_widgets.global_lineEdit import labelledLineEdit
class TemplateSetValues(QtWidgets.QWidget):
def __init__(self, NativeUI, *args, **kwargs):
super(TemplateSetValues, self).__init__(*args, **kwargs)
self.layoutList = []
self.spinDict = {}
self.NativeUI = NativeUI
self.packet = "targets"
self.timer = QtCore.QTimer()
self.timer.setInterval(500) # just faster than 60Hz
self.timer.timeout.connect(self.update_settings_data)
self.timer.start()
def setPacketType(self, packetName):
self.packet = packetName
def finaliseLayout(self):
vlayout = QtWidgets.QVBoxLayout()
for layout in self.layoutList:
vlayout.addLayout(layout)
self.setLayout(vlayout)
def addSpinSingleCol(self, settingsList):
vOptionLayout = QtWidgets.QVBoxLayout()
for info in settingsList:
self.spinDict[info[0]] = labelledSpin(self.NativeUI, info)
vOptionLayout.addWidget(self.spinDict[info[0]])
self.layoutList.append(vOptionLayout)
def addSpinDblCol(self, settingsList):
grid = QtWidgets.QGridLayout()
grid.setHorizontalSpacing(0)
vlayout = QtWidgets.QVBoxLayout()
vlayout2 = QtWidgets.QVBoxLayout()
i = 0
for info in settingsList:
if "_Low" in info[0]:
self.spinDict[info[0]] = labelledSpin(
self.NativeUI, [info[0], "", info[2]]
)
self.spinDict[info[0] + "_2"] = labelledSpin(
self.NativeUI, ["", info[1], info[2]]
)
# hlayout = QtWidgets.QHBoxLayout()
# hlayout.setSpacing(0)
# hlayout.addWidget(self.spinDict[info[0]])
# hlayout.addWidget(self.spinDict[info[0]+ '_2'])
# if (i%2) == 0:
# vlayout.addLayout(hlayout)
# else:
# vlayout2.addLayout(hlayout)
grid.addWidget(self.spinDict[info[0]], int(i / 2), 2 * (i % 2), 1, 1)
grid.addWidget(
self.spinDict[info[0] + "_2"], int(i / 2), 2 * (i % 2) + 1, 1, 1
)
else:
self.spinDict[info[0]] = labelledSpin(self.NativeUI, info)
# if (i%2) == 0:
# vlayout.addWidget(self.spinDict[info[0]])
# else:
# vlayout2.addWidget(self.spinDict[info[0]])
grid.addWidget(self.spinDict[info[0]], int(i / 2), 2 * (i % 2), 1, 2)
i = i + 1
# hlayoutMeta = QtWidgets.QHBoxLayout()
# hlayoutMeta.addLayout(vlayout)
# hlayoutMeta.addLayout(vlayout2)
self.layoutList.append(grid)
def addExpertControls(self, controlDict):
vlayout = QtWidgets.QVBoxLayout()
i = 0
for section in controlDict.keys():
self.titleLabel = QtWidgets.QLabel(section)
self.titleLabel.setStyleSheet(
"background-color:"
+ self.NativeUI.colors["page_background"].name()
+ ";"
"color:" + self.NativeUI.colors["page_foreground"].name() + ";"
"font-size: " + self.NativeUI.text_size + ";"
)
self.titleLabel.setAlignment(QtCore.Qt.AlignCenter)
vlayout.addWidget(self.titleLabel)
grid = QtWidgets.QGridLayout()
grid.setMargin(0)
grid.setSpacing(0)
widg = QtWidgets.QFrame()
widg.setStyleSheet(
"QFrame{"
" border: 2px solid"
+ self.NativeUI.colors["page_foreground"].name()
+ ";"
"}"
"QLabel{"
" border:none;"
"} "
)
j = -1
for boxInfo in controlDict[section]:
j = j + 1
self.spinDict[boxInfo[0]] = labelledSpin(self.NativeUI, boxInfo)
grid.addWidget(
self.spinDict[boxInfo[0]], i + 1 + int(j / 3), 2 * (j % 3), 1, 2
)
widg.setLayout(grid)
vlayout.addWidget(widg)
i = i + 1 + int(j / 3) + 1
self.layoutList.append(vlayout)
def addPersonalCol(self, settingsList, textBoxes):
vOptionLayout = QtWidgets.QVBoxLayout()
for info in settingsList:
if info[0] in textBoxes:
self.spinDict[info[0]] = LabelledLineEditWidget(self.NativeUI, info)
# self.spinDict[info[0]] = labelledLineEdit(self.NativeUI, info)
self.spinDict[info[0]].simpleSpin.textChanged.connect(
lambda textignore, i=1: self.colourButtons(i)
)
else:
self.spinDict[info[0]] = labelledSpin(self.NativeUI, info)
self.spinDict[info[0]].simpleSpin.manualChanged.connect(
lambda i=1: self.colourButtons(i)
)
vOptionLayout.addWidget(self.spinDict[info[0]])
self.layoutList.append(vOptionLayout)
def addButtons(self):
hlayout = QtWidgets.QHBoxLayout()
self.okButton = OkButtonWidget(self.NativeUI)
self.okButton.pressed.connect(self.okButtonPressed)
hlayout.addWidget(self.okButton)
self.cancelButton = CancelButtonWidget(self.NativeUI)
self.cancelButton.pressed.connect(self.cancelButtonPressed)
hlayout.addWidget(self.cancelButton)
self.buttonsList = [self.okButton, self.cancelButton]
self.layoutList.append(hlayout)
for spin in self.spinDict:
self.spinDict[spin].simpleSpin.manualChanged.connect(
lambda i=1: self.colourButtons(i)
)
def addModeButtons(self):
hlayout = QtWidgets.QHBoxLayout()
self.okButton = OkButtonWidget(self.NativeUI)
self.okButton.pressed.connect(self.okButtonPressed)
hlayout.addWidget(self.okButton)
self.okSendButton = OkSendButtonWidget(self.NativeUI)
self.okSendButton.pressed.connect(self.okSendButtonPressed)
hlayout.addWidget(self.okSendButton)
self.cancelButton = CancelButtonWidget(self.NativeUI)
self.cancelButton.pressed.connect(self.cancelButtonPressed)
hlayout.addWidget(self.cancelButton)
self.buttonsList = [self.okButton, self.okSendButton, self.cancelButton]
self.layoutList.append(hlayout)
for spin in self.spinDict:
self.spinDict[spin].simpleSpin.manualChanged.connect(
lambda i=1: self.colourButtons(i)
)
def colourButtons(self, option):
for button in self.buttonsList:
button.setColour(str(option))
def update_settings_data(self):
liveUpdatingCheck = True
db = self.NativeUI.get_db(self.packet)
if db == {}:
return 0 # do nothing
else:
for widget in self.spinDict:
self.spinDict[widget].update_value(db)
liveUpdatingCheck = (
liveUpdatingCheck and not self.spinDict[widget].manuallyUpdated
)
if liveUpdatingCheck:
self.colourButtons(0)
def okButtonPressed(self):
message, command = [], []
for widget in self.spinDict:
if self.spinDict[widget].manuallyUpdated:
setVal = self.spinDict[widget].get_value()
message.append("set" + widget + " to " + str(setVal))
command.append(
[
self.spinDict[widget].cmd_type,
self.spinDict[widget].cmd_code,
setVal,
]
)
self.popup = SetConfirmPopup(self, self.NativeUI, message, command)
self.popup.okButton.pressed.connect(self.commandSent)
self.popup.show()
def okSendButtonPressed(self):
message, command = [], []
for widget in self.spinDict:
if self.spinDict[widget].manuallyUpdated:
setVal = self.spinDict[widget].get_value()
message.append("set" + widget + " to " + str(setVal))
command.append(
[
self.spinDict[widget].cmd_type,
self.spinDict[widget].cmd_code,
setVal,
]
)
self.popUp = SetConfirmPopup(self, self.NativeUI, message, command)
self.popUp.ok_button_pressed()
self.NativeUI.q_send_cmd(
"SET_MODE", self.mode.replace("/", "_").replace("-", "_")
)
self.NativeUI.currentMode = self.mode
self.NativeUI.topBar.tab_modeswitch.switchButton.setText(self.mode)
# self.NativeUI.topBar.tab_modeswitch.mode_popup.radioButtons[self.mode].click()
self.popUp.setParent(None)
self.commandSent()
def commandSent(self):
for button in self.buttonsList:
button.setColour(0)
for widget in self.spinDict:
self.spinDict[widget].manuallyUpdated = False
def cancelButtonPressed(self):
for button in self.buttonsList:
button.setColour(0)
for widget in self.spinDict:
if self.spinDict[widget].manuallyUpdated:
self.spinDict[widget].manuallyUpdated = False
"""
alarm_handler.py
"""
from handler_library.handler import PayloadHandler
from PySide2.QtCore import QObject
class AlarmHandler(PayloadHandler, QObject):
"""
Subclass of the PayloadHandler class (handler.py) to handle alarm data.
Inherits from QObject to give us access to pyside2's signal class.
"""
def __init__(self):
super().__init__()
QObject.__init__(self)
import logging
from handler_library.handler import PayloadHandler
from PySide2.QtCore import Signal, QObject
class BatteryHandler(PayloadHandler):
"""
Subclass of the PayloadHandler class (handler.py) to handle alarm data.
"""
UpdateBatteryDisplay = Signal(dict)
def __init__(self, *args, **kwargs):
super().__init__(["BATTERY"], *args, **kwargs)
def active_payload(self, *args, **kwargs) -> int:
"""
When battery information is set, interprets it to construct the battery status
and emits the UpdateBatteryDisplay signal containing that status as a dict.
"""
new_status = {}
battery_data = self.get_db()
# Update new_status
try:
new_status["on_mains_power"] = bool(battery_data["ok"])
except KeyError:
logging.debug("Keyerror in battery payload: 'ok'")
try:
new_status["on_battery_power"] = bool(battery_data["bat"])
except KeyError:
logging.debug("Keyerror in battery payload: 'bat'")
try:
new_status["battery_percent"] = self.compute_battery_percent(battery_data)
except KeyError:
logging.debug("Keyerror in battery payload: 'bat85'")
try:
if bool(battery_data["prob_elec"]):
new_status["electrical_problem"] = "ERROR ELEC."
else:
new_status["electrical_problem"] = None
except KeyError:
logging.debug("Keyerror in battery payload: 'prob_elec'")
# Sanity checks
if new_status["on_mains_power"] == new_status["on_battery_power"]:
# If there is conflicting information w.r.t. power source, report a problem
new_status["on_mains_power"] = False
new_status["on_battery_power"] = False
new_status["electrical_problem"] = "ERROR ELEC."
self.UpdateBatteryDisplay.emit(new_status)
return 0
def compute_battery_percent(self, battery_data: dict) -> float:
"""
Determine the current battery percentage from the information in battery_data.
As of 17/03/21 battery payloads only contain enough information to
determine if the battery is above or below 85% battery life.
Unless provided with specific information to the contrary, assume that the
battery is on 0% so that we should never overestimate how much remains.
"""
if battery_data["bat85"] == 1:
return 85.0
elif battery_data["bat85"] == 0:
return 0.0
else:
raise TypeError(
"Battery Percentage (entry 'bat85' in the battery payload) is not 1 or 2."
)
from handler_library.handler import PayloadHandler
from PySide2.QtCore import Signal
import numpy as np
from threading import Lock
class DataHandler(PayloadHandler):
UpdatePlots = Signal(dict)
def __init__(self, *args, plot_history_length=500, **kwargs):
super().__init__(["DATA", "CYCLE"], *args, **kwargs)
self.__plot_history_length = plot_history_length
self.__plots_database = {
"data": np.zeros((plot_history_length, 4)),
"timestamp": list(el * (-1) for el in range(plot_history_length))[::-1],
"pressure": list(0 for _ in range(plot_history_length)),
"flow": list(0 for _ in range(plot_history_length)),
"volume": list(0 for _ in range(plot_history_length)),
"cycle_pressure": list(0 for _ in range(plot_history_length)),
"cycle_flow": list(0 for _ in range(plot_history_length)),
"cycle_volume": list(0 for _ in range(plot_history_length)),
"pressure_axis_range": [0, 20],
"flow_axis_range": [-40, 80],
"volume_axis_range": [0, 80],
}
self.__plot_lock = Lock()
self.__cycle_index: list = [
plot_history_length,
plot_history_length,
plot_history_length,
]
def active_payload(self, payload, *args, **kwargs):
"""
Take the raw payload information into conveniently plotable forms and store them
in self.__plots_database.
"""
raw_data = self.get_db()
if payload["type"] == "DATA":
# The earliest index to plot for cycle plots needs to move back to keep pace
# with the data.
self.__cycle_index = [
index - 1 if (index - 1 >= 0) else 0 for index in self.__cycle_index
]
with self.__plot_lock:
# Remove the oldest data and add the new data to the other end of the
# array.
self.__plots_database["data"] = np.append(
np.delete(self.__plots_database["data"], 0, 0),
[
[
raw_data["timestamp"],
raw_data["pressure_patient"],
raw_data["flow"],
raw_data["volume"],
]
],
axis=0,
)
# subtract latest timestamp and scale to seconds.
self.__plots_database["timestamp"] = np.true_divide(
np.subtract(
self.__plots_database["data"][:, 0],
self.__plots_database["data"][-1, 0],
),
1000,
)
# Pull out the specific properties we want to plot.
self.__plots_database["pressure"] = self.__plots_database["data"][:, 1]
self.__plots_database["flow"] = self.__plots_database["data"][:, 2]
self.__plots_database["volume"] = self.__plots_database["data"][:, 3]
self.__plots_database["cycle_pressure"] = self.__plots_database["data"][
self.__cycle_index[0] :, 1
]
self.__plots_database["cycle_flow"] = self.__plots_database["data"][
self.__cycle_index[0] :, 2
]
self.__plots_database["cycle_volume"] = self.__plots_database["data"][
self.__cycle_index[0] :, 3
]
elif payload["type"] == "CYCLE":
self.__cycle_index.pop(0)
self.__cycle_index.append(self.__plot_history_length)
return 0
def send_update_plots_signal(self) -> int:
"""
construct a dictionary of only the properties needed for plotting and emit the
UpdatePlots signal containing it.
"""
outdict = {}
with self.__plot_lock:
for key in [
"flow",
"pressure",
"timestamp",
"volume",
"cycle_flow",
"cycle_pressure",
"cycle_volume",
]:
outdict[key] = self.__plots_database[key]
self.UpdatePlots.emit(outdict)
return 0
"""
handler.py
"""
from threading import Lock
from global_widgets.global_spinbox import labelledSpin
from widget_library.ok_cancel_buttons_widget import OkButtonWidget, CancelButtonWidget, OkSendButtonWidget
from PySide2.QtCore import QObject
from PySide2.QtWidgets import QRadioButton
class GenericDataHandler(QObject):
"""
Base class for non-payload data handlers.
"""
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.__database = {}
self.__lock = Lock()
def set_db(self, data: dict) -> int:
"""
Copy the contents of 'data' to the internal database.
"""
with self.__lock:
for key in data:
self.__database[key] = data[key]
self.on_data_set()
return 0
def get_db(self) -> dict:
"""
Return the content of the __database dictionary.
"""
with self.__lock:
return dict(self.__database)
def on_data_set(self):
"""
Overridable function called after recieving new data.
"""
pass
class PayloadHandler(GenericDataHandler):
"""
Base class for the payload data handlers.
"""
def __init__(self, payload_types: list, *args, **kwargs):
super().__init__(*args, **kwargs)
for key in payload_types:
if not isinstance(key, str):
raise TypeError(
"payload types must be of type 'str', not %s", type(key)
)
self.__database = {}
self.__lock = Lock()
self.__payload_types = payload_types
def set_db(self, payload: dict) -> int:
"""
If the provided database is of the correct type, copy its contents to the database
"""
if payload["type"] not in self.__payload_types:
return 1
with self.__lock:
for key in payload[payload["type"]]:
self.__database[key] = payload[payload["type"]][key]
self.active_payload(payload)
return 0
def get_db(self) -> dict:
"""
Return the content of the __database dictionary.
"""
with self.__lock:
return dict(self.__database)
def active_payload(self, payload: dict):
"""
Overridable function called after recieving new data. Passes in the full payload
so that we have access to the full context of the information.
"""
pass
"""
measurement_handler.py
"""
from handler_library.handler import PayloadHandler
from PySide2.QtCore import Signal
import logging
class MeasurementHandler(PayloadHandler):
"""
Subclass of the PayloadHandler class (handler.py) to handle cycle and readback data.
"""
UpdateMeasurements = Signal(dict)
def __init__(self, *args, **kwargs):
super().__init__(["CYCLE", "READBACK"], *args, **kwargs)
def active_payload(self, *args, **kwargs) -> int:
cycle_data = self.get_db()
outdict = {}
for key in [
"plateau_pressure",
"respiratory_rate",
"fiO2_percent",
"exhaled_tidal_volume",
"exhaled_minute_volume",
"peak_inspiratory_pressure",
"mean_airway_pressure",
"inhaled_tidal_volume",
"inhaled_minute_volume",
"peep",
"inhale_exhale_ratio",
]:
try:
outdict[key] = cycle_data[key]
except KeyError:
logging.debug("Invalid key %s in measurement database", key)
self.UpdateMeasurements.emit(outdict)
return 0
"""
personal_handler.py
"""
from handler_library.handler import PayloadHandler
from PySide2.QtCore import Signal
class PersonalHandler(PayloadHandler):
"""
Subclass of the PayloadHandler class (handler.py) to handle personal data.
Adds the UpdatePersonalDisplay signal designed to convey information to be displayed
to the display widget.
"""
UpdatePersonalDisplay = Signal(dict)
def __init__(self, *args, **kwargs):
super().__init__(["PERSONAL"], *args, **kwargs)
def active_payload(self, *args, **kwargs) -> int:
"""
When the personal data is updated, extract those parameters needed for display
and emit the UpdatePersonalDisplay signal.
"""
current_data = self.get_db()
outdict = {}
for key in ["name", "patient_id", "age", "sex", "height", "weight"]:
outdict[key] = current_data[key]
self.UpdatePersonalDisplay.emit(outdict)
return 0
#!/usr/bin/env python3
"""
hev_alarms.py
"""
__author__ = ["Benjamin Mummery", "Tiago Sarmento"]
__credits__ = ["Benjamin Mummery", "Dónal Murray", "Tim Powell", "Tiago Sarmento"]
__license__ = "GPL"
__version__ = "0.0.1"
__maintainer__ = "Tiago Sarmento"
__email__ = "tiago.sarmento@stfc.ac.uk"
__status__ = "Prototype"
from alarm_widgets.tab_alarms import TabAlarm
from alarm_widgets.tab_alarm_table import TabAlarmTable
from alarm_widgets.tab_clinical import TabClinical
from global_widgets.global_select_button import selectorButton
from global_widgets.template_main_pages import TemplateMainPages
from alarm_widgets.alarm_popup import abstractAlarm
from PySide2 import QtCore
class AlarmView(TemplateMainPages):
"""Subclasses TemplateMainPages to display alarms."""
def __init__(self, NativeUI, *args, **kwargs):
super(AlarmView, self).__init__(*args, **kwargs)
self.NativeUI = NativeUI
self.alarmButton = selectorButton(NativeUI, "List of Alarms")
self.alarmTableButton = selectorButton(NativeUI, "Alarm Table")
self.clinicalButton = selectorButton(NativeUI, "Clinical Limits")
# self.techButton = selectorButton(NativeUI, "Technical Limits")
self.buttonWidgets = [
self.alarmButton,
self.alarmTableButton,
self.clinicalButton,
] # , self.techButton]
self.alarmTab = TabAlarm(NativeUI)
self.alarmTableTab = TabAlarmTable(NativeUI)
self.clinicalTab = TabClinical(NativeUI)
# self.technicalTab = TabClinical(NativeUI)
self.tabsList = [
self.alarmTab,
self.alarmTableTab,
self.clinicalTab,
] # , self.technicalTab]
self.buildPage(self.buttonWidgets, self.tabsList)
self.alarmDict = {}
self.timer = QtCore.QTimer()
self.timer.setInterval(300)
self.timer.timeout.connect(self.updateAlarms)
self.timer.start()
def updateAlarms(self):
print("attempting new alarms")
newAlarmPayload = self.NativeUI.get_db("alarms")
if newAlarmPayload == []:
return
if newAlarmPayload["alarm_code"] in self.alarmDict:
a = 1
self.alarmDict[newAlarmPayload["alarm_code"]].resetTimer()
self.alarmDict[newAlarmPayload["alarm_code"]].calculateDuration()
else:
newAbstractAlarm = abstractAlarm(self.NativeUI, newAlarmPayload)
self.alarmDict[newAlarmPayload["alarm_code"]] = newAbstractAlarm
newAbstractAlarm.alarmExpired.connect(
lambda i=newAbstractAlarm: self.handleAlarmExpiry(i)
)
self.alarmTab.popup.addAlarm(newAbstractAlarm)
self.alarmTab.list.addAlarm(newAbstractAlarm)
self.alarmTableTab.table.addAlarmRow(newAbstractAlarm)
def handleAlarmExpiry(self, abstractAlarm):
abstractAlarm.freezeTimer()
self.alarmTab.popup.removeAlarm(abstractAlarm)
self.alarmTab.list.removeAlarm(abstractAlarm)
self.alarmDict.pop(abstractAlarm.alarmPayload["alarm_code"])
abstractAlarm.recordFinishTime()
import logging
import argparse
import sys
from PySide2.QtCore import Slot
from PySide2.QtWidgets import QWidget, QApplication, QHBoxLayout, QVBoxLayout
from hevclient import HEVClient
from tab_plots import TabPlots
class MainView(QWidget):
def __init__(self, *args, **kwargs):
super(MainView, self).__init__(*args, **kwargs)
hlayout = QHBoxLayout()
vlayout = QVBoxLayout()
self.tab_plots = TabPlots()
vlayout.addWidget(self.tab_plots)
hlayout.addLayout(vlayout)
self.setLayout(hlayout)
\ No newline at end of file
from global_widgets.global_spinbox import labelledSpin
from widget_library.ok_cancel_buttons_widget import OkButtonWidget, CancelButtonWidget, OkSendButtonWidget
#from global_widgets.global_send_popup import SetConfirmPopup
from widget_library.spin_buttons_widget import SpinButton
from PySide2 import QtWidgets, QtGui, QtCore
from handler_library.handler import PayloadHandler
import logging
import json
class ClinicalHandler(PayloadHandler):
#modeSwitched = QtCore.Signal(str)
UpdateClinical = QtCore.Signal(dict)
OpenPopup = QtCore.Signal(PayloadHandler, list)
#settingToggle = QtCore.Signal(str)
def __init__(self, NativeUI, *args, **kwargs):
super().__init__(['TARGET'],*args, **kwargs)
#super(TabModes, self).__init__(NativeUI, *args, **kwargs)
self.NativeUI = NativeUI
self.limSpinDict = {}
self.setSpinDict = {}
self.buttonDict = {}
self.radioDict = {}
self.commandList = []
self.manuallyUpdated = False
self.valueDict = {}
with open("NativeUI/configs/clinical_config.json") as json_file:
clinicalDict = json.load(json_file)
self.singleThresholds = clinicalDict["SingleThresholds"]
self.absoluteLimits = clinicalDict["AbsoluteLimits"]
self.limit_to_mode_dict = {}
self.relevantKeys = []
for setting in clinicalDict['settings']:
if len(setting) == 3:
limit_code = setting[0][2]
mode_code = setting[1][2]
mode_minimum = setting[1][5]
mode_maximum = setting[1][6]
limit_minimum = setting[0][5]
limit_maximum = setting[-1][6]
self.limit_to_mode_dict[limit_code] = [mode_code, mode_minimum, mode_maximum, limit_minimum, limit_maximum]
self.relevantKeys.append(setting[1][2])
def add_widget(self, widget, key: str):
if isinstance(widget, labelledSpin):
if 'min' in key or 'max' in key:
self.limSpinDict[key] = widget
elif 'set' in key:
self.setSpinDict[key] = widget
self.valueDict[key] = widget.get_value()
if isinstance(widget, OkButtonWidget) or isinstance(widget, CancelButtonWidget) or isinstance(widget, OkSendButtonWidget):
self.buttonDict[key] = widget
if isinstance(widget, QtWidgets.QRadioButton):
self.radioDict[key] = widget
def active_payload(self, *args) -> int:
target_data = self.get_db()
outdict = {}
for key in self.relevantKeys:
try:
outdict[key] = target_data[key]
except KeyError:
logging.debug("Invalid key %s in measurement database", key)
self.UpdateClinical.emit(outdict)
return 0
def handle_okbutton_click(self):
message, command = [], []
for key, widget in dict(self.limSpinDict, **self.setSpinDict).items():
if widget.manuallyUpdated:
setVal = widget.get_value()
if ('set' not in key):
setkey = key.replace('min', 'set').replace('max','set')
if (widget.label in self.absoluteLimits):
multiplier = 1
else:
multiplier = self.setSpinDict[setkey].get_value()/100
setVal = self.setSpinDict[setkey].get_value() + setVal*multiplier
setVal = round(setVal,widget.decPlaces)
message.append("set" + key + " to " + str(setVal))
command.append(
[
widget.cmd_type,
widget.cmd_code,
setVal,
]
)
# create a signal emitting message, command, handler identifier - in nativeui connect to a popup widget
# command sending should occur in handler
self.commandList = command
self.OpenPopup.emit(self,message)
def sendCommands(self):
if self.commandList == []:
a=1
else:
for command in self.commandList:
self.NativeUI.q_send_cmd(*command)
self.commandSent()
return 0
def handle_cancelbutton_click(self):
for key, widget in dict(self.limSpinDict, **self.setSpinDict).items():
widget.manuallyUpdated = False
widget.set_value(self.valueDict[key])
self.active_payload()
self.refresh_button_colour()
def handle_cancel_pressed(self,buttonMode):
if buttonMode == self.NativeUI.currentMode:
print('modes match ')
self.commandSent()
else:
print('do nothing in clinical')
def commandSent(self):
self.commandList = []
for key, widget in dict(self.limSpinDict, **self.setSpinDict).items():
widget.manuallyUpdated = False
self.valueDict[key] = widget.get_value()
widget.set_value(widget.get_value())
self.active_payload()
self.refresh_button_colour()
def handle_manual_change(self, changed_spin_key):
self.active_payload()
self.refresh_button_colour()
def setpoint_changed(self, widget):
"""Respond to change in operational settings to modify alarm limits. If setpoint is close to an absolute maximum
or minimum the alarm limits should respond.
Takes the modified widget, uses its tag to identify corresponding alarm limits"""
cmd_code = widget.tag
for key, infoList in self.limit_to_mode_dict.items():
if cmd_code in infoList[0]: # find entry in dictionary corresponding to the modified widget
setValue = widget.get_value()
minValue = float(infoList[1])
maxValue = float(infoList[2])
limMin = float(infoList[3])
limMax = float(infoList[4])
attrName = 'clinical_spin_' + key
minLimitWidget = self.limSpinDict[attrName + '_min']
maxLimitWidget = self.limSpinDict[attrName + '_max']
if widget is not self.setSpinDict[attrName + '_set']: # handle incoming value from 'set point' spin boxes elsewhere in ui
if isinstance(widget, labelledSpin):
if self.NativeUI.currentMode.replace('/','_').replace('-','_') in widget.cmd_type:
self.setSpinDict[attrName + '_set'].simpleSpin.set_value(
widget.get_value())
elif isinstance(widget, SpinButton):
self.setSpinDict[attrName + '_set'].simpleSpin.set_value(widget.get_value())
if widget.label in self.absoluteLimits:
denominator = 100 # just get difference if looking for absolute limit
else:
denominator = setValue # get percentage if looking for percentage
pct_to_max = 100*(maxValue - setValue)/denominator
pct_to_min = 100*(minValue -setValue)/denominator
# print('maximum is ' + str(limMax))
# print('pct to max is ' + str(pct_to_max))
# print('maxval is ' + str(maxValue))
# print('setVal is ' + str(setValue))
# print('denom is ' + str(denominator))
if round(pct_to_max,4) <= round(limMax,4): # round to avoid errors with floating point numbers
maxLimitWidget.set_maximum(pct_to_max)
elif round(pct_to_min,4) >= round(limMin,4):
minLimitWidget.set_minimum(pct_to_min)
else:
maxLimitWidget.set_maximum(10)
minLimitWidget.set_minimum(-10)
self.refresh_button_colour()
def refresh_button_colour(self):
'''Refresh button colour based on whether there are any manually updated spin widgets or not'''
self.manuallyUpdated = False
for spin in dict(self.limSpinDict, **self.setSpinDict).values():
self.manuallyUpdated = self.manuallyUpdated or spin.manuallyUpdated
for button in self.buttonDict:
if isinstance(self.buttonDict[button], OkSendButtonWidget):
self.buttonDict[button].setColour(str(int(True)))
else:
self.buttonDict[button].setColour(str(int(self.manuallyUpdated)))
def get_mode(self, key: str):
for mode in self.modeList:
if mode in key:
return mode
from global_widgets.global_spinbox import labelledSpin
from widget_library.ok_cancel_buttons_widget import OkButtonWidget, CancelButtonWidget, OkSendButtonWidget
#from global_widgets.global_send_popup import SetConfirmPopup
from widget_library.spin_buttons_widget import SpinButton
from PySide2 import QtWidgets, QtGui, QtCore
from handler_library.handler import PayloadHandler
import logging
import json
class ModeHandler(PayloadHandler):
modeSwitched = QtCore.Signal(str)
UpdateModes = QtCore.Signal(dict)
OpenPopup = QtCore.Signal(PayloadHandler, list)
settingToggle = QtCore.Signal(str)
def __init__(self, NativeUI, *args, **kwargs):
super().__init__(['TARGET'],*args, **kwargs)
#super(TabModes, self).__init__(NativeUI, *args, **kwargs)
self.NativeUI = NativeUI
self.spinDict = {}
self.buttonDict = {}
self.radioDict = {}
self.commandList = []
self.mainSpinDict = {}
self.mainButtonDict = {}
self.modeList = ["PC/AC", "PC/AC-PRVC", "PC-PSV", "CPAP", 'CURRENT']
self.manuallyUpdatedBoolDict = { mode: False for mode in self.modeList }
self.mainManuallyUpdated = False
self.activeMode = self.modeList[0]
with open("NativeUI/configs/mode_config.json") as json_file:
modeDict = json.load(json_file)
self.relevantKeys = [setting[2] for setting in modeDict['settings']]
def add_widget(self, widget, key: str):
if isinstance(widget, labelledSpin):
self.spinDict[key] = widget
if isinstance(widget, OkButtonWidget) or isinstance(widget, CancelButtonWidget) or isinstance(widget, OkSendButtonWidget):
if self.get_mode(key) == 'CURRENT':
self.mainButtonDict[key] = widget
else:
self.buttonDict[key] = widget
if isinstance(widget, QtWidgets.QRadioButton):
self.radioDict[key] = widget
if isinstance(widget, SpinButton):
self.mainSpinDict[key] = widget
def active_payload(self, *args) -> int:
target_data = self.get_db()
outdict = {}
for key in self.relevantKeys:
try:
outdict[key] = target_data[key]
except KeyError:
logging.debug("Invalid key %s in measurement database", key)
self.UpdateModes.emit(outdict)
return 0
def handle_okbutton_click(self, key):
mode = self.get_mode(key)
message, command = [], []
for widget in self.spinDict:
if (mode in widget) and self.spinDict[widget].manuallyUpdated:
setVal = self.spinDict[widget].get_value()
setVal = round(setVal, self.spinDict[widget].decPlaces)
message.append("set" + widget + " to " + str(setVal))
command.append(
[
self.spinDict[widget].cmd_type,
self.spinDict[widget].cmd_code,
setVal,
]
)
if isinstance(self.buttonDict[key], OkSendButtonWidget):
message.append("change mode to " + str(mode))
command.append(["SET_MODE", mode.replace("/", "_").replace("-", "_")])
# create a signal emitting message, command, handler identifier - in nativeui connect to a popup widget
# command sending should occur in handler
self.commandList = command
self.OpenPopup.emit(self,message)
def handle_mainokbutton_click(self):
message, command = [], []
for widget in self.mainSpinDict:
if self.mainSpinDict[widget].manuallyUpdated:
setVal = self.mainSpinDict[widget].get_value()
setVal = round(setVal, self.mainSpinDict[widget].decPlaces)
message.append("set" + widget + " to " + str(setVal))
command.append(
[
self.mainSpinDict[widget].cmd_type,
self.mainSpinDict[widget].cmd_code,
setVal,
]
)
# create a signal emitting message, command, handler identifier - in nativeui connect to a popup widget
# command sending should occur in handler
self.commandList = command
self.OpenPopup.emit(self,message)
def sendCommands(self):
if self.commandList == []:
a=1
else:
for command in self.commandList:
print('sending commands')
print(command)
self.NativeUI.q_send_cmd(*command)
self.modeSwitched.emit(self.activeMode)
self.commandSent()
return 0
def handle_cancel_pressed(self, buttonMode):
for widget in self.spinDict:
if buttonMode in widget:
self.spinDict[widget].manuallyUpdated = False
if buttonMode == self.NativeUI.currentMode:
print('modes match ')
for widget in self.mainSpinDict:
self.mainSpinDict[widget].manuallyUpdated = False
else:
print('do nothing in clinical')
self.active_payload()
self.refresh_button_colour()
self.refresh_main_button_colour()
def commandSent(self):
self.commandList = []
for widget in self.spinDict:
if self.activeMode in widget:
self.spinDict[widget].manuallyUpdated = False
for widget in self.mainSpinDict:
self.mainSpinDict[widget].manuallyUpdated = False
self.active_payload()
self.refresh_button_colour()
self.refresh_main_button_colour()
def handle_manual_change(self, changed_spin_key):
self.active_payload()
self.refresh_button_colour()
self.refresh_main_button_colour()
def handle_radio_toggled(self, radioButtonState, radioKey):
"""TODO Docstring"""
mode = self.get_mode(radioKey)
spinKey= radioKey.replace('radio', 'spin')
spinBox = self.spinDict[spinKey]
spinBox.setEnabled(radioButtonState)
if mode == self.NativeUI.currentMode:
self.settingToggle.emit(spinBox.label)
def refresh_button_colour(self):
self.manuallyUpdatedBoolDict = { mode: False for mode in self.modeList }
for spin in self.spinDict:
mode = self.get_mode(spin)
if mode == None: continue
self.manuallyUpdatedBoolDict[mode] = self.manuallyUpdatedBoolDict[mode] or self.spinDict[spin].manuallyUpdated
for button in self.buttonDict:
mode = str(self.get_mode(button))
if isinstance(self.buttonDict[button], OkSendButtonWidget) and (mode != self.NativeUI.currentMode):
self.buttonDict[button].setColour(str(int(True)))
else:
self.buttonDict[button].setColour(str(int(self.manuallyUpdatedBoolDict[mode])))
def propagate_modevalchange(self,widget):
for spin in self.mainSpinDict.values():
if spin.tag == widget.tag:
if spin.get_value() != widget.get_value():
spin.set_value(widget.get_value())
for spin in self.spinDict.values():
if spin.tag == widget.tag:
if self.NativeUI.currentMode.replace('/','_').replace('-','_') in spin.cmd_type:
if spin.get_value() != widget.get_value():
spin.simpleSpin.set_value(widget.get_value())
def refresh_main_button_colour(self):
self.manuallyUpdatedBoolDict['CURRENT'] = False
for spin in self.mainSpinDict:
self.manuallyUpdatedBoolDict['CURRENT'] = self.manuallyUpdatedBoolDict['CURRENT'] or self.mainSpinDict[spin].manuallyUpdated
for button in self.mainButtonDict:
self.mainButtonDict[button].setColour(str(int(self.manuallyUpdatedBoolDict['CURRENT'])))
def get_mode(self, key: str):
for mode in self.modeList:
if mode in key:
return mode
from global_widgets.global_spinbox import labelledSpin
from widget_library.ok_cancel_buttons_widget import OkButtonWidget, CancelButtonWidget, OkSendButtonWidget
#from global_widgets.global_send_popup import SetConfirmPopup
from widget_library.line_edit_widget import LabelledLineEditWidget
from handler_library.handler import PayloadHandler
from PySide2.QtCore import Signal
from PySide2 import QtWidgets, QtGui, QtCore
class PersonalHandler(PayloadHandler): # chose QWidget over QDialog family because easier to modify
UpdatePersonalDisplay = Signal(dict)
def __init__(self, NativeUI, *args, **kwargs):
super().__init__(['PERSONAL'], *args, **kwargs)
self.NativeUI = NativeUI
self.spinDict = {}
self.buttonDict = {}
self.textDict = {}
def add_widget(self, widget, key: str):
if isinstance(widget, labelledSpin):
self.spinDict[key] = widget
if isinstance(widget, LabelledLineEditWidget):
self.textDict[key] = widget
if isinstance(widget, OkButtonWidget) or isinstance(widget, CancelButtonWidget) or isinstance(widget, OkSendButtonWidget):
self.buttonDict[key] = widget
def active_payload(self, *args, **kwargs) -> int:
"""
When the personal data is updated, extract those parameters needed for display
and emit the UpdatePersonalDisplay signal.
"""
current_data = self.get_db()
outdict = {}
for key in ["name", "patient_id", "age", "sex", "height", "weight"]:
outdict[key] = current_data[key]
self.UpdatePersonalDisplay.emit(outdict)
return 0
\ No newline at end of file
from PySide2 import QtWidgets, QtGui, QtCore
from global_widgets.global_select_button import selectorButton
# from global_widgets.global_spinbox import signallingSpinBox
from global_widgets.template_main_pages import TemplateMainPages
from global_widgets.template_set_values import TemplateSetValues
class TabModes(
TemplateMainPages
): # chose QWidget over QDialog family because easier to modify
def __init__(self, NativeUI, *args, **kwargs):
super(TabModes, self).__init__(NativeUI, *args, **kwargs)
self.NativeUI = NativeUI
self.settingsList = [
[
"Respiratory Rate",
"/min",
"respiratory_rate",
"SET_TARGET_",
"RESPIRATORY_RATE",
],
["Inhale Time", "s", "inhale_time", "SET_TARGET_", "INHALE_TIME"],
["IE Ratio", "", "ie_ratio", "SET_TARGET_", "IE_RATIO"],
[
"Inhale Trigger Sensitivity",
"",
"inhale_trigger_threshold",
"SET_TARGET_",
"INHALE_TRIGGER_THRESHOLD",
],
[
"Exhale Trigger Sensitivity",
"",
"exhale_trigger_threshold",
"SET_TARGET_",
"EXHALE_TRIGGER_THRESHOLD",
],
[
"Inhale Pressure",
"",
"inspiratory_pressure",
"SET_TARGET_",
"INSPIRATORY_PRESSURE",
],
["Inhale Volume", "", "volume", "SET_TARGET_", "VOLUME"],
["Percentage O2", "", "fiO2_percent", "SET_TARGET_", "FIO2_PERCENT"],
]
hlayout = QtWidgets.QHBoxLayout()
hlayout.setSpacing(0)
self.pcacButton = selectorButton(NativeUI, "PC/AC")
self.pcacButton.setProperty("selected", "1")
self.pcacButton.style().polish(self.pcacButton)
self.pcacEnable = [1, 0, 1, 1, 0, 1, 0, 1]
self.pcacVals = [1, 2, 3, 4, 5, 6, 7, 8]
self.pcacPage = TemplateSetValues(NativeUI)
self.prvcButton = selectorButton(NativeUI, "PC/AC-PRVC")
self.prvcEnable = [1, 1, 0, 1, 0, 1, 1, 1]
self.prvcVals = [2, 3, 4, 5, 6, 7, 8, 9]
self.prvcPage = TemplateSetValues(NativeUI)
self.psvButton = selectorButton(NativeUI, "PC-PSV")
self.psvEnable = [1, 1, 0, 1, 0, 1, 0, 1]
self.psvVals = [3, 4, 5, 6, 7, 8, 9, 1]
self.psvPage = TemplateSetValues(NativeUI)
self.cpapButton = selectorButton(NativeUI, "CPAP")
self.cpapEnable = [1, 0, 1, 1, 0, 1, 0, 1]
self.cpapVals = [4, 5, 6, 7, 8, 9, 1, 2]
self.cpapPage = TemplateSetValues(NativeUI)
self.buttonWidgets = [
self.pcacButton,
self.prvcButton,
self.psvButton,
self.cpapButton,
]
enableList = [self.pcacEnable, self.prvcEnable, self.psvEnable, self.cpapEnable]
self.tabsDict = { 'PC/AC':self.pcacPage, 'PC/AC-PRVC':self.prvcPage, 'PC-PSV':self.psvPage, 'CPAP':self.cpapPage}
self.valsList = [self.pcacVals, self.prvcVals, self.psvVals, self.cpapVals]
self.modeList = self.NativeUI.modeList
self.spinDict = {}
for tab, mode, enable, vals in zip(
self.tabsDict.values(), self.modeList, enableList, self.valsList
):
#mode = mode.replace("/", "_")
#mode = mode.replace("-", "_")
tempSettingsList = [
[
target.replace("SET_TARGET_", "SET_TARGET_" + mode.replace("/", "_").replace("-", "_"))
for target in spinInfo
]
for spinInfo in self.settingsList
]
tab.addSpinSingleCol(tempSettingsList)
tab.mode = mode
tab.buttonGroup = QtWidgets.QButtonGroup()
for labelledSpin in tab.spinDict:
if tab.spinDict[labelledSpin].label == "Inhale Time":
tab.radioButtonTime = QtWidgets.QRadioButton()
tab.radioButtonTime.setChecked(bool(enable[1]))
tab.radioButtonTime.toggled.connect(
lambda i=tab.radioButtonTime, j=tab.spinDict[
labelledSpin
], k=tab.mode: self.radioPressed(i, j, k)
)
tab.spinDict[labelledSpin].insertWidget(tab.radioButtonTime, 1)
tab.buttonGroup.addButton(tab.radioButtonTime)
if tab.spinDict[labelledSpin].label == "IE Ratio":
tab.radioButtonRat = QtWidgets.QRadioButton()
tab.radioButtonRat.setChecked(bool(enable[2]))
tab.radioButtonRat.toggled.connect(
lambda i=tab.radioButtonRat, j=tab.spinDict[
labelledSpin
], k=tab.mode: self.radioPressed(i, j, k)
)
tab.spinDict[labelledSpin].insertWidget(tab.radioButtonRat, 1)
tab.buttonGroup.addButton(tab.radioButtonRat)
tab.addModeButtons()
tab.finaliseLayout()
self._setEnabled(tab, enable, vals)
self.spinDict[mode] = tab.spinDict
# self.addRadioButtons()
self.tabsList = self.tabsDict.values()
self.buildPage(self.buttonWidgets, self.tabsList)
# def addRadioButtons(self):
# for tab in self.tabsList:
# tab.buttonGroup = QtWidgets.QButtonGroup()
# for labelledSpin in tab.spinDict:
# if tab.spinDict[labelledSpin].label == "Inhale Time":
# tab.radioButtonTime = QtWidgets.QRadioButton()
# tab.radioButtonTime.toggled.connect(
# lambda i=tab.radioButtonTime, j=tab.spinDict[
# labelledSpin
# ], k=tab.mode: self.radioPressed(i, j, k)
# )
# tab.spinDict[labelledSpin].insertWidget(tab.radioButtonTime, 1)
# tab.buttonGroup.addButton(tab.radioButtonTime)
# if tab.spinDict[labelledSpin].label == "IE Ratio":
# tab.radioButtonRat = QtWidgets.QRadioButton()
# tab.radioButtonRat.toggled.connect(
# lambda i=tab.radioButtonRat, j=tab.spinDict[
# labelledSpin
# ]: self.radioPressed(i, j, k)
# )
# tab.spinDict[labelledSpin].insertWidget(tab.radioButtonRat, 1)
# tab.buttonGroup.addButton(tab.radioButtonRat)
def radioPressed(self, radioButtonState, labelledSpin, tabMode):
labelledSpin.simpleSpin.setEnabled(radioButtonState)
labelledSpin.simpleSpin.setProperty("bgColour", str(int(not radioButtonState)))
labelledSpin.simpleSpin.setProperty(
"textColour", str(int(not radioButtonState))
)
labelledSpin.simpleSpin.style().unpolish(labelledSpin.simpleSpin)
labelledSpin.simpleSpin.style().polish(labelledSpin.simpleSpin)
if tabMode == self.NativeUI.currentMode:
a=1
self.NativeUI.widgets.spin_buttons.setStackWidget(labelledSpin.label)
def _setColour(self, buttonWidg):
for button, box in zip(self.buttonWidgets, self.pageList):
if button == buttonWidg:
button.setProperty("selected", "1")
self.stack.setCurrentWidget(box)
else:
button.setProperty("selected", "0")
button.style().unpolish(button)
button.style().polish(button)
def _modeSwitch(self, box):
self.stack.setCurrentWidget(box)
def _setEnabled(self, box, enableList, values):
for widget, enableBool, value in zip(box.spinDict, enableList, values):
box.spinDict[widget].simpleSpin.setEnabled(enableBool)
if enableBool == 1:
box.spinDict[widget].simpleSpin.setProperty("bgColour", "0")
box.spinDict[widget].simpleSpin.setProperty("textColour", "0")
if enableBool == 0:
box.spinDict[widget].simpleSpin.setProperty("bgColour", "1")
box.spinDict[widget].simpleSpin.setProperty("textColour", "1")
box.spinDict[widget].simpleSpin.style().unpolish(
box.spinDict[widget].simpleSpin
)
box.spinDict[widget].simpleSpin.style().polish(
box.spinDict[widget].simpleSpin
)
box.spinDict[widget].simpleSpin.setValue(value)
#!/usr/bin/env python3
"""
tab_personal.py
"""
__author__ = ["Benjamin Mummery", "Tiago Sarmento"]
__credits__ = ["Benjamin Mummery", "Dónal Murray", "Tim Powell", "Tiago Sarmento"]
__license__ = "GPL"
__version__ = "0.0.1"
__maintainer__ = "Tiago Sarmento"
__email__ = "tiago.sarmento@stfc.ac.uk"
__status__ = "Development"
from PySide2 import QtWidgets, QtGui, QtCore
import sys
sys.path.append("~/Documents/hev/NativeUI/")
from global_widgets.template_set_values import TemplateSetValues
class TabPersonal(TemplateSetValues):
def __init__(self, *args, **kwargs):
super(TabPersonal, self).__init__(*args, **kwargs)
settingsList = [
["Name", "/min", "name", "SET_PERSONAL", "NAME"],
["Patient ID", "s", "patient_id", "SET_PERSONAL", "PATIENT_ID"],
["Age", "", "age", "SET_PERSONAL", "AGE"],
["Sex", "", "sex", "SET_PERSONAL", "SEX"],
["Weight", "", "weight", "SET_PERSONAL", "WEIGHT"],
["Height", "", "height", "SET_PERSONAL", "HEIGHT"],
]
textBoxes = ["Name", "Patient ID", "Sex"]
self.setPacketType("personal")
self.addPersonalCol(settingsList, textBoxes)
self.addButtons()
self.finaliseLayout()
@QtCore.Slot(dict)
def localise_text(self, text: dict) -> int:
self.spinDict["Name"].nameLabel.setText(text["personal_tab_name"])
self.spinDict["Patient ID"].nameLabel.setText(text["personal_tab_patientid"])
self.spinDict["Age"].nameLabel.setText(text["personal_tab_age"])
self.spinDict["Sex"].nameLabel.setText(text["personal_tab_sex"])
self.spinDict["Weight"].nameLabel.setText(text["personal_tab_weight"])
self.spinDict["Height"].nameLabel.setText(text["personal_tab_height"])
return 0
if __name__ == "__main__":
# sys.path.append("../")
app = QtWidgets.QApplication(sys.argv)
widg = TabPersonal()
widg.show()
sys.exit(app.exec_())
#!/usr/bin/env python3
import logging
import os
import pyqtgraph as pg
import numpy as np
from PySide2 import QtWidgets, QtCore
from pyqtgraph import PlotWidget, plot, mkColor
from hevclient import HEVClient
logging.basicConfig(level=logging.WARNING, format='%(asctime)s - %(levelname)s - %(message)s')
class TabPlots(QtWidgets.QWidget):
def __init__(self, port=54322, *args, **kwargs):
super(TabPlots, self).__init__(*args, **kwargs)
self.history_length = 500
self.time_range = 30
self.port = port
layout = QtWidgets.QVBoxLayout()
self.graphWidget = pg.GraphicsLayoutWidget()
layout.addWidget(self.graphWidget)
self.pressurePlot = self.graphWidget.addPlot(title="Pressure")
self.graphWidget.nextRow()
self.flowPlot = self.graphWidget.addPlot(title="Flow")
self.graphWidget.nextRow()
self.volumePlot = self.graphWidget.addPlot(title="Volume")
self.graphWidget.nextRow()
self.timestamp = list(el*(-1) for el in range(self.history_length))[::-1]
self.PID_P = list(0 for _ in range(self.history_length))
self.PID_I = list(0 for _ in range(self.history_length))
self.PID_D = list(0 for _ in range(self.history_length))
self.graphWidget.setBackground(mkColor(30, 30, 30))
# Add grid
self.flowPlot.showGrid(x=True, y=True)
self.volumePlot.showGrid(x=True, y=True)
self.pressurePlot.showGrid(x=True, y=True)
# Set Range
self.flowPlot.setXRange(self.time_range * (-1), 0, padding=0)
self.volumePlot.setXRange(self.time_range * (-1), 0, padding=0)
self.pressurePlot.setXRange(self.time_range * (-1), 0, padding=0)
self.flowPlot.enableAutoRange('y', True)
self.volumePlot.enableAutoRange('y', True)
self.pressurePlot.enableAutoRange('y', True)
# Plot styles
self.line1 = self.plot(self.pressurePlot, self.timestamp, self.PID_P, "Airway Pressure", "077")
self.line2 = self.plot(self.flowPlot, self.timestamp, self.PID_D, "Flow", "00F")
self.line3 = self.plot(self.volumePlot, self.timestamp, self.PID_I, "Volume", "707")
self.setLayout(layout)
self.timer = QtCore.QTimer()
self.timer.setInterval(16) # just faster than 60Hz
self.timer.timeout.connect(self.update_plot_data)
self.timer.start()
def plot(self, canvas, x, y, plotname, color):
pen = pg.mkPen(color=color, width=3)
return canvas.plot(x, y, name=plotname, pen=pen)
def update_plot_data(self):
# subtract latest timestamp and scale to seconds
timestamp = np.true_divide(np.subtract(self.parent().parent().plots[:, 0], self.parent().parent().plots[-1, 0]), 1000)
self.line1.setData(timestamp, self.parent().parent().plots[:, 1])
self.line2.setData(timestamp, self.parent().parent().plots[:, 2])
self.line3.setData(timestamp, self.parent().parent().plots[:, 3])
# Tests Documentation
## Unit Tests
To run all unit tests in the `NativeUI/tests/unittests` dir on a Raspberry Pi or VM, run the following
(adjust the full path to your NativeUI directory accordingly):
```bash
. .hev_env/bin/activate
export PYTHONPATH=/home/pi/hev/NativeUI
pytest NativeUI/tests/unittests
```
To run a single unit test file, set the environment as above and specify the test file:
```bash
pytest NativeUI/tests/unittests/test_hevclient.py
```
## Integration Tests
To run all integration tests, the Arduino emulator and hevserver processes first need to be running, and
then run all integrations tests in the `NativeUI/tests/integration` dir:
```bash
. .hev_env/bin/activate
export PYTHONPATH=/home/pi/hev/NativeUI
./raspberry-dataserver/ArduinoEmulator.py -f raspberry-dataserver/share/B6-20201207.dump &
./raspberry-dataserver/hevserver.py --use-dump-data &
pytest NativeUI/tests/integration
```
### Coverage
To get pytest coverage run from the root of the repo:
```bash
pip install pytest-cov
pytest --cov=NativeUI NativeUI
```
## System Tests
### Template
Status is marked in the test title with:
* :x: for not started
* :large_orange_diamond: for WIP
* :white_check_mark: for completed
RiskID | Domain | Functional Area | Standard Reference | Assignee
------ | ------ | --------------- | ------------------ | --------
SW | Software-GUI | Alarms | ISO-XX | Tim Powell
#### Scenario: <EXAMPLE>
GIVEN the <EVENT>
WHEN the <CAUSE>
THEN the <ACTION>
---
### Low Battery Alarm (10 minutes) :x:
RiskID | Domain | Functional Area | Standard Reference | Assignee
------ | ------ | --------------- | ------------------ | --------
SW8 | Software-GUI | Alarms | ISO80601-2-12:2020 | Tim Powell
#### Scenario: There is only 10 minutes of battery life left
GIVEN the alarm payload comes in
WHEN the alarm is about the battery
THEN check if the low battery alarm signal is sent
AND THEN the low battery alarm is displayed
---
### High Pressure Alarm to be HIGH Priority :large_orange_diamond:
RiskID | Domain | Functional Area | Standard Reference | Assignee
------ | ------ | --------------- | ------------------ | --------
SW11 | Software-GUI | Alarms | ISO80601-2-12:2020 | Tim Powell
#### Scenario: Excessive airway pressure applied
GIVEN the alarm payload received
WHEN the alarm_code: 7
THEN the high priority alarm signal should be sent
AND THEN alarm popup is triggered
AND THEN alarm is added to alarm list
{
"ALARM": {
"version": 182,
"timestamp": 816562,
"payload_type": "ALARM",
"alarm_type": "PRIORITY_MEDIUM",
"alarm_code": "HIGH_VTE",
"param": 24868.025390625
},
"type": "ALARM"
}
\ No newline at end of file
"""
Create hevclient WITH a hevserver running and assert expected hevclient state and function calls.
Make sure your PYTHONPATH var is set to the full path of '/<your_hev_root_dir>/hev/NativeUI'.
"""
import os
import tempfile
import time
import pytest
import hevclient
from hevclient import HEVClient
# Overwrite the mm file for OS agnostic testing
hevclient.mmFileName = tempfile.gettempdir() + os.path.sep + "HEVCLIENT_last_Data.mmap"
def set_up():
"""pytest module setup"""
_assert_posix()
_assert_pythonpath()
def test_hev_client_state(caplog):
"""Assert HEVClient state with background processes running (hevserver/Arduino)"""
HEVClient(True)
time.sleep(1) # give enough time for het to log
# confirm message 'is the microcontroller running?' is NOT logged using err code
assert "[Errno" not in caplog.text
# TODO assert hevclient.method tests
# myhevclient.send_cmd("CMD", "blah")
def _assert_posix():
assert os.name == "posix"
def _assert_pythonpath():
pythonpath_key = "$PYTHONPATH"
pythonpath_val = os.path.expandvars(pythonpath_key)
if pythonpath_val == pythonpath_key:
pytest.fail(msg="Please set the $PYTHONPATH env var")
"""
- give NativeUI.get_updates() a fake payload of a high pressure alarm at high priority.
- that triggers NativeUI.set_alarms_db() which sets __alarms DB
- hev_alarms.updateAlarms() is run regularly to check for new alarms. When new alarms are added to NativeUI.__alarms hev_alarms.updateAlarms() triggers.
- hev_alarms.updateAlarms adds the alarm to the alarmDict, creates an alarm popup, and adds it to the alarm table displayed in the UI.
"""
# NativeUI.get_updates()
# NativeUI.set_alarms_db()
# __alarms DB Updated
# hev_alarms.updateAlarms()
# abstractAlarm
# TODO get a fake payload from the MCU side. Currently (8.4.21) not available as MCU and UI are not linked.
import json
from unittest.mock import patch
import os
import sys
from PySide2.QtWidgets import QApplication
import time
import _thread
current_path = os.path.abspath(os.getcwd())
root_path = os.path.abspath(current_path + "/NativeUI")
sys.path.append(root_path)
print(root_path)
from NativeUI import NativeUI
import hevclient
# TODO:
# - Sort Threading: In order to test the UI, the PyQT app needs to be running. This is controlled by app.exec_() (line 110), however when the app is running the main thread is locked out of firing off any other functions (i.e. test_high_pressure_alarm_high_priority()). Currently, the test_high_pressure_alarm_high_priority() test function runs in a secondary thread which doesn't interact with the app running in the main thread.
# - Assert that the __alarms DB is modified correctly (line 77-79)
def test_high_pressure_alarm_high_priority(NativeUI):
"""
The intention of this test is to solve the risk with Risk ID: SW11.
When excessive airway pressure is applied the microcontroller should send a High Pressure Alarm at a High Priority. This integration test tests from the point when that payload is fed into the UI. It will test that the payload will propgate through NativeUI app correctly and create a popup and add the alarm to the list of alarms.
"""
myNativeUI = NativeUI
# define vars
popup_activated = False
list_activated = False
table_activated = False
# define mock functions
def mock_popup():
nonlocal popup_activated
print("popup here")
popup_activated = True
def mock_list():
nonlocal list_activated
list_activated = True
def mock_table():
nonlocal table_activated
table_activated = True
# replace update_alarms function calls with my own calls
patch.object(myNativeUI.widgets.alarm_tab.popup, "addAlarm", new=mock_popup)
patch.object(myNativeUI.widgets.alarm_tab.list, "addAlarm", new=mock_list)
patch.object(
myNativeUI.widgets.alarm_table_tab.table, "addAlarmRow", new=mock_table
)
# Give NativeUI.get_updates() a fake payload
fake_alarm_json = "tests/integration/fixtures/sw11.json"
fake_payload = json.load(open(fake_alarm_json))
print(fake_payload)
myNativeUI.get_updates(fake_payload)
# Check __alarms is modified
alarm_db = myNativeUI.get_db("alarms")
print("\n***", alarm_db, "\n")
# __define_connections calls alarm_widgets.tab_alarms.update_alarms every 16ms and starts when NativeUI is initialized.
wait_time = 5
count = 0
while count < wait_time:
print("waiting " + str(count + 1) + "s out of 5s")
count = count + 1
time.sleep(1)
pass
# (In Stubs) Capture newAbstractAlarm and query it for alarm code and priority set a flag if so.
print("\nRunning tests \n")
# After wait, check for flags set.
assert popup_activated is True, "popup.addAlarm(newAbstractAlarm) not called."
assert list_activated is True, "list.addAlarm(newAbstractAlarm) not called."
assert table_activated is True, "table.addAlarm(newAbstractAlarm) not called."
return
def main():
hevclient.mmFileName = "tests/integration/fixtures/HEVClient_lastData.mmap"
app = QApplication()
myNativeUI = NativeUI()
_thread.start_new_thread(sw11, (myNativeUI,))
app.exec_()
wait_time = 5
count = 0
while count < wait_time:
print("waiting " + str(count + 1) + "s out of 5s")
count = count + 1
time.sleep(1)
pass
_thread.exit()
sys.exit()
return
if __name__ == "__main__":
main()
{
"version": 182,
"timestamp": 0,
"type": "BATTERY",
"bat": 0,
"ok": 1,
"alarm": 0,
"rdy2buf": 0,
"bat85": 0,
"prob_elec": 0,
"dummy": false
}
\ No newline at end of file
{
"on_mains_power": true,
"on_battery_power": false,
"battery_percent": 0.0,
"electrical_problem": null
}
\ No newline at end of file
{"respiratory_rate": 0, "tidal_volume": 0, "exhaled_minute_volume": 0, "inhaled_minute_volume": 0, "minute_volume": 0, "exhaled_tidal_volume": 0, "inhaled_tidal_volume": 0, "lung_compliance": 0, "static_compliance": 0, "inhalation_pressure": 0, "peak_inspiratory_pressure": 0, "plateau_pressure": 0, "mean_airway_pressure": 0, "fiO2_percent": 0, "apnea_index": 0, "apnea_time": 0, "mandatory_breath": 0}
\ No newline at end of file
{"name": "Justin Atest", "patient_id": "15243", "age": "22", "sex": "Testgender", "height": 1.77, "weight": 80}
\ No newline at end of file
{
"type": "TEST",
"TEST":{
"version": 182,
"timestamp": 0,
"type": "TEST",
"attribute_1": true,
"attribute_2": 1,
"attribute_3": 0.579
}
}
#! /usr/bin/env python3
"""
Unit tests for NativeUI
"""
import json
import sys
import hevclient
import numpy as np
import pytest
from PySide2.QtWidgets import QApplication
from NativeUI import NativeUI
sys.path.append("../..")
hevclient.mmFileName = (
"/home/pi/hev/NativeUI/tests/integration/fixtures/HEVClient_lastData.mmap"
)
@pytest.fixture(scope="session", autouse=True)
def widget():
app = QApplication(sys.argv)
return NativeUI()
# Test default values of databases(no set method involved)
# Superseeded by handlers.
# def test_must_return_if_raises_attribute_error_when_false_db_item_is_got_from_get_db(
# widget,
# ):
# with pytest.raises(AttributeError):
# widget.get_db("__false_db_item")
# def test_must_return_correct_db_item_from_get_db_data(widget):
# assert widget.get_db("__data") == {} and widget.get_db("data") == {}
# def test_must_return_correct_db_item_from_get_db_readback(widget):
# assert widget.get_db("__readback") == {} and widget.get_db("readback") == {}
# def test_must_return_correct_db_item_from_get_db_cycle(widget):
# assert widget.get_db("__cycle") == {} and widget.get_db("cycle") == {}
# def test_must_return_correct_db_item_from_get_db_battery(widget):
# assert widget.get_db("__battery") == {} and widget.get_db("battery") == {}
# def test_must_return_correct_db_item_from_get_db_plots(widget):
# plot_history_length = 1000
# plot_dict = {
# "data": np.zeros((plot_history_length, 5)),
# "timestamp": list(el * (-1) for el in range(plot_history_length))[::-1],
# "pressure": list(0 for _ in range(plot_history_length)),
# "flow": list(0 for _ in range(plot_history_length)),
# "volume": list(0 for _ in range(plot_history_length)),
# "pressure_axis_range": [0, 20],
# "flow_axis_range": [-40, 80],
# "volume_axis_range": [0, 80],
# }
# assert (
# widget.get_db("__plots").keys() == plot_dict.keys()
# and widget.get_db("plots").keys() == plot_dict.keys()
# )
# def test_must_return_correct_db_item_from_get_db_alarms(widget):
# assert widget.get_db("__alarms") == {} and widget.get_db("alarms") == {}
# def test_must_return_correct_db_item_from_get_db_targets(widget):
# assert widget.get_db("__targets") == {} and widget.get_db("targets") == {}
# def test_must_return_correct_db_item_from_get_db_personal(widget):
# assert widget.get_db("__personal") == {} and widget.get_db("personal") == {}
# # Test set methods with sample payloads
# def test_must_return_0_for_set_data_db(widget):
# with open("/home/pi/hev/samples/dataSample.json", "r") as f:
# data_payload = json.load(f)
# assert widget.__set_db("data", data_payload) == 0
# def test_must_return_0_for_set_targets_db(widget):
# with open("/home/pi/hev/samples/targetSample.json", "r") as g:
# target_payload = json.load(g)
# assert widget.__set_db("targets", target_payload) == 0
# def test_must_return_0_for_set_readback_db(widget):
# with open("/home/pi/hev/samples/readbackSample.json", "r") as f:
# readback_payload = json.load(f)
# assert widget.__set_db("readback", readback_payload) == 0
# def test_must_return_0_for_set_cycle_db(widget):
# with open(
# "/home/pi/hev/NativeUI/tests/unittests/fixtures/cycleSample.json", "r"
# ) as f:
# cycle_payload = json.load(f)
# assert widget.__set_db("cycle", cycle_payload) == 0
# def test_must_return_0_for_set_battery_db(widget):
# with open("/home/pi/hev/samples/batterySample.json", "r") as f:
# battery_payload = json.load(f)
# assert widget.__set_db("battery", battery_payload) == 0
# def test_must_return_0_for_set_plots_db(widget):
# with open("/home/pi/hev/samples/dataSample.json", "r") as f:
# data_payload = json.load(f)
# assert widget.__set_plots_db(data_payload) == 0
# def test_must_return_error_if_not_data_is_sent_as_payload_for_set_plots_db(widget):
# with open("/home/pi/hev/samples/batterySample.json", "r") as f:
# battery_payload = json.load(f)
# with pytest.raises(KeyError):
# widget.__set_plots_db(battery_payload)
# def test_must_return_0_when__update_plot_ranges_correctly(widget):
# assert widget.__update_plot_ranges() == 0
# def test_must_return_0_for_set_alarms_db(widget):
# with open("/home/pi/hev/samples/alarmSample.json", "r") as f:
# alarm_payload = json.load(f)
# assert widget.__set_db("alarms", alarm_payload) == 0
# def test_must_return_0_for_set_personal_db(widget):
# with open(
# "/home/pi/hev/NativeUI/tests/unittests/fixtures/personalSample.json", "r"
# ) as f:
# personal_payload = json.load(f)
# assert widget.__set_db("personal", personal_payload) == 0
# Asyncio can handle event loops, but we need to add more interaction i think
# @pytest.mark.asyncio
# async def test_start_client(widget):
# with pytest.raises(RuntimeError):
# widget.start_client()
def test_get_updates_data_payload(widget):
"""
Currently fails because the dataSample.json is only part of the data payload.
"""
with open("/home/pi/hev/samples/dataSample.json", "r") as f:
data_payload = json.load(f)
widget.get_updates(data_payload)
def test_get_updates_wrong_payload(widget):
fake_payload = {
"types": "Fake",
"pressure": 1200000.0,
"flow": 777000.0,
"volume": 1.0,
}
with pytest.raises(KeyError):
widget.get_updates(fake_payload)
def test_must_return_0_when_q_send_cmd(widget):
assert widget.q_send_cmd(str, str) == 0
def test_must_return_0_when_q_ack_alarm_when_out_conection(widget):
with pytest.raises(ConnectionError):
widget.q_ack_alarm(str)
def test_must_return_0_when_q_send_personal_when_out_conection(widget):
with pytest.raises(ConnectionError):
widget.q_send_personal(str)
def test_must_return_0_when__find_icons_png_directory(widget):
assert widget.__find_icons("png") == "/home/pi/hev/hev-display/assets/png"
def test_must_return_0_when__find_icons_svg_directory(widget):
assert widget.__find_icons("svg") == "/home/pi/hev/hev-display/assets/svg"
def test_must_return_0_when_cannot__find_icons_directory(widget):
with pytest.raises(FileNotFoundError):
widget.__find_icons("images")
"""
Unit tests for the handler files.
"""
import json
import os
from unittest.mock import MagicMock, patch
from handler_library.handler import Handler
from handler_library.battery_handler import BatteryHandler
def test_handler():
"""
Tests the default handler.
Test for set_db and get_db to set the database from a given payload and compare the
db imported from get_db.
Test for if active_payload gets fired when set_db is called.
"""
# Initalise the handler and import sample test json file
handler = Handler(["TEST"])
test_json_file_path = (
os.environ["PYTHONPATH"].split(os.pathsep)[0]
+ "/tests/unittests/fixtures/testSample.json"
)
test_json = json.load(open(test_json_file_path))
# Set the database for the imported json and get the database imported
set_db_return = handler.set_db(test_json)
db = handler.get_db()
# Check if the input payload and output database are the same
if test_json["TEST"] == db:
payload_database_comparison = True
else:
payload_database_comparison = False
# Mock active_payload to return true if it is called.
handler.active_payload = MagicMock(return_value=True)
# Check whether conditions have been met to pass test
assert set_db_return == 0, "set_db does not return 0 for a valid payload"
assert (
handler.active_payload() is True
), "active_payload was not called when set_db was run."
assert (
payload_database_comparison is True
), "set_db does not set the inputted payload to the database."
def test_battery_handler():
"""
Tests the battery_handler logic by giving active_payload a sample battery payload (NativeUI/tests/unittests/fixtures/batterySample.json) with a known output status (NativeUI/tests/unittests/fixtures/battery_status_output_sample.json).
"""
# Initalise the battery handler and import sample battery json file
batt_handler = BatteryHandler()
batt_json_file_path = (
os.environ["PYTHONPATH"].split(os.pathsep)[0]
+ "/tests/unittests/fixtures/batterySample.json"
)
batt_json = json.load(open(batt_json_file_path))
# Set true/false variables
UpdateBatteryDisplay_activated = False
new_status_correctly_set = False
batt_per_1_correctly_set = False
batt_per_0_correctly_set = False
# Compute battery percent payload information
batt_per_1 = {"bat85": 1}
batt_per_0 = {"bat85": 0}
batt_handler.compute_battery_percent(batt_per_0)
# Mock the get_db function to give the sample input json
batt_handler.get_db = MagicMock(return_value=batt_json)
# Mock function to replace UpdateBatteryDisplay which checks if the function has been called and whether the output status is correct.
def mock_UpdateBatteryDisplay(new_status: dict):
nonlocal UpdateBatteryDisplay_activated
nonlocal new_status_correctly_set
# Set activated variable to true to show that this function was called
UpdateBatteryDisplay_activated = True
# Check whether new_status is the expected output
expected_status_file_path = (
os.environ["PYTHONPATH"].split(os.pathsep)[0]
+ "/tests/unittests/fixtures/battery_status_output_sample.json"
)
expected_status = json.load(open(expected_status_file_path))
if new_status == expected_status:
new_status_correctly_set = True
else:
pass
# Connect to active_payload signal
batt_handler.UpdateBatteryDisplay.connect(mock_UpdateBatteryDisplay)
# Run the battery handler logic
batt_handler.active_payload()
# Run the battery handler compute battery percent logic
if batt_handler.compute_battery_percent(batt_per_1) == 85.0:
batt_per_1_correctly_set = True
if batt_handler.compute_battery_percent(batt_per_0) == 0.0:
batt_per_0_correctly_set = True
# Check whether conditions have been met to pass test
assert (
UpdateBatteryDisplay_activated is True
), "UpdateBatteryDisplay.emit(new_status) is not called."
assert (
new_status_correctly_set is True
), "Output of new_status does not match the expected output."
assert (
batt_per_1_correctly_set is True
), "compute_battery_percent does not return 85% when bat85 is set to 1."
assert (
batt_per_0_correctly_set is True
), "compute_battery_percent does not return 0% when bat85 is set to 0."
"""
Create hevclient WITHOUT a hevserver running and assert expected hevclient state.
Make sure your PYTHONPATH var is set to the full path of '/<your_hev_root_dir>/hev/NativeUI'.
"""
import tempfile
import os
import time
import hevclient
from hevclient import HEVClient
import pytest
# Overwrite the mm file for OS agnostic testing
hevclient.mmFileName = tempfile.gettempdir() + os.path.sep + "HEVCLIENT_last_Data.mmap"
def setup_module():
"""pytest module setup"""
_assert_posix()
_assert_pythonpath()
def test_hev_client_expected_default_state(caplog):
"""Create the HEVClient in isolation without a hevserver running"""
myhevclient = HEVClient()
_hev_client_expected_state(myhevclient, caplog)
def test_hev_client_expected_log_error_on_command(caplog):
"""Create the HEVClient in isolation without a hevserver running"""
myhevclient = HEVClient(False)
myhevclient.send_cmd("CMD", "fake")
_hev_client_expected_state(myhevclient, caplog)
def _hev_client_expected_state(myhevclient: HEVClient, caplog):
assert myhevclient.get_values() is None # probably should return empty dict
assert len(myhevclient.get_alarms()) == 0
assert myhevclient.get_updates(None) is None
assert myhevclient.get_cycle() is None
assert myhevclient.get_logmsg() is None
time.sleep(1) # wait for the async log to be written
for record in caplog.records:
assert record.levelname != "CRITICAL"
assert (
"[Errno" in caplog.text
) # confirm message 'is the microcontroller running?' is logged
# Can't specify an err code as these are different across devices
def _assert_posix():
assert os.name == "posix"
def _assert_pythonpath():
pythonpath_key = "$PYTHONPATH"
pythonpath_val = os.path.expandvars(pythonpath_key)
if pythonpath_val == pythonpath_key:
pytest.fail(msg="Please set the $PYTHONPATH env var")
#!/usr/bin/env python3
"""
ui_layout.py
Contains the layout logic used in NativeUI.
"""
__author__ = "Benjamin Mummery"
__credits__ = ["Benjamin Mummery", "Dónal Murray", "Tim Powell", "Tiago Sarmento"]
__license__ = "GPL"
__version__ = "0.0.1"
__maintainer__ = "Benjamin Mummery"
__email__ = "benjamin.mummery@stfc.ac.uk"
__status__ = "Prototype"
from PySide2 import QtWidgets, QtCore, QtGui
# from PySide2.QtGui import QFont, QSizePolicy
from widget_library.switchable_stack_widget import SwitchableStackWidget
import json
# from widget_library.page_stack_widget import PageStackWidget
class Layout:
"""
Contains all of the layout logic for the UI.
global_layout and layout_page_* (so far main, alarms, settings, mode) are the only
methods that should reference specific widgets, everything else should have the
widgets passed into them. This keeps all of the widget choices at the page level.
"""
def __init__(self, NativeUI, widgets, *args, **kwargs):
self.NativeUI = NativeUI
self.widgets = widgets
self.text_font = NativeUI.text_font
self.screen_width = NativeUI.screen_width
self.screen_height = NativeUI.screen_height
# Define sizes
# Global
self.widget_spacing = max(
[int(self.screen_height / 192), 5]
) # 10 for 1920x1080
self.measurement_widget_size_ratio = 1 / 0.46
self.top_bar_height = int(self.screen_height / 14.4) # 75 for 1920x1080
self.left_bar_width = int(self.screen_width / 12.8) # 150 for 1920x1080
# main page
self.main_page_bottom_bar_height = int(
self.screen_height / 7.2
) # 150 for 1920x1080
self.main_page_normal_measurements_width = int(
self.screen_width / 7.68
) # 250 for 1920x1080
self.main_page_detailed_measurement_width = (
self.main_page_normal_measurements_width * 2
)
self.construct_page_widgets()
# Popups
NativeUI.widgets.alarm_popup.setFont(self.NativeUI.text_font)
def construct_page_widgets(self) -> int:
"""
Build all of the main pages
"""
self.widgets.add_widget(self.layout_page_main(), "main_page")
self.widgets.add_widget(self.layout_page_alarms(), "alarms_page")
self.widgets.add_widget(self.layout_page_settings(), "settings_page")
self.widgets.add_widget(self.layout_page_modes(), "modes_page")
return 0
def startup_layout(self):
v_layout = QtWidgets.QVBoxLayout()
h_layout = QtWidgets.QHBoxLayout()
h_button_layout = QtWidgets.QHBoxLayout()
# Stack the data collection pages.
self.widgets.add_widget(
SwitchableStackWidget(
self.NativeUI.colors,
self.NativeUI.text,
[
self.layout_mode_startup(),
self.layout_mode_personal("startup_", False),
self.layout_startup_confirmation(),
],
[
"button_label_modes_mode",
"button_label_modes_personal",
"button_label_modes_summary",
],
),
"startup_stack",
)
self.widgets.startup_stack.setFont(self.NativeUI.text_font)
# Add buttons
h_button_layout.addWidget(self.NativeUI.widgets.backButton)
h_button_layout.addWidget(self.NativeUI.widgets.skipButton)
h_button_layout.addWidget(self.NativeUI.widgets.nextButton)
# Put the layouts together
h_layout.addWidget(self.layout_startup_main())
h_layout.addWidget(self.widgets.startup_stack)
v_layout.addLayout(h_layout)
v_layout.addLayout(h_button_layout)
# Ensure that next and skip buttons are disabled by default.
self.NativeUI.widgets.skipButton.setEnabled(False)
self.NativeUI.widgets.nextButton.setEnabled(False)
return v_layout
def layout_startup_main(self):
vlayout = QtWidgets.QVBoxLayout()
vlayout.addWidget(self.widgets.calibration)
vlayout.addWidget(self.widgets.leak_test)
vlayout.addWidget(self.widgets.maintenance)
widg = QtWidgets.QWidget()
widg.setLayout(vlayout)
return widg
def global_layout(self):
hlayout = QtWidgets.QHBoxLayout()
vlayout = QtWidgets.QVBoxLayout()
# Define Sizes
f_mode = 0.25
f_battery = 0.2
f_localisation = 0.1
f_personal = 1 - (f_mode + f_battery + f_localisation)
mode_display_width = int(self.screen_width * f_mode)
personal_display_width = int(self.screen_width * f_personal)
localisation_display_width = int(self.screen_width * f_localisation)
battery_display_width = int(self.screen_width * f_battery)
# Define the stack of pages (used by the page buttons to set the current page)
self.widgets.add_widget(
self.__make_stack(
[
self.widgets.main_page,
self.widgets.settings_page,
self.widgets.alarms_page,
self.widgets.modes_page,
]
),
"page_stack",
)
self.widgets.plot_stack.setFont(self.NativeUI.text_font)
# Populate the Left Bar
hlayout.addWidget(
self.layout_left_bar(
[
self.widgets.page_buttons,
self.widgets.ventilator_start_stop_buttons_widget,
]
)
)
self.widgets.page_buttons.set_size(
self.left_bar_width, None, spacing=self.widget_spacing
)
self.widgets.ventilator_start_stop_buttons_widget.set_size(
self.left_bar_width, None, spacing=self.widget_spacing
)
self.widgets.ventilator_start_stop_buttons_widget.setFont(
self.NativeUI.text_font
)
# Add the page stack
hlayout.addWidget(self.widgets.page_stack)
# Populate the Top Bar
vlayout.addWidget(
self.layout_top_bar(
[
self.widgets.tab_modeswitch,
self.widgets.personal_display,
self.widgets.localisation_button,
self.widgets.battery_display,
self.widgets.lock_button,
]
)
)
self.widgets.tab_modeswitch.set_size(
mode_display_width, self.top_bar_height, spacing=self.widget_spacing
)
# self.widgets.tab_modeswitch.setFont()
self.widgets.personal_display.set_size(
personal_display_width, self.top_bar_height, spacing=self.widget_spacing
)
self.widgets.personal_display.setFont(self.NativeUI.text_font)
self.widgets.localisation_button.set_size(
localisation_display_width, self.top_bar_height, spacing=self.widget_spacing
)
self.widgets.localisation_button.setFont(self.NativeUI.text_font)
self.widgets.battery_display.set_size(
battery_display_width, self.top_bar_height, spacing=self.widget_spacing
)
self.widgets.battery_display.setFont(self.NativeUI.text_font)
vlayout.addLayout(hlayout)
return vlayout
def layout_page_main(self) -> QtWidgets.QWidget:
"""
Define the page_main widget layout, populate it, and set the sizes of the
various widgets contained.
Setting size for subwidgets is done here so as to keep the layouts in
layout_tab_main_normal and layout_tab_main_detailed abstracted.
"""
# Create the normal view
tab_main_normal = self.layout_tab_main_normal(
[self.widgets.normal_plots, self.widgets.normal_measurements]
)
self.widgets.normal_measurements.set_size( # Fix the size of the measurements
self.main_page_normal_measurements_width, # but allow plots to expand to
None, # fill the available space.
widget_size_ratio=self.measurement_widget_size_ratio,
spacing=self.widget_spacing,
)
self.widgets.normal_measurements.set_label_font(self.NativeUI.text_font)
self.widgets.normal_measurements.set_value_font(self.NativeUI.value_font)
# Create the detailed view
tab_main_detailed = self.layout_tab_main_detailed(
[
self.widgets.detailed_plots,
self.widgets.detailed_measurements,
self.widgets.circle_plots,
]
)
self.widgets.detailed_measurements.set_size(
self.main_page_detailed_measurement_width,
None,
widget_size_ratio=self.measurement_widget_size_ratio,
)
self.widgets.detailed_measurements.set_label_font(self.NativeUI.text_font)
self.widgets.detailed_measurements.set_value_font(self.NativeUI.value_font)
# Put the normal and detailed views into a switchable stack
self.widgets.add_widget(
SwitchableStackWidget(
self.NativeUI.colors,
self.NativeUI.text,
[tab_main_normal, tab_main_detailed],
["button_label_main_normal", "button_label_main_detailed"],
),
"plot_stack",
)
# Create and populate the full page layout
page_main = QtWidgets.QWidget()
page_main_layout = QtWidgets.QVBoxLayout()
page_main_center_layout = QtWidgets.QHBoxLayout()
page_main_bottom_layout = QtWidgets.QHBoxLayout()
spin_buttons = self.layout_main_spin_buttons()
center_widgets = [self.widgets.plot_stack]
bottom_widgets = [self.widgets.history_buttons, spin_buttons]
self.widgets.history_buttons.set_size(
None, self.main_page_bottom_bar_height, spacing=self.widget_spacing
)
self.widgets.history_buttons.setFont(self.NativeUI.text_font)
for widget in center_widgets:
page_main_center_layout.addWidget(widget)
for widget in bottom_widgets:
page_main_bottom_layout.addWidget(widget)
page_main_layout.addLayout(page_main_center_layout)
page_main_layout.addLayout(page_main_bottom_layout)
page_main.setLayout(page_main_layout)
return page_main
def layout_page_alarms(self) -> QtWidgets.QWidget:
"""
Layout for the alarms page.
"""
alarm_tab_widgets = [self.widgets.alarm_list, self.widgets.acknowledge_button]
alarm_table_tab_widgets = [self.widgets.alarm_table]
page_alarms = SwitchableStackWidget(
self.NativeUI.colors,
self.NativeUI.text,
[
self.layout_tab_alarm_list(alarm_tab_widgets),
self.layout_tab_alarm_table(alarm_table_tab_widgets),
self.layout_tab_clinical_limits(),
],
[
"button_label_alarms_list",
"button_label_alarms_table",
"button_label_alarms_clinical",
],
)
page_alarms.setFont(self.NativeUI.text_font)
return page_alarms
def layout_page_settings(self) -> QtWidgets.QWidget:
"""
Layout for the settings page.
"""
# Create the Charts tab
tab_charts = self.layout_tab_charts(
[self.widgets.charts_widget, self.widgets.chart_buttons_widget]
)
self.widgets.chart_buttons_widget.setFont(self.NativeUI.text_font)
self.widgets.chart_buttons_widget.set_size(
self.left_bar_width, None, spacing=self.widget_spacing
)
# Create the system info tab
sysinfo_widgets = [
self.widgets.version_display_widget,
self.widgets.maintenance_time_display_widget,
self.widgets.update_time_display_widget,
]
tab_info = self.layout_tab_info(sysinfo_widgets)
for widget in sysinfo_widgets:
widget.setFont(self.NativeUI.text_font)
# Create the stack
page_settings = SwitchableStackWidget(
self.NativeUI.colors,
self.NativeUI.text,
[tab_charts, tab_info,self.layout_settings_expert(),],
[
"button_label_settings_charts",
"button_label_settings_info",
"button_label_settings_expert",
],
)
page_settings.setFont(self.NativeUI.text_font)
self.widgets.add_widget(page_settings, "setting_stack")
return page_settings
def layout_page_modes(self) -> QtWidgets.QWidget:
"""
Layout for the Modes page.
"""
# self.widgets.add_widget(
# SwitchableStackWidget(
# self.NativeUI,
# [QtWidgets.QLabel("1"), QtWidgets.QLabel("2"), QtWidgets.QLabel("3"), QtWidgets.QLabel("4")],
# ["PC/AC", "PC/AC-PRVC", "PC-PSV", "CPAP"]
# ),
# "mode_settings_tab"
# )
modes_stack = SwitchableStackWidget(
self.NativeUI.colors,
self.NativeUI.text,
[
self.layout_mode_settings(True),
self.layout_mode_personal("", True),
], # self.widgets.mode_personal_tab],
["button_label_modes_mode", "button_label_modes_personal"],
)
modes_stack.setFont(self.NativeUI.text_font)
self.widgets.add_widget(modes_stack, "modes_stack")
return modes_stack
def layout_top_bar(self, widgets: list) -> QtWidgets.QWidget:
"""
Construct the layout for the global top bar
"""
assert len(widgets) > 0
top_bar = QtWidgets.QWidget()
top_bar_layout = QtWidgets.QHBoxLayout(top_bar)
for widget in widgets:
top_bar_layout.addWidget(widget)
top_bar.setLayout(top_bar_layout)
return top_bar
def layout_left_bar(self, widgets: list) -> QtWidgets.QWidget:
"""
Construct the layout for the global left bar
"""
left_bar = QtWidgets.QWidget()
left_bar_layout = QtWidgets.QVBoxLayout(left_bar)
for widget in widgets:
left_bar_layout.addWidget(widget)
left_bar_layout.setSpacing(0)
left_bar_layout.setContentsMargins(0, 0, 0, 0)
left_bar.setLayout(left_bar_layout)
return left_bar
def layout_tab_main_normal(self, widgets: list) -> QtWidgets.QWidget:
"""
Construct the layout for the 'normal' plots and measurements display.
"""
tab_main_normal = QtWidgets.QWidget()
tab_main_normal_layout = QtWidgets.QHBoxLayout(tab_main_normal)
for widget in widgets:
tab_main_normal_layout.addWidget(widget)
tab_main_normal.setLayout(tab_main_normal_layout)
return tab_main_normal
def layout_tab_main_detailed(self, widgets: list) -> QtWidgets.QWidget:
"""
Construct the layout for the 'detailed' plots and measurements display.
"""
tab_main_detailed = QtWidgets.QWidget()
tab_main_detailed_layout = QtWidgets.QHBoxLayout(tab_main_detailed)
for widget in widgets:
tab_main_detailed_layout.addWidget(widget)
tab_main_detailed.setLayout(tab_main_detailed_layout)
return tab_main_detailed
def layout_tab_charts(self, widgets: list) -> QtWidgets.QWidget:
"""
Construct the layout for the charts page.
"""
tab_charts = QtWidgets.QWidget()
tab_charts_layout = QtWidgets.QHBoxLayout(tab_charts)
for widget in widgets:
tab_charts_layout.addWidget(widget)
tab_charts.setLayout(tab_charts_layout)
return tab_charts
def layout_tab_info(self, widgets: list) -> QtWidgets.QWidget:
"""
Construct the layout for the info page.
"""
tab_info = QtWidgets.QWidget()
tab_info_layout = QtWidgets.QVBoxLayout(tab_info)
for widget in widgets:
tab_info_layout.addWidget(widget)
tab_info.setLayout(tab_info_layout)
return tab_info
def __make_stack(self, widgets):
"""
Make a stack of widgets
"""
stack = QtWidgets.QStackedWidget()
for widget in widgets:
stack.addWidget(widget)
return stack
def layout_tab_alarm_list(self, widgets: list) -> QtWidgets.QWidget:
"""
Construct the layout for the 'normal' plots and measurements display.
"""
tab_alarm_list = QtWidgets.QWidget()
tab_alarm_list_layout = QtWidgets.QHBoxLayout(tab_alarm_list)
for widget in widgets:
tab_alarm_list_layout.addWidget(widget)
tab_alarm_list.setLayout(tab_alarm_list_layout)
return tab_alarm_list
def layout_tab_alarm_table(self, widgets: list) -> QtWidgets.QWidget:
"""
Construct the layout for the 'normal' plots and measurements display.
"""
tab_alarm_table = QtWidgets.QWidget()
tab_alarm_table_layout = QtWidgets.QHBoxLayout(tab_alarm_table)
for widget in widgets:
tab_alarm_table_layout.addWidget(widget)
tab_alarm_table.setLayout(tab_alarm_table_layout)
return tab_alarm_table
def layout_mode_settings(self, buttons) -> QtWidgets.QWidget:
"""
Construct the layout for the mode pages
"""
mode_pages = [] # enableDict may need to go elsewhere
with open("NativeUI/configs/mode_config.json") as json_file:
modeDict = json.load(json_file)
enableDict = modeDict["enableDict"]
buttons = True
for mode in self.NativeUI.modeList:
mode_pages.append(
self.layout_mode_tab(
modeDict["settings"], mode, "", enableDict[mode], buttons
)
)
page_modes = SwitchableStackWidget(
self.NativeUI.colors, self.NativeUI.text, mode_pages, self.NativeUI.modeList
)
self.widgets.add_widget(page_modes, "mode_settings_stack")
page_modes.setFont(self.NativeUI.text_font)
return page_modes
def layout_mode_tab(
self, settings, mode: str, startup: str, enableList: list, buttons: bool
) -> QtWidgets.QWidget:
"""
Construct the layout for an individual mode setting tab
"""
spinList = []
for setting in settings:
attrName = "spin_" + mode + startup + "_" + setting[2]
spinList.append(self.NativeUI.widgets.get_widget(attrName))
if len(spinList) != len(enableList):
print("lengths do not match, error!")
print(spinList)
print(enableList)
radioWidgets = ["Inhale Time", "IE Ratio"]
vLayout = QtWidgets.QVBoxLayout()
for widget, enableBool in zip(spinList, enableList):
vLayout.addWidget(widget)
if widget.label in radioWidgets:
self.NativeUI.widgets.get_widget(
"radio_" + mode + startup + "_" + widget.tag
).setChecked(bool(enableBool))
self.NativeUI.widgets.get_widget(
"spin_" + mode + startup + "_" + widget.tag
).insertWidget(
self.NativeUI.widgets.get_widget(
"radio_" + mode + startup + "_" + widget.tag
),
1,
)
self.NativeUI.widgets.get_widget(
"spin_" + mode + startup + "_" + widget.tag
).setEnabled(bool(enableBool))
if buttons == True:
hButtonLayout = QtWidgets.QHBoxLayout()
hButtonLayout.addWidget(
self.NativeUI.widgets.get_widget("ok_button_" + mode)
)
hButtonLayout.addWidget(
self.NativeUI.widgets.get_widget("ok_send_button_" + mode)
)
hButtonLayout.addWidget(
self.NativeUI.widgets.get_widget("cancel_button_" + mode)
)
vLayout.addLayout(hButtonLayout)
mode_tab = QtWidgets.QWidget()
mode_tab.setLayout(vLayout)
return mode_tab
def layout_mode_startup(self) -> QtWidgets.QWidget:
"""
Construct the layout for the mode pages
"""
mode_pages = [] # enableDict may need to go elsewhere
with open("NativeUI/configs/mode_config.json") as json_file:
modeDict = json.load(json_file)
enableDict = modeDict["enableDict"]
for mode in self.NativeUI.modeList:
mode_pages.append(
self.layout_mode_tab(
modeDict["settings"], mode, "_startup", enableDict[mode], False
)
)
mode_stack = SwitchableStackWidget(
self.NativeUI.colors, self.NativeUI.text, mode_pages, self.NativeUI.modeList
)
mode_stack.setFont(self.NativeUI.text_font)
self.widgets.add_widget(mode_stack, "mode_settings_stack_startup")
hRadioLayout = QtWidgets.QHBoxLayout()
for mode in self.NativeUI.modeList:
hRadioLayout.addWidget(
self.NativeUI.widgets.get_widget("startup_radio_" + mode)
)
vlayout = QtWidgets.QVBoxLayout()
vlayout.addWidget(mode_stack)
vlayout.addLayout(hRadioLayout)
page_modes = QtWidgets.QWidget()
page_modes.setLayout(vlayout)
return page_modes
def layout_mode_personal(self, startup: str, buttons: bool):
"""
Construct the layout for the personal settings page
"""
with open("NativeUI/configs/personal_config.json") as json_file:
personalDict = json.load(json_file)
textBoxes = personalDict["textBoxes"]
personalList = []
for setting in personalDict["settings"]:
attrName = startup + "personal_edit_" + setting[2]
if setting[0] in textBoxes:
personalList.append(
self.NativeUI.widgets.get_widget("text_" + attrName)
)
else:
personalList.append(
self.NativeUI.widgets.get_widget("spin_" + attrName)
)
vLayout = QtWidgets.QVBoxLayout()
for widget in personalList:
vLayout.addWidget(widget)
if buttons:
hButtonLayout = QtWidgets.QHBoxLayout()
hButtonLayout.addWidget(
self.NativeUI.widgets.get_widget("ok_button_personal")
)
hButtonLayout.addWidget(
self.NativeUI.widgets.get_widget("ok_send_button_personal")
)
hButtonLayout.addWidget(
self.NativeUI.widgets.get_widget("cancel_button_personal")
)
vLayout.addLayout(hButtonLayout)
personal_tab = QtWidgets.QWidget()
personal_tab.setLayout(vLayout)
return personal_tab
def layout_settings_expert(self):
"""
Construct the layout for the expert settings page, reads controlDict.json to do so
"""
vlayout = QtWidgets.QVBoxLayout()
i = 0
with open("NativeUI/configs/expert_config.json") as json_file:
controlDict = json.load(json_file)
for key in controlDict.keys():
titleLabel = self.NativeUI.widgets.get_widget("expert_label_" + key)
titleLabel.setStyleSheet(
"background-color:"
+ self.NativeUI.colors["page_background"].name()
+ ";"
"color:" + self.NativeUI.colors["page_foreground"].name() + ";"
)
titleLabel.setFont(self.text_font)
titleLabel.setAlignment(QtCore.Qt.AlignCenter)
vlayout.addWidget(titleLabel)
grid = QtWidgets.QGridLayout()
grid.setMargin(0)
grid.setSpacing(0)
widg = QtWidgets.QFrame()
widg.setStyleSheet(
"QFrame{"
" border: 2px solid"
+ self.NativeUI.colors["page_foreground"].name()
+ ";"
"}"
"QLabel{"
" border:none;"
"} "
)
j = -1
for boxInfo in controlDict[key]:
j = j + 1
grid.addWidget(
self.NativeUI.widgets.get_widget("expert_spin_" + boxInfo[2]),
i + 1 + int(j / 3),
2 * (j % 3),
1,
2,
)
widg.setLayout(grid)
vlayout.addWidget(widg)
i = i + 1 + int(j / 3) + 1
expert_tab = QtWidgets.QWidget()
expert_tab.setLayout(vlayout)
hButtonLayout = QtWidgets.QHBoxLayout()
hButtonLayout.addWidget(self.NativeUI.widgets.get_widget("ok_button_expert"))
hButtonLayout.addWidget(
self.NativeUI.widgets.get_widget("ok_send_button_expert")
)
hButtonLayout.addWidget(
self.NativeUI.widgets.get_widget("cancel_button_expert")
)
vlayout.addLayout(hButtonLayout)
#passlock_stack = #QtWidgets.QStackedWidget()
self.NativeUI.widgets.expert_passlock_stack.addWidget(self.NativeUI.widgets.expert_password_widget)
self.NativeUI.widgets.expert_passlock_stack.addWidget(expert_tab)
#break this here
return self.NativeUI.widgets.expert_passlock_stack#expert_tab
def layout_main_spin_buttons(self) -> QtWidgets.QWidget:
hlayout = QtWidgets.QHBoxLayout()
with open("NativeUI/configs/mode_config.json") as json_file:
modeDict = json.load(json_file)
stack = self.NativeUI.widgets.main_mode_stack
for setting in modeDict["settings"]:
if setting[0] in modeDict["mainPageSettings"]:
attrName = "CURRENT_" + setting[2]
widg = self.NativeUI.widgets.get_widget(attrName)
if setting[0] in modeDict["radioSettings"]:
stack.addWidget(widg)
else:
hlayout.addWidget(widg)
hlayout.addWidget(self.NativeUI.widgets.main_mode_stack)
vbuttonLayout = QtWidgets.QVBoxLayout()
okButton = self.NativeUI.widgets.get_widget("CURRENT_ok_button")
okButton.setSizePolicy(QtGui.QSizePolicy.Expanding, QtGui.QSizePolicy.Fixed)
vbuttonLayout.addWidget(okButton)
cancelButton = self.NativeUI.widgets.get_widget("CURRENT_cancel_button")
cancelButton.setSizePolicy(QtGui.QSizePolicy.Expanding, QtGui.QSizePolicy.Fixed)
vbuttonLayout.addWidget(cancelButton)
hlayout.addLayout(vbuttonLayout)
combined_spin_buttons = QtWidgets.QWidget()
combined_spin_buttons.setLayout(hlayout)
x = self.screen_width - self.left_bar_width - self.main_page_bottom_bar_height
y = self.main_page_bottom_bar_height
spacing = self.widget_spacing
combined_spin_buttons.setFixedSize(x, y)
x_spin = int(x / hlayout.count() - spacing)
y_spin = y - spacing
for setting in modeDict["settings"]:
if setting[0] in modeDict["mainPageSettings"]:
attrName = "CURRENT_" + setting[2]
self.NativeUI.widgets.get_widget(attrName).setFixedSize(x_spin, y_spin)
self.NativeUI.widgets.get_widget(attrName).simpleSpin.setFixedSize(
x_spin, 0.7 * y_spin
)
self.NativeUI.widgets.get_widget(attrName).simpleSpin.setFont(
self.NativeUI.text_font
)
self.NativeUI.widgets.get_widget(attrName).label.setFont(
self.NativeUI.text_font
)
stack.setFixedSize(x_spin, y_spin)
cancelButton.setFixedSize(x_spin, int(y_spin / 2) - spacing)
okButton.setFixedSize(x_spin, int(y_spin / 2) - spacing)
# spin_buttons.set_label_font(self.NativeUI.text_font)
# spin_buttons.set_value_font(self.NativeUI.value_font
return combined_spin_buttons
def layout_tab_clinical_limits(self):
with open("NativeUI/configs/clinical_config.json") as json_file:
clinicalDict = json.load(json_file)
vlayout = QtWidgets.QVBoxLayout()
for setting in clinicalDict["settings"]:
attrName = "clinical_spin_" + setting[0][2]
hlayout = QtWidgets.QHBoxLayout()
if len(setting) >= 2:
hlayout.addWidget(self.NativeUI.widgets.get_widget(attrName + "_min"))
if len(setting) == 3:
hlayout.addWidget(
self.NativeUI.widgets.get_widget(attrName + "_set")
)
hlayout.addWidget(self.NativeUI.widgets.get_widget(attrName + "_max"))
elif len(setting) == 1:
hlayout.addWidget(self.NativeUI.widgets.get_widget(attrName + "_lim"))
vlayout.addLayout(hlayout)
hbuttonlayout = QtWidgets.QHBoxLayout()
hbuttonlayout.addWidget(self.NativeUI.widgets.get_widget("clinical_ok_button"))
hbuttonlayout.addWidget(
self.NativeUI.widgets.get_widget("clinical_cancel_button")
)
vlayout.addLayout(hbuttonlayout)
clinical_page = QtWidgets.QWidget()
clinical_page.setLayout(vlayout)
return clinical_page
def layout_startup_confirmation(self):
vlayout = QtWidgets.QVBoxLayout()
i = 0
for key, spinBox in self.NativeUI.widgets.get_widget(
"startup_handler"
).spinDict.items():
i = i + 1
hlayout = QtWidgets.QHBoxLayout()
nameLabel = QtWidgets.QLabel(key)
valLabel = QtWidgets.QLabel(str(spinBox.get_value()))
hlayout.addWidget(nameLabel)
hlayout.addWidget(valLabel)
vlayout.addLayout(hlayout)
if i == 10:
break
widg = QtWidgets.QWidget()
widg.setLayout(vlayout)
return widg
"""
ui_widgets.py
Creates all of the widgets used in NativeUI and stores references to them as attributes
of a single object for ease of reference
"""
__author__ = "Benjamin Mummery"
__credits__ = ["Benjamin Mummery", "Dónal Murray", "Tim Powell", "Tiago Sarmento"]
__license__ = "GPL"
__version__ = "0.0.1"
__maintainer__ = "Benjamin Mummery"
__email__ = "benjamin.mummery@stfc.ac.uk"
__status__ = "Prototype"
from PySide2.QtWidgets import (
QWidget,
QPushButton,
QRadioButton,
QButtonGroup,
QLabel,
QStackedWidget,
)
from global_widgets.tab_modeswitch_button import TabModeswitchButton
from global_widgets.global_spinbox import labelledSpin
from global_widgets.global_send_popup import SetConfirmPopup
from widget_library.localisation_button_widget import LocalisationButtonWidget
from widget_library.startup_handler import StartupHandler
from widget_library.startup_calibration_widget import calibrationWidget
from widget_library.ok_cancel_buttons_widget import (
OkButtonWidget,
CancelButtonWidget,
OkSendButtonWidget,
)
from widget_library.history_buttons_widget import HistoryButtonsWidget
from widget_library.measurements_widget import (
NormalMeasurementsBlockWidget,
ExpertMeasurementsBloackWidget,
)
from widget_library.battery_display_widget import BatteryDisplayWidget
from widget_library.info_display_widgets import (
VersionDisplayWidget,
MaintenanceTimeDisplayWidget,
UpdateTimeDisplayWidget,
)
# from widget_library.tab_charts import TabChart
from widget_library.chart_buttons_widget import ChartButtonsWidget
from widget_library.page_buttons_widget import PageButtonsWidget, PageButton
from widget_library.personal_display_widget import PersonalDisplayWidget
from widget_library.plot_widget import (
ChartsPlotWidget,
CirclePlotsWidget,
TimePlotsWidget,
)
from widget_library.spin_buttons_widget import SpinButton
# from widget_library.tab_expert import TabExpert
from widget_library.ventilator_start_stop_buttons_widget import (
VentilatorStartStopButtonsWidget,
)
from widget_library.line_edit_widget import LabelledLineEditWidget
from global_widgets.global_typeval_popup import TypeValuePopup, AbstractTypeValPopup
# from widget_library.NativeUI.expert_handler import ExpertHandler
# from mode_widgets.NativeUI.personal_handler import PersonalHandler
# from widget_library.tab_expert import TabExpert
# from widget_library.tab_charts import TabChart
# from mode_widgets.tab_modes import TabModes
# from mode_widgets.tab_personal import TabPersonal
# from mode_widgets.NativeUI.mode_handler import ModeHandler
from alarm_widgets.alarm_handler import AlarmHandler
from alarm_widgets.alarm_list import AlarmList
from alarm_widgets.alarm_popup import AlarmPopup
from alarm_widgets.alarm_table import AlarmTable
# from alarm_widgets.tab_alarm_table import TabAlarmTable
# from alarm_widgets.tab_clinical import TabClinical
import json
import os
class Widgets:
def __init__(self, NativeUI, *args, **kwargs):
"""
Creates and stores references to all of the widgets we use.
Widgets are grouped by pages for convenience, however this class deliberately
contains no layout logic.
"""
# NativeUI = NativeUI
# Start up procedure
self.startup_confirm_popup = SetConfirmPopup(NativeUI)
self.startup_handler = StartupHandler(NativeUI, self.startup_confirm_popup)
with open("NativeUI/configs/startup_config.json") as json_file:
startupDict = json.load(json_file)
for key, procedureDict in startupDict.items():
self.add_handled_widget(
calibrationWidget(NativeUI, key, procedureDict),
key,
self.startup_handler,
)
for mode in NativeUI.modeList:
self.add_handled_widget(
QRadioButton(mode), "startup_radio_" + mode, self.startup_handler
)
self.add_handled_widget(
OkButtonWidget(NativeUI), "nextButton", self.startup_handler
)
self.nextButton.setColour(0)
self.add_handled_widget(
OkSendButtonWidget(NativeUI), "skipButton", self.startup_handler
)
self.skipButton.setColour(0)
self.add_handled_widget(
CancelButtonWidget(NativeUI), "backButton", self.startup_handler
)
# Top bar widgets
self.tab_modeswitch = TabModeswitchButton(NativeUI)
self.battery_display = BatteryDisplayWidget(NativeUI)
self.personal_display = PersonalDisplayWidget(NativeUI)
self.localisation_button = LocalisationButtonWidget(
NativeUI.localisation_files, NativeUI.colors
)
# Left Bar widgets
self.page_buttons = PageButtonsWidget(NativeUI)
self.ventilator_start_stop_buttons_widget = VentilatorStartStopButtonsWidget(
NativeUI
)
self.lock_button = PageButton(
NativeUI,
"",
signal_value="lock_screen",
icon=NativeUI.icons["lock_screen"],
)
# Main Page Widgets
self.history_buttons = HistoryButtonsWidget(NativeUI)
self.normal_plots = TimePlotsWidget(NativeUI)
self.detailed_plots = TimePlotsWidget(NativeUI)
self.normal_measurements = NormalMeasurementsBlockWidget(NativeUI)
self.circle_plots = CirclePlotsWidget(NativeUI)
self.detailed_measurements = ExpertMeasurementsBloackWidget(NativeUI)
# Alarm Page Widgets
self.alarm_handler = AlarmHandler(NativeUI)
self.alarm_popup = AlarmPopup(NativeUI)
self.alarm_list = AlarmList(NativeUI)
self.acknowledge_button = QPushButton()
self.alarm_table = AlarmTable(NativeUI)
self.clinical_tab = QWidget() # TabClinical(NativeUI)
### Alarm limits
with open("NativeUI/configs/clinical_config.json") as json_file:
clinicalDict = json.load(json_file)
for setting in clinicalDict["settings"]:
attrName = "clinical_spin_" + setting[0][2]
if len(setting) == 1:
self.add_handled_widget(
labelledSpin(NativeUI, NativeUI.typeValPopupNum,setting[0]),
attrName + "_lim",
NativeUI.clinical_handler,
)
if len(setting) >= 2:
self.add_handled_widget(
labelledSpin(NativeUI, NativeUI.typeValPopupNum,setting[0]),
attrName + "_min",
NativeUI.clinical_handler,
)
self.add_handled_widget(
labelledSpin(NativeUI, NativeUI.typeValPopupNum,setting[-1]),
attrName + "_max",
NativeUI.clinical_handler,
)
if len(setting) == 3:
self.add_handled_widget(
labelledSpin(NativeUI, NativeUI.typeValPopupNum,setting[1]),
attrName + "_set",
NativeUI.clinical_handler,
)
self.add_handled_widget(
OkButtonWidget(NativeUI), "clinical_ok_button", NativeUI.clinical_handler
)
self.add_handled_widget(
CancelButtonWidget(NativeUI),
"clinical_cancel_button",
NativeUI.clinical_handler,
)
#### Mode settings tab: Mode (x4), Personal
# Modes Page Widgets
with open("NativeUI/configs/mode_config.json") as json_file:
modeDict = json.load(json_file)
radioSettings = modeDict["radioSettings"]
modes = NativeUI.modeList
self.add_handled_widget(
QStackedWidget(), "main_mode_stack", NativeUI.mode_handler
)
for setting in modeDict["settings"]:
if setting[0] in modeDict["mainPageSettings"]:
attrName = "CURRENT_" + setting[2]
self.add_handled_widget(
SpinButton(NativeUI, NativeUI.typeValPopupNum,setting), attrName, NativeUI.mode_handler
)
self.add_handled_widget(
OkButtonWidget(NativeUI), "CURRENT_ok_button", NativeUI.mode_handler
)
self.add_handled_widget(
CancelButtonWidget(NativeUI), "CURRENT_cancel_button", NativeUI.mode_handler
)
self.groupDict = {}
for mode in modes:
for startup in ["", "_startup"]:
self.groupDict[mode + startup] = QButtonGroup()
for setting in modeDict["settings"]:
attrName = mode + startup + "_" + setting[2]
targettedSetting = [
target.replace(
"SET_TARGET_",
"SET_TARGET_" + mode.replace("/", "_").replace("-", "_"),
)
if isinstance(target, str)
else target
for target in setting
]
if startup == "_startup":
self.add_handled_widget(
labelledSpin(NativeUI, NativeUI.typeValPopupNum,targettedSetting),
"spin_" + attrName,
self.startup_handler,
)
else:
self.add_handled_widget(
labelledSpin(NativeUI, NativeUI.typeValPopupNum,targettedSetting),
"spin_" + attrName,
NativeUI.mode_handler,
)
if setting[0] in radioSettings:
radioButton = QRadioButton()
self.groupDict[mode + startup].addButton(radioButton)
if startup == "_startup":
self.add_handled_widget(
radioButton, "radio_" + attrName, self.startup_handler
)
else:
self.add_handled_widget(
radioButton, "radio_" + attrName, NativeUI.mode_handler
)
if startup != "_startup":
self.add_handled_widget(
OkButtonWidget(NativeUI),
"ok_button_" + mode,
NativeUI.mode_handler,
)
self.add_handled_widget(
OkSendButtonWidget(NativeUI),
"ok_send_button_" + mode,
NativeUI.mode_handler,
)
self.add_handled_widget(
CancelButtonWidget(NativeUI),
"cancel_button_" + mode,
NativeUI.mode_handler,
)
# Personal tab widgets
# self.personal_confirm_popup = SetConfirmPopup(NativeUI)
# NativeUI.personal_handler = PersonalHandler(NativeUI, self.personal_confirm_popup)
with open("NativeUI/configs/personal_config.json") as json_file:
personalDict = json.load(json_file)
textBoxes = personalDict["textBoxes"]
for startup in ["", "startup_"]:
for setting in personalDict["settings"]:
attrName = "personal_edit_" + setting[2]
if setting[0] in textBoxes:
if startup == "startup_":
self.add_handled_widget(
LabelledLineEditWidget(NativeUI, NativeUI.typeValPopupAlpha, setting),
"text_" + startup + attrName,
self.startup_handler,
)
else:
self.add_handled_widget(
LabelledLineEditWidget(NativeUI, NativeUI.typeValPopupAlpha, setting),
"text_" + startup + attrName,
NativeUI.personal_handler,
)
else:
if startup == "startup_":
self.add_handled_widget(
labelledSpin(NativeUI, NativeUI.typeValPopupNum,setting),
"spin_" + startup + attrName,
self.startup_handler,
)
else:
self.add_handled_widget(
labelledSpin(NativeUI, NativeUI.typeValPopupNum,setting),
"spin_" + startup + attrName,
NativeUI.personal_handler,
)
self.add_handled_widget(
OkButtonWidget(NativeUI), "ok_button_personal", NativeUI.personal_handler
)
self.add_handled_widget(
OkSendButtonWidget(NativeUI),
"ok_send_button_personal",
NativeUI.personal_handler,
)
self.add_handled_widget(
CancelButtonWidget(NativeUI),
"cancel_button_personal",
NativeUI.personal_handler,
)
##### Settings Tab: Expert and Charts tabs
self.add_widget(QStackedWidget(),'expert_passlock_stack')
self.add_handled_widget(AbstractTypeValPopup(NativeUI,'alpha'), 'expert_password_widget', NativeUI.expert_handler)
# Expert Tab
with open("NativeUI/configs/expert_config.json") as json_file:
controlDict = json.load(json_file)
for key in controlDict:
self.add_widget(QLabel(key), "expert_label_" + key)
for setting in controlDict[key]:
attrName = "expert_spin_" + setting[2]
self.add_handled_widget(
labelledSpin(NativeUI, NativeUI.typeValPopupNum,setting), attrName, NativeUI.expert_handler
)
self.add_handled_widget(
OkButtonWidget(NativeUI), "ok_button_expert", NativeUI.expert_handler
)
self.add_handled_widget(
OkSendButtonWidget(NativeUI),
"ok_send_button_expert",
NativeUI.expert_handler,
)
self.add_handled_widget(
CancelButtonWidget(NativeUI),
"cancel_button_expert",
NativeUI.expert_handler,
)
# Chart Tab
self.charts_widget = ChartsPlotWidget(colors=NativeUI.colors)
self.chart_buttons_widget = ChartButtonsWidget(colors=NativeUI.colors)
# Info Tab
self.version_display_widget = VersionDisplayWidget(NativeUI.colors)
self.maintenance_time_display_widget = MaintenanceTimeDisplayWidget(
NativeUI.colors
)
self.update_time_display_widget = UpdateTimeDisplayWidget(NativeUI.colors)
def add_widget(self, widget, name) -> int:
setattr(self, name, widget)
return 0
def add_handled_widget(self, widget, name, handler) -> int:
"""Add a widget to Widgets and pass it into a handler"""
setattr(self, name, widget)
handler.add_widget(widget, name)
return 0
def get_widget(self, name) -> QWidget:
return getattr(self, name)
#!/usr/bin/env python3
"""
battery_display_widget.py
"""
__author__ = ["Benjamin Mummery", "Tiago Sarmento"]
__credits__ = ["Benjamin Mummery", "Dónal Murray", "Tim Powell", "Tiago Sarmento"]
__license__ = "GPL"
__version__ = "0.0.1"
__maintainer__ = "Benjamin Mummery"
__email__ = "benjamin.mummery@stfc.ac.uk"
__status__ = "Development"
import logging
import os
from PySide2 import QtCore, QtGui, QtWidgets
class BatteryDisplayWidget(QtWidgets.QWidget):
"""
Widget that contains both the battery icon and a text readout of the current
battery charge.
"""
def __init__(self, NativeUI, *args, **kwargs):
super().__init__(*args, **kwargs)
self.NativeUI = NativeUI
layout = QtWidgets.QHBoxLayout(self)
self.icon_display = BatteryIcon(NativeUI)
self.text_display = BatteryText(NativeUI)
self.widgets = [self.icon_display, self.text_display]
for widget in self.widgets:
layout.addWidget(widget, alignment=QtCore.Qt.AlignRight)
layout.setSpacing(0)
layout.setContentsMargins(0, 0, 0, 0)
self.setLayout(layout)
self.status = {}
self.set_default_status()
self.update_status(self.status)
def set_default_status(self) -> dict:
"""
Set the default battery status, to be assumed until told otherwise.
For safety, the default assumption is that the ventilator is on battery power
with 0 % battery life remaining and an electrical problem.
"""
self.status = {
"on_mains_power": False,
"on_battery_power": True,
"battery_percent": 0,
"electrical_problem": "No Battery Info",
}
return self.status
@QtCore.Slot(dict)
def update_status(self, input_status: dict):
"""
TODO: docstring
"""
self.status = input_status
# Update widgets with new status
for widget in self.widgets:
widget.update_value(self.status)
return 0
def set_size(self, x: int, y: int, spacing=None) -> int:
"""
Set the size of the battery display widget. Due to the way that the text_display
needs to resize itself, both the x and y sizes must be specified.
"""
assert isinstance(x, int)
assert isinstance(y, int)
self.setFixedSize(x, y)
self.icon_display.set_size(y, y)
self.text_display.set_size(x - y, y)
return 0
def setFont(self, font: QtGui.QFont) -> int:
"""
Overrides the existing setFont method in order to propogate the change to
subwidgets.
"""
self.text_display.setFont(font)
return 0
class BatteryText(QtWidgets.QLabel):
""""""
def __init__(self, NativeUI, *args, **kwargs):
super().__init__("", *args, **kwargs)
self.__size = (0, 0)
self.setStyleSheet(
"background-color:" + NativeUI.colors["page_background"].name() + ";"
"border: none;"
"color:" + NativeUI.colors["page_foreground"].name() + ";"
)
def update_value(self, status):
""""""
self.__apply_default_size()
if status["electrical_problem"] is not None:
self.setText(status["electrical_problem"])
return 0
if status["on_mains_power"]:
self.setText("")
self.__apply_temp_size(0, self.__size[1])
return 0
self.setText(str(status["battery_percent"]) + " %")
return 0
def set_size(self, x: int, y: int) -> int:
"""
Set the default size of the widget.
As the widget needs to resize when displaying an empty string, we store its
default size in the __size attribute so that this can be reapplied later.
"""
self.__size = (x, y)
self.__apply_default_size()
return 0
def __apply_default_size(self) -> int:
"""
Set the size of the widget to the default size as defined in the __size
attribute.
"""
self.setFixedSize(*self.__size)
return 0
def __apply_temp_size(self, x, y) -> int:
"""
Temporarily set the size of the widget to the specified dimensions. This change
can be undone by calling the __apply_default_size method.
"""
self.setFixedSize(x, y)
return 0
class BatteryIcon(QtWidgets.QPushButton):
"""
Widget to display the current battery icon
"""
def __init__(self, NativeUI, *args, **kwargs):
super().__init__("", *args, **kwargs)
self.NativeUI = NativeUI
self.__icon_list = self.__make_icon_list()
self.__mains_icon = os.path.join(self.NativeUI.iconpath, "plug-solid.png")
self.__alert_icon = os.path.join(
self.NativeUI.iconpath, "exclamation-triangle-solid.png"
)
self.__icon_percentiles = self.__make_percentile_ranges()
self.setEnabled(False)
self.setStyleSheet(
"background-color:" + NativeUI.colors["page_background"].name() + ";"
"border: none"
)
def update_value(self, status):
"""
Update the icon to match that of the specified battery percentage value.
"""
if status["electrical_problem"] is not None:
self.setIcon(QtGui.QIcon(self.__alert_icon))
return 0
if status["on_mains_power"]:
self.setIcon(QtGui.QIcon(self.__mains_icon))
return 0
self.setIcon(
QtGui.QIcon(
self.__icon_list[self.__get_range_index(status["battery_percent"])]
)
)
return 0
def __get_range_index(self, battery_percent):
"""
Determine which of the percentile ranges the current battery value falls into.
"""
i = 0
while i < len(self.__icon_percentiles):
if (battery_percent >= self.__icon_percentiles[i][0]) and (
battery_percent < self.__icon_percentiles[i][1]
):
return i
i += 1
raise Exception("battery value out of range?")
def __make_percentile_ranges(self, zero_point: float = 15):
"""
zero_point: percentage of battery life at which the icon shows empty
"""
if self.__icon_list is None:
self.__icon_list = self.__make_icon_list()
n_icons = len(self.__icon_list)
percent_per_icon = 100.0 / n_icons
if percent_per_icon < zero_point:
zero_point = percent_per_icon
lower_bounds = [0.0]
upper_bounds = [zero_point]
while upper_bounds[-1] + percent_per_icon < 100:
lower_bounds.append(upper_bounds[-1])
upper_bounds.append(upper_bounds[-1] + percent_per_icon)
lower_bounds.append(upper_bounds[-1])
upper_bounds.append(100.0)
return [(a, b) for a, b in zip(lower_bounds, upper_bounds)]
def __make_icon_list(self):
"""
Make a list of paths to battery icons, in order of the battery charge
they represent
"""
icon_list = [
"battery-empty-solid.png",
"battery-quarter-solid.png",
"battery-half-solid.png",
"battery-three-quarters-solid.png",
]
icon_list = [os.path.join(self.NativeUI.iconpath, icon) for icon in icon_list]
return icon_list
def set_size(self, x: int, y: int) -> int:
self.setIconSize(QtCore.QSize(x, y))
self.setFixedSize(x, y)
return 0
#!/usr/bin/env python3
"""
chart_buttons_widget.py
"""
__author__ = ["Benjamin Mummery", "Tiago Sarmento"]
__credits__ = ["Benjamin Mummery", "Dónal Murray", "Tim Powell", "Tiago Sarmento"]
__license__ = "GPL"
__version__ = "0.0.1"
__maintainer__ = "Benjamin Mummery"
__email__ = "benjamin.mummery@stfc.ac.uk"
__status__ = "Development"
from PySide2 import QtGui, QtWidgets
from PySide2.QtCore import QSize, Signal, Slot
class ChartButtonsWidget(QtWidgets.QWidget):
def __init__(self, *args, colors: dict = {}, **kwargs):
super().__init__(*args, **kwargs)
self.buttons = [
ToggleButtonWidget("Pressure", signal_value="pressure"),
ToggleButtonWidget("Flow", signal_value="flow"),
]
stylesheet = (
"QPushButton{"
" background-color:" + colors["button_background_enabled"].name() + ";"
" color:" + colors["button_foreground_enabled"].name() + ";"
" border: none"
"}"
"QPushButton:checked{"
" background-color:" + colors["button_background_disabled"].name() + ";"
" color:" + colors["button_foreground_enabled"].name() + ";"
" border: none"
"}"
)
for button in self.buttons:
button.setStyleSheet(stylesheet)
# Layout buttons block
grid = QtWidgets.QGridLayout()
i_row = 0
i_col = 0
for widget in self.buttons:
grid.addWidget(widget, i_row, i_col)
i_row += 1
self.setLayout(grid)
def set_size(self, x: int, y: int, spacing: int = 10) -> int:
"""
Set the size of the widget and its subwidgets.
"""
pass
def setFont(self, font: QtGui.QFont) -> int:
"""
Overrides the existing setFont method in order to propogate the change to
subwidgets.
"""
for button in self.buttons:
button.setFont(font)
return 0
class ToggleButtonWidget(QtWidgets.QPushButton):
"""
Variant of the QPushButton that emits a signal containing a string (signal_value)
"""
ToggleButtonPressed = Signal(str)
ToggleButtonReleased = Signal(str)
def __init__(self, *args, signal_value: str = None, **kwargs):
super().__init__(*args, **kwargs)
self.__signal_value = signal_value
self.setCheckable(True)
self.pressed.connect(self.on_press)
def on_press(self) -> int:
"""
When the button is pressed, emit the either the ToggleButtonPressed or
ToggleButtonReleased signal depending on whether the button was in the checked
or unchacked state.
"""
if self.isChecked(): # active -> inactive
self.ToggleButtonReleased.emit(self.__signal_value)
else: # inactive -> active
self.ToggleButtonPressed.emit(self.__signal_value)
return 0
from global_widgets.global_spinbox import labelledSpin
from widget_library.ok_cancel_buttons_widget import OkButtonWidget, CancelButtonWidget, OkSendButtonWidget
from global_widgets.global_typeval_popup import AbstractTypeValPopup
from PySide2 import QtWidgets, QtGui, QtCore
from handler_library.handler import PayloadHandler
import logging
import json
class ExpertHandler(PayloadHandler): # chose QWidget over QDialog family because easier to modify
UpdateExpert = QtCore.Signal(dict)
OpenPopup = QtCore.Signal(PayloadHandler,list)
def __init__(self, NativeUI, *args, **kwargs):
super().__init__(['READBACK'],*args, **kwargs)
self.NativeUI = NativeUI
self.spinDict = {}
self.buttonDict = {}
self.manuallyUpdated = False
self.commandList = []
with open("NativeUI/configs/expert_config.json") as json_file:
controlDict = json.load(json_file)
self.relevantKeys = [list[2] for key in controlDict for list in controlDict[key]]
def add_widget(self, widget, key: str):
if isinstance(widget, labelledSpin):
self.spinDict[key] = widget
if isinstance(widget, OkButtonWidget) or isinstance(widget, CancelButtonWidget) or isinstance(widget,OkSendButtonWidget):
self.buttonDict[key] = widget
if isinstance(widget, AbstractTypeValPopup):
self.password_lock = widget
def active_payload(self, *args) -> int:
readback_data = self.get_db()
outdict = {}
for key in self.relevantKeys:
try:
outdict[key] = readback_data[key]
except KeyError:
logging.debug("Invalid key %s in measurement database", key)
self.UpdateExpert.emit(outdict)
return 0
def handle_okbutton_click(self, key):
message, command = [], []
for widget in self.spinDict:
if self.spinDict[widget].manuallyUpdated:
setVal = self.spinDict[widget].get_value()
setVal = round(setVal, self.spindict[widget].decPlaces)
message.append("set" + widget + " to " + str(setVal))
command.append(
[
self.spinDict[widget].cmd_type,
self.spinDict[widget].cmd_code,
setVal,
]
)
self.commandList = command
if 'send' in key:
self.sendCommands()
else:
self.OpenPopup.emit(self,message)
def sendCommands(self):
if self.commandList == []:
a=1
else:
for command in self.commandList:
self.NativeUI.q_send_cmd(*command)
self.commandSent()
return 0
def commandSent(self):
self.commandList = []
for widget in self.spinDict:
self.spinDict[widget].manuallyUpdated = False
self.refresh_button_colour()
def handle_manual_change(self, changed_spin_key):
self.refresh_button_colour()
def refresh_button_colour(self):
self.manuallyUpdated = False
for spin in self.spinDict:
self.manuallyUpdated = self.manuallyUpdated or self.spinDict[spin].manuallyUpdated
for button in self.buttonDict:
self.buttonDict[button].setColour(str(int(self.manuallyUpdated)))
#!/usr/bin/env python3
"""
history_buttons_widget.py
Part of NativeUI. Defines the HistoryButton class to control the lookback time
of plots, and constructs the HistoryButtonsWidget to contain the requisite
historybuttons.
"""
__author__ = ["Benjamin Mummery", "Tiago Sarmento"]
__credits__ = ["Benjamin Mummery", "Dónal Murray", "Tim Powell", "Tiago Sarmento"]
__license__ = "GPL"
__version__ = "0.0.1"
__maintainer__ = "Benjamin Mummery"
__email__ = "benjamin.mummery@stfc.ac.uk"
__status__ = "Development"
from PySide2 import QtGui, QtWidgets
from PySide2.QtCore import QSize, Signal, Slot
class HistoryButtonsWidget(QtWidgets.QWidget):
"""
Widget to hold the HistoryButtons.
"""
def __init__(self, NativeUI, *args, **kwargs):
super().__init__(*args, **kwargs)
self.NativeUI = NativeUI
self.buttons = [
HistoryButton("60s", signal_value=61),
HistoryButton("30s", signal_value=31),
HistoryButton("15s", signal_value=15),
HistoryButton("5s", signal_value=5),
]
self.__grid_columns = 2
# Button Appearance
stylesheet = (
"QPushButton{"
" background-color:"
+ NativeUI.colors["button_background_enabled"].name()
+ ";"
" color: " + NativeUI.colors["button_foreground_enabled"].name() + ";"
" border: none;"
"}"
"QPushButton:disabled{"
" background-color:"
+ NativeUI.colors["button_background_disabled"].name()
+ ";"
" color: " + NativeUI.colors["button_foreground_enabled"].name() + ";"
" border: none;"
"}"
)
for button in self.buttons:
button.setStyleSheet(stylesheet)
# Button Layout
grid = QtWidgets.QGridLayout()
i_col = 0
self.__grid_rows = 0
for widget in self.buttons:
grid.addWidget(widget, self.__grid_rows, i_col)
i_col += 1
if i_col == self.__grid_columns:
i_col = 0
self.__grid_rows += 1
self.setLayout(grid)
# Connect the buttons so that pressing one enables all of the others
for pressed_button in self.buttons:
for unpressed_button in self.buttons:
if pressed_button == unpressed_button:
continue
pressed_button.pressed.connect(unpressed_button.enable)
self.buttons[0].on_press()
def set_size(self, x: int, y: int, spacing: int = 10) -> int:
"""
Set the size of the widget and its subwidgets.
Sizes are computed on the assumption that all buttons should be square.
If both x and y are set, HistoryButtonsWidget will have size x by y, and buttons
will be size MIN(x/n_cols, y/n_rows)-spacing where n_cols and n_rows are number
of columns and rows in the button grid respectively.
If x alone is set, the buttons will have size x/n_cols-spacing, and
HistoryButtonsWidget will have size x by n_rows*(x/n_cols) (i.e. the height to
fit all of the buttons).
If y alone is set, the buttons will have size y/n_rows-spacing, and
HistoryButtonsWidget will have size n_cols*(y/n_rows) by y (i.e. the width
expands to fit all of the buttons).
"""
x_set, y_set = False, False
if x is not None:
x_set = True
if y is not None:
y_set = True
if x_set and y_set:
self.setFixedSize(x, y)
button_size_temp_x = int(x / self.__grid_columns)
button_size_temp_y = int(y / self.__grid_rows)
button_size = int(min(x / self.__grid_columns, y / self.__grid_rows))
elif x_set and not y_set:
button_size = int(x / self.__grid_columns)
self.setFixedSize(x, self.__grid_rows * button_size)
elif y_set and not x_set:
button_size = int(y / self.__grid_rows)
self.setFixedSize(self.__grid_columns * button_size, y)
else:
raise ValueError("set_size called with no size information")
for button in self.buttons:
button.setFixedSize(button_size - spacing, button_size - spacing)
return 0
def setFont(self, font: QtGui.QFont) -> int:
"""
Overrides the existing setFont method in order to propogate the change to
subwidgets.
"""
for button in self.buttons:
button.setFont(font)
return 0
class HistoryButton(QtWidgets.QPushButton):
"""
Identical to QPushButton but accepts an aditional optional argument signal_value
that is emitted as part of the signal 'HistoryButtonPressed'. When pressed, all
linked buttons as defined by the buttons list of the parent widget are enabled,
while the pushed button is disabled.
"""
HistoryButtonPressed = Signal(int)
def __init__(self, *args, signal_value: int = None, **kwargs):
super().__init__(*args, **kwargs)
self.__signal_value = signal_value
self.pressed.connect(self.on_press)
@Slot()
def enable(self):
self.setEnabled(True)
return 0
def on_press(self):
"""
When the button is pressed, disable it and emit the HistoryButtonPressed signal.
"""
self.setEnabled(False)
self.HistoryButtonPressed.emit(self.__signal_value)
return 0
#!/usr/bin/env python3
"""
info_display_widgets.py
Simple widgets to display various system information parameters including version
number(s), time since last maintenance, and time since last update.
"""
__author__ = ["Benjamin Mummery", "Tiago Sarmento"]
__credits__ = ["Benjamin Mummery", "Dónal Murray", "Tim Powell", "Tiago Sarmento"]
__license__ = "GPL"
__version__ = "0.1.0"
__maintainer__ = "Benjamin Mummery"
__email__ = "benjamin.mummery@stfc.ac.uk"
__status__ = "Development"
import logging
from PySide2 import QtCore, QtWidgets
class VersionDisplayWidget(QtWidgets.QLabel):
"""
Widget that displays the current version number.
We don't need to override setFont as long as we're just subclassing the QLabel
widget. If we later expand this class to include more complexity, we'll have to add
setFont as a method to propogate the font to any subwidgets.
"""
def __init__(self, colors, *args, **kwargs):
super().__init__(*args, **kwargs)
self.__mcu_version: str = "???"
self.__mcu_hash: str = None
self.__ui_version: str = "???"
self.__ui_hash: str = None
self.setStyleSheet(
"background-color:" + colors["page_background"].name() + ";"
"border: none;"
"color:" + colors["page_foreground"].name() + ";"
)
self.__refresh_display()
def __refresh_display(self) -> int:
"""
Update the display to show the current values of __ui_version and __mcu_version.
"""
display_text = ""
display_text += "MCU Software Version: %s" % self.__mcu_version
if self.__mcu_hash is not None:
display_text += " (%s)" % self.__mcu_hash
display_text += "<br>UI Software Version: %s" % self.__ui_version
if self.__ui_hash is not None:
display_text += " (%s)" % self.__ui_hash
self.setText(display_text)
return 0
@QtCore.Slot(str)
def update_UI_version(self, version: str) -> int:
"""
Update the value shown for the UI version
"""
self.__ui_version = version
self.__refresh_display()
return 0
@QtCore.Slot(str)
def update_UI_hash(self, hash: str) -> int:
"""
Update the value shown for the UI hash
"""
self.__ui_hash = hash
self.__refresh_display()
return 0
@QtCore.Slot(str)
def update_mcu_version(self, version: str) -> int:
"""
Update the value shown for the MCU version
"""
self.__mcu_version = version
self.__refresh_display()
return 0
@QtCore.Slot(str)
def update_mcu_version(self, hash: str) -> int:
"""
Update the value shown for the MCU hash
"""
self.__mcu_hash = hash
self.__refresh_display()
return 0
def set_size(self, x: int, y: int) -> int:
"""
Set the size of the widget.
"""
self.setFixedSize(x, y)
return 0
class MaintenanceTimeDisplayWidget(QtWidgets.QLabel):
"""
Widget that displays the time since the last maintenance.
"""
def __init__(self, colors, *args, **kwargs):
super().__init__(*args, **kwargs)
self.__time_since_maintenance: str = "???"
self.__time_to_maintenance: str = "???"
self.__maintenance_needed: bool = True
self.__normal_color = colors["page_foreground"].name()
self.__alert_color = colors["red"].name()
self.setStyleSheet(
"background-color:" + colors["page_background"].name() + ";"
"border: none;"
"color:" + self.__normal_color + ";"
)
self.__refresh_display()
def __refresh_display(self) -> int:
"""
Update the display to show the current values of time to, and since,
maintenance.
"""
self.setText(
"%s since last maintenance. Maintenance due in %s."
% (self.__time_since_maintenance, self.__time_to_maintenance)
)
if self.__maintenance_needed:
self.setStyleSheet("color:%s" % self.__alert_color)
else:
self.setStyleSheet("color:%s" % self.__normal_color)
return 0
@QtCore.Slot(str, str, bool)
def set_time_values(
self,
time_since_maintenance: str,
time_to_maintenance: str,
maintenance_needed: bool,
) -> int:
"""
set the time since and to maintenance, and whether maintenance is needed.
"""
self.__time_since_maintenance = time_since_maintenance
self.__time_to_maintenance = time_to_maintenance
self.__maintenance_needed = maintenance_needed
return self.__refresh_display()
class UpdateTimeDisplayWidget(QtWidgets.QLabel):
"""
Widget that displays the time since the last update.
"""
def __init__(self, colors, *args, **kwargs):
super().__init__(*args, **kwargs)
self.__time_since_update: str = "???"
self.__time_to_update_check: str = "???"
self.__update_check_needed: bool = True
self.__normal_color = colors["page_foreground"].name()
self.__alert_color = colors["red"].name()
self.setStyleSheet(
"background-color:" + colors["page_background"].name() + ";"
"border: none;"
"color:" + self.__normal_color + ";"
)
self.__refresh_display()
def __refresh_display(self) -> int:
"""
Update the display to show the current values of time to, and since,
update.
"""
self.setText(
"%s since last update. Check for updates due in %s."
% (self.__time_since_update, self.__time_to_update_check)
)
if self.__update_check_needed:
self.setStyleSheet("color:%s" % self.__alert_color)
else:
self.setStyleSheet("color:%s" % self.__normal_color)
return 0
@QtCore.Slot(str, str, bool)
def set_time_values(
self,
time_since_update: str,
time_to_update_check: str,
update_check_needed: bool,
) -> int:
"""
set the time since and to maintenance, and whether maintenance is needed.
"""
self.__time_since_update = time_since_update
self.__time_to_update_check = time_to_update_check
self.__update_check_needed = update_check_needed
return self.__refresh_display()
from PySide2 import QtWidgets, QtGui, QtCore
from global_widgets.global_typeval_popup import TypeValuePopup
class SignallingLineEditWidget(QtWidgets.QLineEdit):
manualChanged = QtCore.Signal()
def __init__(self, NativeUI, popup, label):
super().__init__()
self.installEventFilter(self)
self.label_text = label
self.NativeUI = NativeUI
self.popUp = popup#NativeUI.typeValPopupAlpha
#self.popUp = TypeValuePopup(NativeUI)#,'text edit',0,1,2,3,4)
#self.popUp.lineEdit.setValidator(None) # nsure it accepts text
#self.popUp.okButton.clicked.connect(self.okButtonPressed)
#self.popUp.cancelButton.clicked.connect(self.cancelButtonPressed)
def okButtonPressed(self):
val = self.popUp.lineEdit.text()
self.setText(val)
self.popUp.close()
self.manualChanged.emit()
def cancelButtonPressed(self):
self.popUp.lineEdit.setText(self.popUp.lineEditp.saveVal)
self.popUp.close()
def eventFilter(self, source, event):
if source is self and event.type() == QtCore.QEvent.MouseButtonDblClick:
self.popUp.populatePopup(self, self.NativeUI.display_stack.currentWidget())
self.NativeUI.display_stack.setCurrentWidget(self.popUp)
return True
return False
def value(self):
return self.text()
def setValue(self, value):
self.setText(str(value))
class LabelledLineEditWidget(QtWidgets.QWidget):
def __init__(self, NativeUI, popup, infoArray, *args, **kwargs):
super().__init__(*args, **kwargs)
# print(infoArray)
self.NativeUI = NativeUI
self.cmd_type, self.cmd_code = "", ""
self.min, self.max, self.step = 0, 10000, 0.3
self.decPlaces = 2
if len(infoArray) == 9:
self.label, self.units, self.tag, self.cmd_type, self.cmd_code, self.min, self.max, self.step, self.decPlaces = (
infoArray
)
elif len(infoArray) == 6:
self.label, self.units, self.tag, self.cmd_type, self.cmd_code, self.initText = infoArray
elif len(infoArray) == 3:
self.label, self.units, self.tag = infoArray
self.manuallyUpdated = False
layout = QtWidgets.QHBoxLayout()
widgetList = []
textStyle = "color:white; font: 16pt"
if self.label != "":
self.nameLabel = QtWidgets.QLabel(self.label)
self.nameLabel.setStyleSheet(textStyle)
self.nameLabel.setAlignment(QtCore.Qt.AlignRight)
widgetList.append(self.nameLabel)
self.simpleSpin = SignallingLineEditWidget(NativeUI, popup, self.label)
self.simpleSpin.setText(self.initText)
self.simpleSpin.setStyleSheet(
"""QDoubleSpinBox{ width:100px; font:16pt}
QDoubleSpinBox[bgColour="0"]{background-color:white; }
QDoubleSpinBox[bgColour="1"]{background-color:grey; }
QDoubleSpinBox[textColour="0"]{color:black}
QDoubleSpinBox[textColour="1"]{color:red}
QDoubleSpinBox::up-button{width:20; border:solid white; color:black }
QDoubleSpinBox::down-button{width:20; }
"""
)
self.simpleSpin.setProperty("textColour", "0")
self.simpleSpin.setProperty("bgColour", "0")
self.simpleSpin.setAlignment(QtCore.Qt.AlignCenter)
self.simpleSpin.textChanged.connect(self.textUpdate)
if self.cmd_type == "":
self.simpleSpin.setReadOnly(True)
self.simpleSpin.setProperty("bgColour", "1")
widgetList.append(self.simpleSpin)
if self.units != "":
self.unitLabel = QtWidgets.QLabel(self.units)
self.unitLabel.setStyleSheet(textStyle)
self.unitLabel.setAlignment(QtCore.Qt.AlignLeft)
widgetList.append(self.unitLabel)
for widget in widgetList:
layout.addWidget(widget)
self.setLayout(layout)
def textUpdate(self):
self.manuallyUpdated = True
def update_value(self,placeholdertemp):
newVal = self.NativeUI.get_db("personal")
if newVal == {}:
a = 1 # do nothing
else:
print('got a personal db')
self.simpleSpin.setText(newVal[self.tag])
self.simpleSpin.setProperty("textColour", "0")
self.simpleSpin.style().polish(self.simpleSpin)
def get_value(self):
return self.simpleSpin.text()
#!/usr/bin/.hev_env python3
"""
localisation_button_widget.py
Part of NativeUI. Defines the LocalisationButtonWidget class to allow the user to set
the language for the interface.
"""
__author__ = "Benjamin Mummery"
__credits__ = ["Benjamin Mummery", "Dónal Murray", "Tim Powell", "Tiago Sarmento"]
__license__ = "GPL"
__version__ = "0.0.1"
__maintainer__ = "Benjamin Mummery"
__email__ = "benjamin.mummery@stfc.ac.uk"
__status__ = "Development"
from PySide2 import QtWidgets, QtGui
import json
import os
from PySide2.QtCore import Signal
class LocalisationButtonWidget(QtWidgets.QWidget):
"""
TODO BM: add set_size and setFont
"""
SetLocalisation = Signal(dict)
def __init__(
self, localisation_config_file_paths: list, colors: dict, *args, **kwargs
):
super().__init__(*args, **kwargs)
self.__localisation_dict: dict = {}
self.__localisation_files_list: list = localisation_config_file_paths
self.__current_localisation_index: int = -1
self.localisation_button = QtWidgets.QPushButton()
hlayout = QtWidgets.QHBoxLayout()
hlayout.addWidget(self.localisation_button)
self.setLayout(hlayout)
self.localisation_button.setStyleSheet(
"background-color:" + colors["button_background_enabled"].name() + ";"
"border-color:" + colors["page_foreground"].name() + ";"
"color:" + colors["page_foreground"].name() + ";"
"border: none"
)
self.set_localisation(0)
self.localisation_button.pressed.connect(self.on_press)
def set_size(self, x: int, y: int, spacing: int = 10) -> int:
self.setFixedSize(x, y)
self.localisation_button.setFixedSize(x - spacing, y - spacing)
return 0
def setFont(self, font: QtGui.QFont) -> int:
self.localisation_button.setFont(font)
return 0
def on_press(self) -> int:
"""
When the button is pressed, update the localisation.
"""
index = self.__current_localisation_index + 1
if index >= len(self.__localisation_files_list):
index = 0
self.set_localisation(index)
return 0
def set_localisation(self, index: int) -> int:
"""
Set the current localisation parameters to those of the the specified index,
then emit the SetLocalisation signal to propogate that change.
"""
if index == self.__current_localisation_index:
return 0
self.__current_localisation_index = index
self.__import_localisation_config()
self.localisation_button.setText(self.__localisation_dict["language_name"])
self.SetLocalisation.emit(self.__localisation_dict)
return 0
def __import_localisation_config(self) -> int:
"""
Read in the current configuration
"""
with open(
self.__localisation_files_list[self.__current_localisation_index]
) as infile:
self.__localisation_dict = json.load(infile)
return 0
#!/usr/bin/env python3
"""
measurements_widget.py
Part of NativeUI. Defines the MeasurementWidget class to display current
parameters, and constructs the TabMeasurements widget to display the requisite
MeasurementWidgets.
"""
__author__ = ["Benjamin Mummery", "Tiago Sarmento"]
__credits__ = ["Benjamin Mummery", "Dónal Murray", "Tim Powell", "Tiago Sarmento"]
__license__ = "GPL"
__version__ = "0.0.1"
__maintainer__ = "Benjamin Mummery"
__email__ = "benjamin.mummery@stfc.ac.uk"
__status__ = "Development"
import logging
from PySide2 import QtCore, QtGui, QtWidgets
import math
class MeasurementsBlockWidget(QtWidgets.QWidget):
"""
Block of widgets displaying various measurement parameters
"""
def __init__(
self, NativeUI, *args, measurements: list = None, columns: int = 1, **kwargs
):
super().__init__(*args, **kwargs)
self.__grid_columns = columns
layout = QtWidgets.QGridLayout(self)
# Create "Measurements" Title
self.title_label = QtWidgets.QLabel()
self.title_label.setStyleSheet(
"color:" + NativeUI.colors["page_foreground"].name() + ";"
"background-color:" + NativeUI.colors["page_background"].name() + ";"
)
self.title_label.setSizePolicy(
QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Fixed
)
self.title_label.setAlignment(QtCore.Qt.AlignHCenter)
# Create Muasurement widgets
self.widget_list = []
for measurement in measurements:
if len(measurement) > 2:
self.widget_list.append(
MeasurementWidget(
NativeUI,
measurement[0], # Label key
measurement[1], # Key
measurement[2], # Format
)
)
else:
self.widget_list.append(
MeasurementWidget(NativeUI, measurement[0], measurement[1])
)
# Compute max number of items per column
self.__grid_rows = int(len(self.widget_list) / (self.__grid_columns))
if len(self.widget_list) % (self.__grid_columns) != 0:
self.__grid_rows += 1
# Arrange layout widgets in rows and columns
layout.addWidget(
self.title_label,
columnspan=self.__grid_columns,
alignment=QtCore.Qt.AlignHCenter,
)
i_row_min = 1 # first row for measurement widgets below the label
i_row = i_row_min
i_col = 0
for widget in self.widget_list:
layout.addWidget(widget, i_row, i_col)
i_row += 1
if i_row == self.__grid_rows + i_row_min:
i_row = i_row_min
i_col += 1
layout.setAlignment(QtCore.Qt.AlignHCenter)
self.setLayout(layout)
self.localise_text(NativeUI.text)
def set_size(
self, x: int, y: int, spacing: int = 10, widget_size_ratio: float = 2.5
) -> int:
"""
Set the size of the measurements block widget.
Sizes are computed on the assumption that the ratio of width to height for
individual measurement widgets is equal to widget_size_ratio.
If both x and y are set, MeasurementsBlockWidget will have size x by y, and
individual widgets will be size x/n_cols-spacing by MIN(y/n_rows,
x/(n_cols*widget_size_ratio))-spacing.
If x alone is set, individual widgets will have size x/n_cols-spacing by
(x/n_cols)/widget_size_ratio-spacing. MeasurementsBlockWidget will have size x
by ((x/n_cols)/widget_size_ratio)*n_rows (i.e. height expands to fit all of the
widgets).
If y alone is set, individual widgets will have size
(y/n_rows)*widget_size_ratio-spacing by y/n_rows-spacing.
MeasurementsBlockWidget will have size ((y/n_rows)*widget_size_ratio)*n_cols by
y (e.r. width expands to fit all of the widgets).
"""
if x is not None and y is not None:
self.setFixedSize(x, y)
self.title_label.setFixedWidth(x)
x_widget = int(x / self.__grid_columns)
y_widget = min(int(y / self.__grid_rows), int(x_widget / widget_size_ratio))
elif x is not None and y is None:
self.title_label.setFixedWidth(x)
x_widget = int(x / self.__grid_columns)
y_widget = int(x_widget / widget_size_ratio)
self.setFixedSize(x, y_widget * self.__grid_rows)
elif x is None and y is not None:
y_widget = int(y / self.__grid_rows)
x_widget = int(y_widget * widget_size_ratio)
self.setFixedSize(x_widget * self.__grid_columns, y)
else:
raise ValueError("set_size called with no size information")
for widget in self.widget_list:
widget.set_size(x_widget - spacing, y_widget - spacing)
return 0
def set_label_font(self, font: QtGui.QFont) -> int:
"""
Set the font of the title label and measurement names.
"""
self.title_label.setFont(font)
for widget in self.widget_list:
widget.name_display.setFont(font)
return 0
def set_value_font(self, font: QtGui.QFont) -> int:
"""
Set the font of the measurement value displays.
"""
for widget in self.widget_list:
widget.value_display.setFont(font)
return 0
@QtCore.Slot(dict)
def localise_text(self, text: dict) -> int:
"""
Update the text displayed on the title and all measurement labels.
"""
# set the text for the title label
self.title_label.setText(text["layout_label_measurements"])
# Set the text for each of the measurement widgets
for widget in self.widget_list:
widget.localise_text(text)
return 0
class MeasurementWidget(QtWidgets.QWidget):
"""
Non-interactive widget to display a single measurement along with its label.
Parameters
----------
label (str): the measuremnt label as displayed to the user (can include html).
keydir (str): the data dict in which the quantity to be displayed is stored.
key (str): the key for the measurement as used in keydir
Optional Parameters
-------------------
width (int): the width of the widget in pixels
height (int): the height of the widget in pixels
Methods
-------
update_value():
"""
def __init__(
self,
NativeUI,
label_key: str,
key: str,
format: str = "{:.1f}",
*args,
**kwargs
):
super(MeasurementWidget, self).__init__(*args, **kwargs)
self.NativeUI = NativeUI
self.label_key = label_key
self.key = key
self.format = format
# Layout and widgets
layout = QtWidgets.QVBoxLayout()
self.name_display = QtWidgets.QLabel()
self.value_display = QtWidgets.QLabel()
layout.addWidget(self.name_display)
layout.addWidget(self.value_display)
# Appearance
self.name_display.setAlignment(QtCore.Qt.AlignCenter)
self.name_display.setStyleSheet(
"color: " + self.NativeUI.colors["label_foreground"].name() + ";"
"background-color:" + self.NativeUI.colors["label_background"].name() + ";"
"border: none;"
)
self.value_display.setAlignment(QtCore.Qt.AlignCenter)
self.value_display.setStyleSheet(
"color: " + self.NativeUI.colors["label_background"].name() + ";"
"background-color: " + self.NativeUI.colors["label_foreground"].name() + ";"
"border: none;"
)
# Layout
layout.setSpacing(0)
self.setLayout(layout)
self.set_value({})
def set_value(self, data: dict) -> int:
"""
Update the displayed value
"""
if self.key is None: # widget can be created without assigning a parameter
self.value_display.setText("-")
return 0
try:
self.value_display.setText(self.__format_value(data[self.key]))
except KeyError:
self.value_display.setText("-")
return 0
def __format_value(self, number):
if self.format is "ratio":
n_digits = 1
vals = number.as_integer_ratio()
order_of_mag = math.floor(math.log(vals[0], 10))
if order_of_mag > n_digits:
vals = [
round(val / (10 ** (order_of_mag - (n_digits - 1)))) for val in vals
]
return "{:.0f}:{:.0f}".format(*vals)
return self.format.format(number)
def set_size(self, x: int, y: int) -> int:
"""
Set the size of the widget to the specified x and y values in pixels.
The overall widget size is fixed to x by y pixels. Name and value displays have
width x, and height y/3 and 2y/3 respectively.
"""
self.setFixedSize(x, y)
self.name_display.setFixedSize(x, int(y / 3))
self.value_display.setFixedSize(x, int(2 * y / 3))
return 0
def localise_text(self, text: dict) -> int:
"""
Set the name display text the relevent entry in the provided dictionary.
"""
self.name_display.setText(text[self.label_key])
return 0
class NormalMeasurementsBlockWidget(MeasurementsBlockWidget):
"""
Widget to contain the measurements for the standard page. Essentially a
wrapper for the Measurements_Block class that specifies the measurements
and number of columns.
"""
def __init__(self, NativeUI, *args, **kwargs):
measurements = [
("measurement_label_plateau_pressure", "plateau_pressure"),
("measurement_label_respiratory_rate", "respiratory_rate"),
("measurement_label_fio2_percent", "fiO2_percent"),
("measurement_label_exhaled_tidal_volume", "exhaled_tidal_volume"),
(
"measurement_label_exhaled_minute_volume",
"exhaled_minute_volume",
"{:.0f}",
),
("measurement_label_peep", "peep"),
]
super().__init__(
NativeUI, *args, measurements=measurements, columns=1, **kwargs
)
class ExpertMeasurementsBloackWidget(MeasurementsBlockWidget):
"""
Widget to contain the measurements for the standard page. Essentially a
wrapper for the Measurements_Block class that specifies the measurements
and number of columns.
"""
def __init__(self, NativeUI, *args, **kwargs):
measurements = [
("measurement_label_fio2_percent", "fiO2_percent"),
("measurement_label_inhale_exhale_ratio", "inhale_exhale_ratio", "ratio"),
(
"measurement_label_peak_inspiratory_pressure",
"peak_inspiratory_pressure",
),
("measurement_label_plateau_pressure", "plateau_pressure"),
("measurement_label_mean_airway_pressure", "mean_airway_pressure"),
("measurement_label_peep", "peep"),
("measurement_label_inhaled_tidal_volume", "inhaled_tidal_volume"),
("measurement_label_exhaled_tidal_volume", "exhaled_tidal_volume"),
("measurement_label_inhaled_minute_volume", "inhaled_minute_volume"),
(
"measurement_label_exhaled_minute_volume",
"exhaled_minute_volume",
"{:.0f}",
),
]
super().__init__(
NativeUI, *args, measurements=measurements, columns=2, **kwargs
)
#!/usr/bin/env python3
"""
numpad_wdget.py
"""
__author__ = ["Benjamin Mummery", "Tiago Sarmento"]
__credits__ = ["Benjamin Mummery", "Dónal Murray", "Tim Powell", "Tiago Sarmento"]
__license__ = "GPL"
__version__ = "0.0.1"
__maintainer__ = "Tiago Sarmento"
__email__ = "tiago.sarmento@stfc.ac.uk"
__status__ = "Prototype"
from PySide2 import QtWidgets, QtGui, QtCore
class NumberpadButton(QtWidgets.QPushButton):
"""Individual numberpad buttons are styled here. Consider moving this to NumberpadWidget."""
def __init__(self, NativeUI, *args, **kwargs):
super().__init__(*args, **kwargs)
self.setStyleSheet("background-color: " + NativeUI.colors["button_background_enabled"].name() + ";"
"color: " + NativeUI.colors["label_foreground"].name())
self.setFont(NativeUI.text_font)
class NumberpadWidget(QtWidgets.QWidget):
"""A widget with digits 0-9, a decimal point '.', and a backspace '<'.
Has one signal for any button pressed, the corresponding character is emitted with the signal.
"""
numberPressed = QtCore.Signal(str)
def __init__(self, NativeUI, *args, **kwargs):
super().__init__(*args, **kwargs)
symbol_list = ['0', '.', '<', '1', '2', '3', '4', '5', '6', '7', '8', '9']
button_dict = {}
grid = QtWidgets.QGridLayout()
ncolumns = 3
i = 0
for symbol in symbol_list:
button_dict[symbol] = NumberpadButton(NativeUI, symbol)
button_dict[symbol].pressed.connect(lambda j=symbol: self.buttonPressed(j))
grid.addWidget(button_dict[symbol], int(i/ncolumns), i%ncolumns)
i = i+1
self.setLayout(grid)
def buttonPressed(self,symbol: str):
"""Emit a signal with the button's character"""
self.numberPressed.emit(symbol)
class AlphapadWidget(QtWidgets.QWidget):
"""A widget with digits 0-9, a decimal point '.', and a backspace '<'.
Has one signal for any button pressed, the corresponding character is emitted with the signal.
"""
numberPressed = QtCore.Signal(str)
def __init__(self, NativeUI, *args, **kwargs):
super().__init__(*args, **kwargs)
symbol_list = ['Q','W','E','R','T','Y','U','I','O','P','A','S','D','F','G','H','J','K','L','Z','X','C','V','B','N','M','<']
newLineCharacters = ['P','L']
button_dict = {}
grid = QtWidgets.QGridLayout()
ncolumns = 3
i = 0
j=0
for symbol in symbol_list:
button_dict[symbol] = NumberpadButton(NativeUI, symbol)
button_dict[symbol].pressed.connect(lambda j=symbol: self.buttonPressed(j))
grid.addWidget(button_dict[symbol], j, i)
i = i+1
if symbol in newLineCharacters:
i = 0
j = j + 1
self.setLayout(grid)
def buttonPressed(self,symbol: str):
"""Emit a signal with the button's character"""
self.numberPressed.emit(symbol)
\ No newline at end of file
#!/usr/bin/env python3
"""
ok_cancel_buttons_widget.py
"""
__author__ = ["Benjamin Mummery", "Tiago Sarmento"]
__credits__ = ["Benjamin Mummery", "Dónal Murray", "Tim Powell", "Tiago Sarmento"]
__license__ = "GPL"
__version__ = "0.0.1"
__maintainer__ = "Tiago Sarmento"
__email__ = "tiago.sarmento@stfc.ac.uk"
__status__ = "Prototype"
from PySide2 import QtWidgets, QtGui, QtCore
import os
class styledButton(QtWidgets.QPushButton):
def __init__(self, NativeUI, colour, iconpath_play, *args, **kwargs):
super().__init__(*args, **kwargs)
# set icon color
pixmap = QtGui.QPixmap(iconpath_play)
mask = pixmap.mask()
pixmap.fill(NativeUI.colors["button_background_enabled"])
pixmap.setMask(mask)
self.setIcon(QtGui.QIcon(pixmap))
self.setStyleSheet(
"QPushButton[bgColour='0']{background-color: "
+ NativeUI.colors["page_foreground"].name()
+ ";}"
"QPushButton[bgColour='1']{background-color: "
+ NativeUI.colors[colour].name()
+ ";}"
"QPushButton{color: " + NativeUI.colors["page_background"].name() + ";"
"border-color: " + NativeUI.colors["page_foreground"].name() + ";"
"border-radius: 8px;"
"border:none}"
)
self.setFont(NativeUI.text_font)
self.setProperty("bgColour", "0")
self.setEnabled(False)
self.setFixedHeight(50)
# self.setFixedSize(QtCore.QSize(150, 50))
def setColour(self, option):
# print('setting colour again again')
self.setEnabled(bool(float(option)))
self.setProperty("bgColour", str(option))
self.style().polish(self)
class OkButtonWidget(styledButton):
def __init__(self, NativeUI, *args, **kwargs):
iconpath_check = os.path.join(NativeUI.iconpath, "check-solid.png")
super().__init__(NativeUI, "green", iconpath_check, *args, **kwargs)
class CancelButtonWidget(styledButton):
def __init__(self, NativeUI, *args, **kwargs):
iconpath_cross = os.path.join(NativeUI.iconpath, "times-solid.png")
super().__init__(NativeUI, "red", iconpath_cross, *args, **kwargs)
class OkSendButtonWidget(
styledButton
): # chose QWidget over QDialog family because easier to modify
def __init__(self, NativeUI, *args, **kwargs):
iconpath_play = os.path.join(NativeUI.iconpath, "play-solid.png")
super().__init__(NativeUI, "green", iconpath_play, *args, **kwargs)
#!/usr/bin/env python3
"""
page_buttons_widget.py
"""
__author__ = ["Benjamin Mummery", "Tiago Sarmento"]
__credits__ = ["Benjamin Mummery", "Dónal Murray", "Tim Powell", "Tiago Sarmento"]
__license__ = "GPL"
__version__ = "0.0.1"
__maintainer__ = "Tiago Sarmento"
__email__ = "benjamin.mummery@stfc.ac.uk"
__status__ = "Prototype"
import os
from PySide2 import QtGui, QtWidgets
from PySide2.QtCore import QSize, Signal, Slot
import logging
class PageButtonsWidget(QtWidgets.QWidget):
"""
Widget to contain the buttons that allow movement between pages. Buttons
are oriented vertically.
Button colors may be dictated by setting the colors dict, wherein
foreground and background colors are provided in QColor types. If button
colors are not set they default to red.
"""
def __init__(self, NativeUI, *args, **kwargs):
super().__init__(*args, **kwargs)
self.NativeUI = NativeUI
layout = QtWidgets.QVBoxLayout()
self.main_button = PageButton(
"", signal_value="main_page", icon=NativeUI.icons["button_main_page"]
)
self.alarms_button = PageButton(
"", signal_value="alarms_page", icon=NativeUI.icons["button_alarms_page"]
)
self.modes_button = PageButton(
"", signal_value="modes_page", icon=NativeUI.icons["button_modes_page"]
)
self.settings_button = PageButton(
"",
signal_value="settings_page",
icon=NativeUI.icons["button_settings_page"],
)
self.buttons = [
self.main_button,
self.alarms_button,
self.modes_button,
self.settings_button,
]
stylesheet = (
"QPushButton{"
" border:none"
"}"
"QPushButton[selected='0']{"
" background-color:"
+ NativeUI.colors["button_background_enabled"].name()
+ ";"
"}"
"QPushButton[selected='1']{"
" background-color:"
+ NativeUI.colors["button_background_highlight"].name()
+ ";"
"}"
"QPushButton:disabled{"
" background-color:"
+ NativeUI.colors["button_background_disabled"].name()
+ ";"
"}"
)
for button in self.buttons:
# set button appearance
button.setStyleSheet(stylesheet)
button.setIconColor(NativeUI.colors["page_foreground"])
button.pressed.connect(lambda i=button: self.set_pressed(i))
layout.addWidget(button)
self.setLayout(layout)
self.set_pressed(self.buttons[0])
def set_pressed(self, button_pressed) -> int:
"""
Set the specified buttons to enabled (unpressed) or disabled (pressed) states.
By default, all buttons in self.buttons will be made enabled except those in the
"pressed" list.
pressed can be str or list of str.
"""
for button in self.buttons:
if button == button_pressed:
button.setProperty("selected", "1")
else:
button.setProperty("selected", "0")
button.style().unpolish(button)
button.style().polish(button)
return 0
def set_size(self, x: int, y: int, spacing: int = 10) -> int:
"""
Set the size of the widget and its subwidgets.
Spacing is computed on the assumption that the buttons should be square.
If both x and y are set, buttons will be size (x - spacing) by MIN(x - spacing,
y/n - spacing) where n is the number of buttons, and the PageButtonsWidget will
have size x by y
If x alone is set, buttons will be size (x-spacing) by (x-spacing), and the
PageButtonsWidget will be size x by n*x where n is the number of buttons.
If y alone is set, buttons will be size (y/n - spacing) by (y/n - spacing) where
n is the number of buttons, and the PageButtonsWidget will have size y/n by y.
"""
button_border = int(x / 3)
n_buttons = len(self.buttons)
x_set, y_set = False, False
if x is not None:
x_set = True
if y is not None:
y_set = True
if x_set and y_set:
self.setFixedSize(x, y)
x_button = x - spacing
y_button = min([x, int(y / n_buttons)]) - spacing
elif x_set and not y_set:
self.setFixedSize(x, n_buttons * x)
x_button = x - spacing
y_button = x - spacing
elif y_set and not x_set:
x_button = int(y / n_buttons)
self.setFixedSize(x_button, y)
y_button = x_button
else:
raise ValueError("set_size called with no size information")
for button in self.buttons:
button.setFixedSize(x_button, y_button)
button.setIconSize(
QSize(x_button - button_border, y_button - button_border)
)
return 0
class PageButton(QtWidgets.QPushButton):
PageButtonPressed = Signal(str)
def __init__(self, *args, signal_value: str = None, icon: str = None, **kwargs):
super().__init__(*args, **kwargs)
self.__signal_value = signal_value
self.__icon_path = icon
self.setIconColor("white")
self.pressed.connect(self.on_press)
def setIconColor(self, color):
"""
Change the color of the icon to the specified color.
"""
pixmap = QtGui.QPixmap(self.__icon_path)
mask = pixmap.mask() # mask from alpha
pixmap.fill(color) # fill with color
pixmap.setMask(mask) # reapply mask
self.setIcon(QtGui.QIcon(pixmap))
return 0
def on_press(self):
"""
When the button is pressed, disable it and emit the PageButtonPressed signal.
"""
self.PageButtonPressed.emit(self.__signal_value)
return 0
def localise_text(self, *args, **kwargs) -> int:
pass
#!/usr/bin/env python3
"""
personal_display_widget.py
"""
__author__ = ["Benjamin Mummery", "Tiago Sarmento"]
__credits__ = ["Benjamin Mummery", "Dónal Murray", "Tim Powell", "Tiago Sarmento"]
__license__ = "GPL"
__version__ = "0.0.1"
__maintainer__ = "Tiago Sarmento"
__email__ = "tiago.sarmento@stfc.ac.uk"
__status__ = "Prototype"
from PySide2 import QtGui, QtWidgets, QtCore
class PersonalDisplayWidget(QtWidgets.QWidget):
"""
Display the current status of the personal information database
"""
def __init__(self, NativeUI, *args, **kwargs):
super().__init__(*args, **kwargs)
self.info_label = QtWidgets.QLabel("No personal information set.")
self.info_label.setStyleSheet(
"color:" + NativeUI.colors["page_foreground"].name() + ";"
)
self.info_label.setAlignment(QtCore.Qt.AlignCenter)
hlayout = QtWidgets.QHBoxLayout()
hlayout.addWidget(self.info_label)
self.setLayout(hlayout)
def set_size(self, x: int, y: int, spacing=None) -> int:
"""
Set the size of the personal display widget.
A size can be left free to change by setting its value to None.
"""
x_set, y_set = False, False
if x is not None:
x_set = True
if y is not None:
y_set = True
if x_set and y_set:
self.setFixedSize(x, y)
self.info_label.setFixedSize(x, y)
elif x_set and not y_set:
self.setFixedWidth(x)
self.info_label.setFixedWidth(x)
elif y_set and not x_set:
self.setFixedHeight(y)
self.info_label.setFixedHeight(y)
else:
raise ValueError("set_size called with no size information")
return 0
def setFont(self, font: QtGui.QFont) -> int:
"""
Overrides the existing setFont method in order to propogate the change to
subwidgets.
"""
self.info_label.setFont(font)
return 0
@QtCore.Slot(dict)
def update_status(self, new_info: dict) -> int:
"""
Update the display information.
"""
outtxt = "{name}, {height}m".format(**new_info)
self.info_label.set_text(outtxt)
return 0
#!/usr/bin/env python3
"""
plot_widget.py
Part of NativeUI.
"""
__author__ = ["Benjamin Mummery", "Tiago Sarmento"]
__credits__ = ["Benjamin Mummery", "Dónal Murray", "Tim Powell", "Tiago Sarmento"]
__license__ = "GPL"
__version__ = "0.0.1"
__maintainer__ = "Benjamin Mummery"
__email__ = "benjamin.mummery@stfc.ac.uk"
__status__ = "Development"
import logging
import numpy as np
import pyqtgraph as pg
from pyqtgraph import mkColor
from PySide2 import QtCore, QtGui, QtWidgets
class TimePlotsWidget(QtWidgets.QWidget):
def __init__(self, NativeUI, port=54322, *args, **kwargs):
super().__init__(*args, **kwargs)
self.NativeUI = NativeUI
self.time_range = 30
self.port = port
layout = QtWidgets.QVBoxLayout()
self.graph_widget = pg.GraphicsLayoutWidget()
layout.addWidget(self.graph_widget)
labelStyle = {"color": NativeUI.colors["page_foreground"], "font-size": "15pt"}
# Set up pressure - time plot
self.pressure_plot = self.graph_widget.addPlot()
self.pressure_plot.setLabel(
"left", NativeUI.text["plot_axis_label_pressure"], **labelStyle
)
self.graph_widget.nextRow()
# Set up flow - time plot
self.flow_plot = self.graph_widget.addPlot()
self.flow_plot.setLabel(
"left", NativeUI.text["plot_axis_label_flow"], **labelStyle
)
self.flow_plot.setXLink(self.pressure_plot)
self.graph_widget.nextRow()
# Set up volume -time plot
self.volume_plot = self.graph_widget.addPlot()
self.volume_plot.setLabel(
"left", NativeUI.text["plot_axis_label_volume"], **labelStyle
)
self.volume_plot.setLabel(
"bottom", NativeUI.text["plot_axis_label_time"], **labelStyle
)
self.volume_plot.setXLink(self.pressure_plot)
self.graph_widget.nextRow()
self.plots = [self.pressure_plot, self.flow_plot, self.volume_plot]
self.graph_widget.setContentsMargins(0.0, 0.0, 0.0, 0.0)
self.graph_widget.setBackground(self.NativeUI.colors["page_background"])
# Add grid, hide the autoscale button, and add the legend
for plot in self.plots:
plot.showGrid(x=True, y=True)
plot.hideButtons()
l = plot.addLegend(offset=(-1, 1))
l.setFont(NativeUI.text_font)
plot.setMouseEnabled(x=False, y=False)
plot.getAxis("bottom").setStyle(tickFont=NativeUI.text_font)
plot.getAxis("left").setStyle(tickFont=NativeUI.text_font)
plot.getAxis("left").setTextPen(NativeUI.colors["page_foreground"])
plot.getAxis("bottom").setTextPen(NativeUI.colors["page_foreground"])
# Set Range
self.update_plot_time_range(61)
# Plot styles
self.pressure_line = self.plot(
self.pressure_plot,
[0, 0],
[0, 0],
NativeUI.text["plot_line_label_pressure"],
NativeUI.colors["plot_pressure"].name(),
)
self.flow_line = self.plot(
self.flow_plot,
[0, 0],
[0, 0],
NativeUI.text["plot_line_label_flow"],
NativeUI.colors["plot_flow"].name(),
)
self.volume_line = self.plot(
self.volume_plot,
[0, 0],
[0, 0],
NativeUI.text["plot_line_label_volume"],
NativeUI.colors["plot_volume"].name(),
)
self.setLayout(layout)
def plot(self, canvas, x, y, plotname, color):
pen = pg.mkPen(color=color, width=3)
return canvas.plot(x, y, name=plotname, pen=pen)
@QtCore.Slot(dict)
def update_plot_data(self, plots: dict):
"""
Get the current plots database and update the plots to match
"""
# Replot lines with new data
self.pressure_line.setData(plots["timestamp"], plots["pressure"])
self.flow_line.setData(plots["timestamp"], plots["flow"])
self.volume_line.setData(plots["timestamp"], plots["volume"])
return 0
@QtCore.Slot()
def update_plot_time_range(self, time_range: int):
self.time_range = time_range
for plot in self.plots:
plot.setXRange(self.time_range * (-1), 0, padding=0)
plot.enableAutoRange("y", True)
return 0
@QtCore.Slot(dict)
def localise_text(self, text: dict) -> int:
"""
Update the text displayed on the axis' and legend of time plots.
"""
self.pressure_plot.setLabel("left", text["plot_axis_label_pressure"])
self.pressure_plot.legend.clear()
self.pressure_plot.legend.addItem(
self.pressure_line, text["plot_line_label_pressure"]
)
self.flow_plot.setLabel("left", text["plot_axis_label_flow"])
self.flow_plot.legend.clear()
self.flow_plot.legend.addItem(self.flow_line, text["plot_line_label_flow"])
self.volume_plot.setLabel("left", text["plot_axis_label_volume"])
self.volume_plot.setLabel("bottom", text["plot_axis_label_time"])
self.volume_plot.legend.clear()
self.volume_plot.legend.addItem(
self.volume_line, text["plot_line_label_volume"]
)
return 0
class CirclePlotsWidget(QtWidgets.QWidget):
def __init__(self, NativeUI, port=54322, *args, **kwargs):
super().__init__(*args, **kwargs)
self.NativeUI = NativeUI
self.time_range = 30
self.port = port
layout = QtWidgets.QVBoxLayout()
self.graph_widget = pg.GraphicsLayoutWidget()
layout.addWidget(self.graph_widget)
labelStyle = {"color": NativeUI.colors["page_foreground"], "font-size": "15pt"}
self.pressure_flow_plot = self.graph_widget.addPlot()
self.pressure_flow_plot.setLabel(
"left", NativeUI.text["plot_axis_label_pressure"], **labelStyle
)
self.pressure_flow_plot.setLabel(
"bottom", NativeUI.text["plot_axis_label_flow"], **labelStyle
)
self.graph_widget.nextRow()
self.flow_volume_plot = self.graph_widget.addPlot()
self.flow_volume_plot.setLabel(
"left", NativeUI.text["plot_axis_label_volume"], **labelStyle
)
self.flow_volume_plot.setLabel(
"bottom", NativeUI.text["plot_axis_label_flow"], **labelStyle
)
self.graph_widget.nextRow()
self.volume_pressure_plot = self.graph_widget.addPlot()
self.volume_pressure_plot.setLabel(
"left", NativeUI.text["plot_axis_label_pressure"], **labelStyle
)
self.volume_pressure_plot.setLabel(
"bottom", NativeUI.text["plot_axis_label_volume"], **labelStyle
)
self.graph_widget.nextRow()
self.graph_widget.nextRow()
self.plots = [
self.pressure_flow_plot,
self.flow_volume_plot,
self.volume_pressure_plot,
]
self.graph_widget.setContentsMargins(0.0, 0.0, 0.0, 0.0)
# Set background to match global background
self.graph_widget.setBackground(self.NativeUI.colors["page_background"])
# Add grid, hide the autoscale button, and add the legend
for plot in self.plots:
plot.showGrid(x=True, y=True)
plot.hideButtons()
l = plot.addLegend(offset=(-1, 1))
l.setFont(NativeUI.text_font)
plot.setMouseEnabled(x=False, y=False)
plot.getAxis("bottom").setStyle(tickFont=NativeUI.text_font)
plot.getAxis("left").setStyle(tickFont=NativeUI.text_font)
plot.getAxis("left").setTextPen(NativeUI.colors["page_foreground"])
plot.getAxis("bottom").setTextPen(NativeUI.colors["page_foreground"])
# Plot styles
self.pressure_flow_line = self.plot(
self.pressure_flow_plot,
[0, 0],
[0, 0],
NativeUI.text["plot_line_label_pressure_flow"],
NativeUI.colors["plot_pressure_flow"].name(),
)
self.flow_volume_line = self.plot(
self.flow_volume_plot,
[0, 0],
[0, 0],
NativeUI.text["plot_line_label_flow_volume"],
NativeUI.colors["plot_flow_volume"].name(),
)
self.volume_pressure_line = self.plot(
self.volume_pressure_plot,
[0, 0],
[0, 0],
NativeUI.text["plot_line_label_volume_pressure"],
NativeUI.colors["plot_volume_pressure"].name(),
)
self.setLayout(layout)
def plot(self, canvas, x, y, plotname, color):
pen = pg.mkPen(color=color, width=3)
return canvas.plot(x, y, name=plotname, pen=pen)
@QtCore.Slot(dict)
def update_plot_data(self, plots: dict):
"""
Update the plots to match the new data.
"""
self.pressure_flow_line.setData(plots["cycle_flow"], plots["cycle_pressure"])
self.flow_volume_line.setData(plots["cycle_volume"], plots["cycle_flow"])
self.volume_pressure_line.setData(
plots["cycle_pressure"], plots["cycle_volume"]
)
return 0
@QtCore.Slot(dict)
def localise_text(self, text: dict) -> int:
"""
Update the text displayed on the axis' and legend of circle plots.
"""
self.pressure_flow_plot.setLabel("left", text["plot_axis_label_pressure"])
self.pressure_flow_plot.setLabel("bottom", text["plot_axis_label_flow"])
self.pressure_flow_plot.legend.clear()
self.pressure_flow_plot.legend.addItem(
self.pressure_flow_line, text["plot_line_label_pressure_flow"]
)
self.flow_volume_plot.setLabel("left", text["plot_axis_label_flow"])
self.flow_volume_plot.setLabel("bottom", text["plot_axis_label_volume"])
self.flow_volume_plot.legend.clear()
self.flow_volume_plot.legend.addItem(
self.flow_volume_line, text["plot_line_label_flow_volume"]
)
self.volume_pressure_plot.setLabel("left", text["plot_axis_label_volume"])
self.volume_pressure_plot.setLabel("bottom", text["plot_axis_label_pressure"])
self.volume_pressure_plot.legend.clear()
self.volume_pressure_plot.legend.addItem(
self.volume_pressure_line, text["plot_line_label_volume_pressure"]
)
return 0
class ChartsPlotWidget(QtWidgets.QWidget):
def __init__(self, port=54322, *args, colors: dict = {}, **kwargs):
super().__init__(*args, **kwargs)
self.port = port
layout = QtWidgets.QHBoxLayout()
# Set up the graph widget
self.graph_widget = pg.GraphicsLayoutWidget()
layout.addWidget(self.graph_widget)
labelStyle = {"color": "#FFF", "font-size": "15pt"}
# Add the plot axes to the graph widget
self.display_plot = self.graph_widget.addPlot()
self.display_plot.setLabel("left", "????", **labelStyle)
self.display_plot.setLabel("bottom", "????", **labelStyle)
self.display_plot.getAxis("left").setTextPen("w")
self.display_plot.getAxis("bottom").setTextPen("w")
self.graph_widget.nextRow()
# Store plots in a list in case we need to add additional axes in the future.
plots = [self.display_plot]
# Create lines
self.lines = {
"pressure": self.plot(
self.display_plot, [0, 10], [5, -5], "pressure", (0, 0, 0, 0)
),
"flow": self.plot(
self.display_plot,
[0, 2, 4, 6, 8, 10],
[3, 1, 4, 1, 5, 9],
"flow",
(0, 0, 0, 0),
),
}
# Store the colors of the lines
self.colors = {"pressure": colors["plot_pressure"], "flow": colors["plot_flow"]}
self.graph_widget.setContentsMargins(0.0, 0.0, 0.0, 0.0)
self.graph_widget.setBackground(colors["page_background"])
self.legends = []
font = QtGui.QFont() # TODO: change to an imported font from NativeuI
font.setPixelSize(25)
for plot in plots:
plot.showGrid(x=True, y=True)
plot.hideButtons()
plot.setMouseEnabled(x=False, y=False)
self.legends.append(plot.addLegend(offset=(-1, 1)))
plot.getAxis("bottom").setStyle(tickFont=font)
plot.getAxis("left").setStyle(tickFont=font)
self.setLayout(layout)
self.hide_line("pressure")
self.show_line("pressure")
def setFont(self, font: QtGui.QFont) -> int:
for l in self.legends:
l.setFont(font)
return 0
def update_plot_data(self):
pass
def plot(self, canvas, x, y, plotname, color):
pen = pg.mkPen(color=color, width=3)
return canvas.plot(x, y, name=plotname, pen=pen)
@QtCore.Slot(str)
def show_line(self, key: str) -> int:
"""
Show the specified line
"""
self.lines[key].setPen(pg.mkPen(color=self.colors[key], width=3))
return 0
@QtCore.Slot(str)
def hide_line(self, key: str) -> int:
"""
Hide the specified line
"""
self.lines[key].setPen(pg.mkPen(color=(0, 0, 0, 0), width=0))
return 0
@QtCore.Slot(dict)
def localise_text(self, text: dict) -> int:
"""
Update the text displayed on the axis' and legend of time plots.
Currently a placeholder.
"""
self.display_plot.setLabel("left", text["plot_axis_label_pressure"])
# self.display_plot.legend.clear()
# self.display_plot.legend.addItem(
# self.pressure_line, text["plot_line_label_pressure"]
# )
return 0
#!/usr/bin/env python3
"""
spin_buttons_widget.py
"""
__author__ = ["Benjamin Mummery", "Tiago Sarmento"]
__credits__ = ["Benjamin Mummery", "Dónal Murray", "Tim Powell", "Tiago Sarmento"]
__license__ = "GPL"
__version__ = "0.0.1"
__maintainer__ = "Tiago Sarmento"
__email__ = "tiago.sarmento@stfc.ac.uk"
__status__ = "Development"
# from customPopup2 import customPopup2
import sys
from PySide2 import QtCore, QtGui, QtWidgets
from global_widgets.global_typeval_popup import TypeValuePopup
# from global_widgets.global_ok_cancel_buttons import okButton, cancelButton
from widget_library.ok_cancel_buttons_widget import OkButtonWidget, CancelButtonWidget
from global_widgets.global_spinbox import signallingSpinBox
# from global_widgets.global_send_popup import SetConfirmPopup
class SpinButton(QtWidgets.QFrame):
"""TO DO: Implement command sending"""
def __init__(self, NativeUI, popup, infoArray):
super().__init__()
self.manuallyUpdated = False
# self.setStyleSheet("background-color:blue;")
self.currentVal = 0
if len(infoArray) == 10:
self.label_text, self.units, self.tag, self.cmd_type, self.cmd_code, self.min, self.max, self.initVal, self.step, self.decPlaces = (
infoArray
)
#print('before')
#print(self.cmd_type)
self.cmd_type = self.cmd_type.replace('SET_TARGET_','SET_TARGET_CURRENT')
#self.cmd_type = settings[3]
#self.cmd_code = settings[4]
#self.tag = settings[2]
self.NativeUI = NativeUI
self.layout = QtWidgets.QVBoxLayout()
self.layout.setSpacing(0)
self.layout.setMargin(0)
# create and style label
self.label = QtWidgets.QLabel()
self.label.setText(self.label_text)
labelBgColour = "rgb(60,58,60)"
self.label.setStyleSheet(
"color:white;"
"background-color:" + labelBgColour + ";"
"" # border-radius:4px;"
"" # border: 2px solid white"
)
self.label.setAlignment(QtCore.Qt.AlignCenter)
# self.label.setFixedHeight(45)
# self.label.setFont(NativeUI.text_font)
self.layout.addWidget(self.label)
# self.setFont(NativeUI.text_font)
self.simpleSpin = signallingSpinBox(NativeUI, popup, self.label_text, self.min, self.max, self.initVal, self.step, self.decPlaces)
self.simpleSpin.lineEdit().setStyleSheet("border:blue;")
# self.simpleSpin.setFixedHeight(100)
# self.simpleSpin.setFont(NativeUI.text_font)
boxStyleString = (
"QDoubleSpinBox{"
" border:none;"
" background-color: black;"
"}"
"QDoubleSpinBox[colour='0'] {"
" color:green;"
"}"
"QDoubleSpinBox[colour='1'] {"
" color:rgb(144,231,211);"
"}"
"QDoubleSpinBox[colour='2'] {"
" color:red;"
"}"
)
# self.setFont(NativeUI.text_font)
upButtonStyleString = "QDoubleSpinBox::up-button{" "height:50;" "width:50;" "}"
# upButtonPressedStyleString = (
# "QDoubleSpinBox::up-button:pressed{ border:orange;}"
# )
downButtonStyleString = upButtonStyleString.replace(
"up", "down"
) # "QDoubleSpinBox::down-button{image: url('" + downImage + "');}"
# downButtonPressedStyleString = "" # "QDoubleSpinBox::down-button:pressed{background-color:white;image: url('" + upImage + "');}"
upButtonStyleString = "QDoubleSpinBox::up-button{" "height:30;" "width:40;" "}"
downButtonStyleString = upButtonStyleString.replace("up", "down")
self.simpleSpin.setStyleSheet(
boxStyleString + upButtonStyleString + downButtonStyleString
)
self.simpleSpin.setProperty("colour", "1")
self.simpleSpin.setButtonSymbols(
QtWidgets.QAbstractSpinBox.ButtonSymbols.PlusMinus
)
self.simpleSpin.setAlignment(QtCore.Qt.AlignCenter)
self.simpleSpin.manualChanged.connect(self.manualChanged)
self.simpleSpin.programmaticallyChanged.connect(self.manualChanged)
self.simpleSpin.setSizePolicy(
QtGui.QSizePolicy.Expanding, QtGui.QSizePolicy.Fixed
)
self.layout.addWidget(self.simpleSpin)
self.setLayout(self.layout)
self.setFixedWidth(300)
# self.setStyleSheet("border:2px solid white; border-radius:4px; padding:0px;")
def update_value(self, db):
newVal = db
if (newVal == {}) or (self.cmd_code == ""):
a = 1 # do nothing
else:
if not self.manuallyUpdated:
self.simpleSpin.setValue(newVal[self.tag])
self.setTextColour(1)
else:
if round(self.simpleSpin.value(),self.decPlaces) == round(newVal[self.tag],self.decPlaces):
self.manuallyUpdated = False
self.setTextColour(1)
def get_value(self):
return self.simpleSpin.value()
def set_value(self, value):
self.simpleSpin.setValue(value)
self.manuallyUpdated = True
self.simpleSpin.programmaticallyChanged.emit()
def manualChanged(self):
"""Called when user manually makes a change. Stops value from updating and changes colour"""
self.manuallyUpdated = True
self.setTextColour(2)
return 0
def setTextColour(self, option):
"""Set text colour and unpolish polish widget to show change"""
self.simpleSpin.setProperty("colour", option)
self.simpleSpin.style().unpolish(self.simpleSpin)
self.simpleSpin.style().polish(self.simpleSpin)
return 0
def set_label_font(self, font) -> int:
"""
Set the font for the spinbox label.
"""
self.label.setFont(font)
return 0
def set_value_font(self, font) -> int:
"""
Set the font for the spinbox value display.
"""
self.simpleSpin.setFont(font)
return 0
#!/usr/bin/env python3
"""
ok_cancel_buttons_widget.py
"""
__author__ = ["Benjamin Mummery", "Tiago Sarmento"]
__credits__ = ["Benjamin Mummery", "Dónal Murray", "Tim Powell", "Tiago Sarmento"]
__license__ = "GPL"
__version__ = "0.0.1"
__maintainer__ = "Tiago Sarmento"
__email__ = "tiago.sarmento@stfc.ac.uk"
__status__ = "Prototype"
from PySide2 import QtWidgets, QtGui, QtCore
from datetime import datetime
import os
class calibrationWidget(
QtWidgets.QWidget
):
def __init__(self, NativeUI, key, infoDict, *args, **kwargs):
super().__init__(*args, **kwargs)
self.NativeUI = NativeUI
self.key = key
self.infoDict = infoDict
hlayout = QtWidgets.QHBoxLayout()
self.button = QtWidgets.QPushButton(infoDict["label"])
hlayout.addWidget(self.button)
self.progBar = QtWidgets.QProgressBar()
hlayout.addWidget(self.progBar)
self.lastTime = datetime.fromtimestamp(infoDict['last_performed'])
self.lastTime.strftime('%d-%m-%y %H:%M')
self.lineEdit = QtWidgets.QLineEdit(str(self.lastTime))
hlayout.addWidget(self.lineEdit)
self.setLayout(hlayout)
\ No newline at end of file
from global_widgets.global_spinbox import labelledSpin
from widget_library.startup_calibration_widget import calibrationWidget
from widget_library.ok_cancel_buttons_widget import (
OkButtonWidget,
CancelButtonWidget,
OkSendButtonWidget,
)
from global_widgets.global_send_popup import SetConfirmPopup
from PySide2.QtWidgets import QRadioButton
from datetime import datetime
import json
from PySide2 import QtWidgets, QtGui, QtCore
class StartupHandler(
QtWidgets.QWidget
): # chose QWidget over QDialog family because easier to modify
UpdateModes = QtCore.Signal(dict)
OpenPopup = QtCore.Signal(list)
settingToggle = QtCore.Signal(str)
def __init__(self, NativeUI, *args, **kwargs):
super().__init__(*args, **kwargs)
self.NativeUI = NativeUI
self.buttonDict = {}
self.spinDict = {}
self.calibDict = {}
self.modeRadioDict = {}
self.settingsRadioDict = {}
self.calibs_done_dict = {}
def add_widget(self, widget, key: str):
if isinstance(widget, labelledSpin):
self.spinDict[key] = widget
widget.cmd_type = widget.cmd_type.replace("startup", "CURRENT")
if isinstance(widget, calibrationWidget):
self.calibDict[key] = widget
if (
isinstance(widget, OkButtonWidget)
or isinstance(widget, CancelButtonWidget)
or isinstance(widget, OkSendButtonWidget)
):
self.buttonDict[key] = widget
if isinstance(widget, QRadioButton):
if widget.text() in self.NativeUI.modeList:
self.modeRadioDict[key] = widget
else:
self.settingsRadioDict[key] = widget
def handle_mode_radiobutton(self, checked, radio):
if checked:
self.NativeUI.currentMode = radio.text()
def handle_settings_radiobutton(self, radioButtonState, radioKey):
"""TODO Docstring"""
mode = self.get_mode(radioKey)
spinKey = radioKey.replace("radio", "spin")
spinBox = self.spinDict[spinKey]
spinBox.setEnabled(radioButtonState)
if mode == self.NativeUI.currentMode:
self.settingToggle.emit(spinBox.label)
def handle_calibrationPress(self, calibrationWidget) -> int:
"""
When a calibration buttonis pressed, run the corresponding calibration. If all
calibrations are completed, emit the CalibrationComplete signal.
Currently doesn't actually do any calibrations, just a placeholder for now.
"""
calibrationWidget.progBar.setValue(100)
calibrationWidget.lineEdit.setText("completed")
with open("NativeUI/configs/startup_config.json", "r") as json_file:
startupDict = json.load(json_file)
startupDict[calibrationWidget.key]["last_performed"] = int(
datetime.now().timestamp()
)
with open("NativeUI/configs/startup_config.json", "w") as json_file:
json.dump(startupDict, json_file)
self.calibs_done_dict[calibrationWidget.key] = True
if self.all_calibs_done():
for key in ["nextButton", "skipButton"]:
self.buttonDict[key].setEnabled(True)
self.buttonDict[key].setColour(1)
return 0
def all_calibs_done(self) -> bool:
"""
Check if all required calibrations are complete. For now this is as simple as
comparing the self.calibs_done_dict to the self.calibDict.
"""
for key in self.calibDict:
if key not in self.calibs_done_dict:
return False
return True
def handle_sendbutton(self):
message, command = [], []
for widget in self.spinDict:
setVal = self.spinDict[widget].get_value()
setVal = round(setVal, self.spinDict[widget].decPlaces)
message.append("set" + widget + " to " + str(setVal))
command.append(
[self.spinDict[widget].cmd_type, self.spinDict[widget].cmd_code, setVal]
)
for com in command:
self.NativeUI.q_send_cmd(*com)
self.NativeUI.q_send_cmd(
"SET_MODE", self.NativeUI.currentMode.replace("/", "_").replace("-", "_")
)
def handle_nextbutton(self, stack) -> int:
"""
Handle the pressing of the nextbutton
"""
currentIndex = stack.currentIndex()
nextIndex = currentIndex + 1
totalLength = stack.count()
stack.setCurrentIndex(nextIndex)
if nextIndex == totalLength - 1:
self.buttonDict["nextButton"].setColour(0)
else:
self.buttonDict["nextButton"].setColour(1)
self.buttonDict["backButton"].setColour(1)
def handle_backbutton(self, stack):
print("backbutton pressed")
currentIndex = stack.currentIndex()
nextIndex = currentIndex - 1
stack.setCurrentIndex(nextIndex)
if nextIndex == 0:
self.buttonDict["backButton"].setColour(0)
else:
self.buttonDict["backButton"].setColour(1)
self.buttonDict["nextButton"].setColour(1)
def get_mode(self, key: str):
for mode in self.NativeUI.modeList:
if mode in key:
return mode
"""
New version of what was template_main_pages.
"""
from PySide2 import QtWidgets
from PySide2.QtGui import QFont
class SwitchableStackWidget(QtWidgets.QWidget):
def __init__(
self, colors, text, widget_list: list, button_label_keys: list, *args, **kwargs
):
super().__init__(*args, **kwargs)
self.widget_list = widget_list
self.button_list = self.__make_buttons(colors, text, button_label_keys)
self.__build()
if len(self.button_list) > 0:
self.setTab(self.button_list[0])
def rebuild(self, colors, text, widget_list, button_label_keys):
"""
For an already created SwitchableStackWidget, change the tabs in the stack.
"""
self.__clear()
self.widget_list = widget_list
self.button_list = self.__make_buttons(colors, text, button_label_keys)
self.__build()
self.setTab(self.button_list[0])
return 0
def __clear(self):
"""
Delete all widgets in the current layout
"""
for i in reversed(range(self.layout.count())):
self.layout.itemAt(i).widget().setParent(None)
return 0
def __build(self):
"""
Construct the widget for the current status of widget_list and button_list
"""
vlayout = QtWidgets.QVBoxLayout()
hButtonLayout = QtWidgets.QHBoxLayout()
self.stack = QtWidgets.QStackedWidget()
assert len(self.widget_list) == len(self.button_list)
for button, widget in zip(self.button_list, self.widget_list):
hButtonLayout.addWidget(button)
self.stack.addWidget(widget)
button.pressed.connect(lambda i=button: self.setTab(i))
vlayout.addLayout(hButtonLayout)
vlayout.addWidget(self.stack)
self.setLayout(vlayout)
def __make_buttons(self, colors, text, button_label_keys: list) -> list:
"""
Make the selector buttons
"""
return [
SelectorButtonWidget(colors, text, label_key)
for label_key in button_label_keys
]
def setTab(self, button_pressed) -> int:
"""
Show the specified tab
"""
for button, widget in zip(self.button_list, self.widget_list):
if button == button_pressed:
button.setProperty("selected", "1")
self.stack.setCurrentWidget(widget)
else:
button.setProperty("selected", "0")
button.style().unpolish(button)
button.style().polish(button)
return 0
def addTab(self, widget, label: str) -> int:
"""
Add a tab to the stack
"""
self.__clear()
self.widget_list.append(widget)
self.button_list += self.__make_buttons([label])
self.__build()
self.setTab(self.button_list[0])
return 0
def setFont(self, font: QFont) -> int:
for button in self.button_list:
button.setFont(font)
def setButtonSize(self, x: int, y: int, spacing: int = 10) -> int:
if x is not None and y is not None:
for button in self.button_list:
button.setFixedSize(x - spacing, y)
elif x is not None and y is None:
for button in self.button_list:
button.setFixedWidth(x - spacing)
elif x is None and y is not None:
for button in self.button_list:
button.setFixedHeight(y)
else:
raise AttributeError("setButtonSize called without usable size information")
return 0
def localise_text(self, text: dict) -> int:
for button in self.button_list:
button.localise_text(text)
return 0
class SelectorButtonWidget(QtWidgets.QPushButton):
def __init__(self, colors: dict, text: dict, label_key: str, *args, **kwargs):
super().__init__(text[label_key], *args, **kwargs)
self.__label_key = label_key
style = (
"QPushButton[selected='0']{"
" color: " + colors["button_foreground_enabled"].name() + ";"
" background-color: " + colors["button_background_enabled"].name() + ";"
" border:none"
"}"
"QPushButton[selected='1']{"
" color: " + colors["button_foreground_highlight"].name() + ";"
" background-color:" + colors["button_background_highlight"].name() + ";"
" border:none"
"}"
"QPushButton:disabled{"
" color:" + colors["button_foreground_disabled"].name() + ";"
" background-color:" + colors["button_background_disabled"].name() + ";"
" border:none"
"}"
)
self.setStyleSheet(style)
self.setProperty("selected", "0")
def localise_text(self, text: dict) -> int:
self.setText(text[self.__label_key])
return 0
#!/usr/bin/env python3
"""
tab_expert.py
"""
__author__ = ["Benjamin Mummery", "Tiago Sarmento"]
__credits__ = ["Benjamin Mummery", "Dónal Murray", "Tim Powell", "Tiago Sarmento"]
__license__ = "GPL"
__version__ = "0.0.1"
__maintainer__ = "Benjamin Mummery"
__email__ = "benjamin.mummery@stfc.ac.uk"
__status__ = "Development"
# from PySide2 import QtWidgets, QtGui, QtCore
# from global_widgets.global_spinbox import simpleSpin
# from global_widgets.global_select_button import selectorButton
# from global_widgets.global_send_popup import SetConfirmPopup
from global_widgets.template_set_values import TemplateSetValues
class TabExpert(TemplateSetValues):
def __init__(self, NativeUI, *args, **kwargs):
super().__init__(NativeUI, *args, **kwargs)
self.setPacketType("readback")
controlDict = {
"Buffers": [
[
"Calibration",
"ms",
"duration_calibration",
"SET_DURATION",
"CALIBRATION",
0,
1000,
50,
0,
],
["Purge", "ms", "duration_buff_purge", "SET_DURATION", "BUFF_PURGE"],
["Flush", "ms", "duration_buff_flush", "SET_DURATION", "BUFF_FLUSH"],
[
"Pre-fill",
"ms",
"duration_buff_prefill",
"SET_DURATION",
"BUFF_PREFILL",
],
["Fill", "ms", "duration_buff_prefill", "SET_DURATION", "BUFF_FILL"],
[
"Pre-inhale",
"ms",
"duration_buff_pre_inhale",
"SET_DURATION",
"BUFF_PRE_INHALE",
],
],
"PID": [
["KP", "", "kp", "SET_PID", "KP"],
["KI", "", "ki", "SET_PID", "KI"],
["KD", "", "kd", "SET_PID", "KD"],
["PID Gain", "", "pid_gain", "SET_PID", "PID_GAIN"],
[
"Max. PP",
"",
"max_patient_pressure",
"SET_PID",
"MAX_PATIENT_PRESSURE",
],
],
"Valves": [
["Air in", "", "valve_air_in"],
["O2 in", "", "valve_o2_in"],
["Inhale", "", "valve_inhale"],
["Exhale", "", "valve_exhale"],
["Purge valve", "", "valve_purge"],
["Inhale Opening", "%", "valve_inhale_percent"],
["Exhale Opening", "%", "valve_exhale_percent"],
],
"Breathing": [
["Inhale", "ms", "duration_inhale", "SET_DURATION", "INHALE"],
["Pause", "ms", "duration_pause", "SET_DURATION", "PAUSE"],
["Exhale fill", "ms", "duration_exhale", "SET_DURATION", "EXHALE_FILL"],
["Exhale", "ms", "duration_exhale", "SET_DURATION", "EXHALE"],
["I:E Ratio", "", "inhale_exhale_ratio"],
],
}
self.addExpertControls(controlDict)
self.addButtons()
self.finaliseLayout()
#!/usr/bin/env python3
"""
ventialtor_start_stop_buttons_widget.py
"""
__author__ = ["Benjamin Mummery", "Tiago Sarmento"]
__credits__ = ["Benjamin Mummery", "Dónal Murray", "Tim Powell", "Tiago Sarmento"]
__license__ = "GPL"
__version__ = "0.0.1"
__maintainer__ = "Tiago Sarmento"
__email__ = "tiago.sarmento@stfc.ac.uk"
__status__ = "Prototype"
import logging
from PySide2 import QtGui, QtWidgets
from PySide2.QtCore import QSize
from PySide2 import QtCore
from global_widgets.tab_hold_buttons import holdButton
class VentilatorStartStopButtonsWidget(QtWidgets.QWidget):
"""
TODO
"""
def __init__(self, NativeUI, *args, size: QSize = None, **kwargs):
super().__init__(*args, **kwargs)
self.NativeUI = NativeUI
if size is not None:
self.__button_size = size
else:
self.__button_size = QSize(100, 20)
layout = QtWidgets.QVBoxLayout()
self.button_start = QtWidgets.QPushButton() # QtWidgets.QPushButton()
self.button_start.pressed.connect(lambda: NativeUI.send_cmd("GENERAL","START"))
self.button_stop = holdButton(NativeUI) # QtWidgets.QPushButton()
self.button_standby = holdButton(NativeUI) # QtWidgets.QPushButton()
self.__buttons = [self.button_start, self.button_stop, self.button_standby]
self.localise_text(NativeUI.text)
self.__buttoncommand = [""]
for button, text in zip(self.__buttons, self.__buttontext):
button.setText(text)
if isinstance(button, holdButton):
button.popUp.completeLabel.setText("Ventilation " + text)
layout.addWidget(button)
button.setStyleSheet(
"background-color:"
+ NativeUI.colors["button_background_enabled"].name()
+ ";"
"color:" + NativeUI.colors["button_foreground_enabled"].name() + ";"
"border:none"
)
self.setLayout(layout)
def set_size(self, x: int, y: int, spacing: int = 10) -> int:
"""
Set the size of the widget and its subwidgets.
Sizing is computed on the assumption that the buttons should be as large as
possible.
If both x and y are set, VentilatorStartStopButtonsWidget will have size x by y,
and buttons will be size (x - spacing) by MIN(x - spacing, y/n - spacing) where
n is the number of buttons.
If x alone is set, VentilatorStartStopButtonsWidget will have width x, and
buttons will have width x-spacing. Both will expand to fill the available
vertical space.
If y alone is set, VentilatorStartStopButtonsWidget will have height y, and
buttons will have height (y/n - spacing). Both will expand to fill the available
horizontal space.
"""
n_buttons = len(self.__buttons)
x_set, y_set = False, False
if x is not None:
x_set = True
if y is not None:
y_set = True
if x_set and y_set:
self.setFixedSize(x, y)
x_button = x - spacing
y_button = min([x, int(y / n_buttons)]) - spacing
for button in self.__buttons:
button.setFixedSize(x_button, y_button)
button.setSizePolicy(
QtWidgets.QSizePolicy.Fixed, QtWidgets.QSizePolicy.Fixed
)
elif x_set and not y_set:
self.setFixedWidth(x)
for button in self.__buttons:
button.setFixedWidth(x - spacing)
button.setSizePolicy(
QtWidgets.QSizePolicy.Fixed, QtWidgets.QSizePolicy.Expanding
)
elif y_set and not x_set:
self.setFixedHeight(y)
y_button = int(y / n_buttons)
for button in self.__buttons:
button.setFixedHeight(y_button)
button.setSizePolicy(
QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Fixed
)
else:
raise ValueError("set_size called with no size information")
return 0
def setFont(self, font: QtGui.QFont) -> int:
"""
Overrides the existing setFont method in order to propogate the change to
subwidgets.
"""
for button in self.__buttons:
button.setFont(font)
return 0
@QtCore.Slot(dict)
def localise_text(self, text: dict):
self.__buttontext = [
text["start_button"],
text["stop_button"],
text["standby_button"],
]
self.button_start.setText(text["start_button"])
self.button_stop.setText(text["stop_button"])
self.button_standby.setText(text["standby_button"])
return 0
# High Energy Ventilator
Rapidly-producible ventilator developed with CERN and collaborating institutes for the COVID-19 pandemic
Disclaimer
==========
See LICENCE file
> Note: this README is a WIP.
Test - moved to CERN gitlab
Rapidly-producible ventilator developed with CERN and collaborating institutes for the COVID-19 pandemic.
## Contents
* [Installation](#installation)
* [Usage](#usage)
* [Testing](#testing)
* [License](#license)
* [Acknowledgments](#acknowledgments)
## Installation
The following has been tested using Python 3.7 on a Raspberry Pi 4 4GB model B and Raspbian Desktop VM (Buster).
You can install a VM using either [VirtualBox](https://www.virtualbox.org/) or [VMWare Fusion Player](https://www.vmware.com/products/fusion.html), and download the Raspbian OS from [here](https://www.raspberrypi.org/software/raspberry-pi-desktop/).
### Prerequisites
Raspbian Version: Raspbian GNU/Linux 10 (buster)
Python Version: 3.7
Ansible: Version 2.8 or later
### Setup
There are 2 methods of installation, [using SSH](#ssh-installation) or installing [locally](#local-installation). Both methods can be used with both a Raspberry Pi and a VM depending on your setup and personal preference.
The advantage of SSH is that the Raspberry Pi does not need dedicated peripherals and you can make use of your local development environment. However, there is no control over the default installation location (`/home/pi/hev`). Whereas, the local installation method installs all the requirements in the location you clone this repo to.
#### SSH Installation
On your local PC, install [ansible](https://docs.ansible.com/ansible/latest/installation_guide/index.html) at least version 2.8. The easiest way to do this via `pip`:
```bash
pip3 install ansible
```
> Make sure that SSH is enabled. To check this to go:
>
> With GUI: `Preferences > Raspberry Pi Configuration > Interfaces`
>
> Without GUI: `sudo touch /boot/ssh`
>
> ***WARNING:** There may be extra steps if you are using a VM on your local machine.*
For ansible to work, you need to create an ssh keypair with your Raspberry Pi / VM. On your local PC generate a ssh keypair and copy it over to the pi:
```bash
ssh-keygen
ssh-copy-id pi@IP-ADDRESS
```
To obtain the IP Address of your Raspberry Pi / VM, on your Raspberry Pi / VM run:
```bash
hostname -I
```
Run and follow the prompts:
```bash
./setup.sh
```
Ansible logs are saved in `ansible/playbooks/logs`.
#### Local Installation
Change default Python to Python3.7:
```bash
sudo update-alternatives --install /usr/bin/python python /usr/bin/python2.7 1
sudo update-alternatives --install /usr/bin/python python /usr/bin/python3.7 2
```
Install ansible with the following:
```bash
pip3 install ansible
sudo reboot
```
Clone this repo and checkout the `release/ui` branch:
```bash
git clone https://ohwr.org/project/hev.git
cd hev
git checkout ui_dev
```
Run `setup.sh` and enter `localhost` when asked for an IP address.
```bash
./setup.sh
```
Ansible logs are saved in `ansible/playbooks/logs`.
## Usage
### Running the HEV UI
Running the HEV UI requires three separate python process running in the same virtualenv from the `hev` directory. This varies depending on your installation method as follows:
* Remote Installation:
```bash
cd /home/pi/hev
```
* Local Installation:
```bash
cd /path/to/hev
```
1) Run ArduinoEmulator
Note that a selection of dump files are provided in the `raspberry-dataserver/share` dir.
```bash
source .hev_env/bin/activate
./raspberry-dataserver/ArduinoEmulator.py -f raspberry-dataserver/share/B6-20201207.dump
```
2) Run hevserver in another shell
```bash
source .hev_env/bin/activate
cd raspberry-dataserver
./hevserver.py --use-dump-data
```
3) Run NativeUI in another shell
```bash
source .hev_env/bin/activate
./NativeUI/NativeUI.py
```
### Command-Line Arguments
NativeUI.py accepts the following command line arguments:
| Command | Pattern(s) | Effect | Example |
|---------|------------|--------|---------|
| Windowed | -w, --windowed | Create the user interface in windowed mode | ```./NativeUI/NativeUI.py -w``` |
| Debug | -d, --debug | Set the logging output level. By default only log messages of ERROR or higher priority will be displayed. The debug flag changes this to messages of INFO or higher if provided once, or DEBUG or higher if provided twice. | ```./NativeUI/NativeUI.py -dd```|
| Resolution | -r , --resolution | Takes the following string as specifying the desired size of the UI in pixels. Resolutions should be two integers separated by a non-numerical character. | ```./NativeUI/NativeUI.py -r 1600x900```
## Testing
For full testing documentation please refer to the [testing README](NativeUI/tests/README.md).
## Unit Tests
To run the unit tests on a Raspberry Pi or VM, run the following:
```bash
source .hev_env/bin/activate
pytest NativeUI
```
### Coverage
To get pytest coverage run from the root of the repo:
```bash
pip install pytest-cov
pytest --cov=NativeUI NativeUI
```
## License
See [LICENSE](LICENCE.txt) file.
## Acknowledgments
* LIST OF PEOPLE / INSTITUTES
* CERN
* STFC
# Installing the Raspberry Pi
- First assumption - we're using latest Raspbian
- Install Raspbian with the Raspberry Pi imager https://www.raspberrypi.org/downloads/
- In the boot partition of the SD card create and empty file called `ssh`
- Boot as default and set your preferred options for country, password, etc.
> From command line on your local PC (not the pi):
- generate and copy your ssh keys if you don't want a password every time
- ssh-keygen
- ssh-copy-id pi@MY-RPI-IPADDRESS
- then setup ansible:
- install ansible on your local PC with your package manager
- download the hev-sw repo:
```
git clone https://github.com/hev-sw/hev-sw
cd hev-sw/ansible
source hev-ansible.sh
cd playbooks
```
- add the address of your Raspberry Pi to the `hosts` file under the section `[hevpi]`. The default host file is found at /etc/ansible/hosts
- example :
```
[hevpi]
192.168.1.23
```
- run the ansible playbooks
```
ansible-playbook firstboot.yml
ansible-playbook install_software.yml
```
- finally reboot the pi
```
ssh pi@MY-RPI-IPADDRESS "sudo /sbin/reboot"
```
#!/bin/bash
# © Copyright CERN, Riga Technical University and University of Liverpool 2020.
# All rights not expressly granted are reserved.
#
......@@ -20,9 +22,10 @@
# of all those involved with the High Energy Ventilator project
# (https://hev.web.cern.ch/).
ansible_home=`pwd`/playbooks
ansible_home=$(pwd)/playbooks
export ANSIBLE_CONFIG=$ansible_home/ansible.cfg
export ANSIBLE_INVENTORY=$ansible_home/hosts
export ANSIBLE_LIBRARY=$ansible_home
export ANSIBLE_LOG_PATH=$ansible_home/logs/"$(date +'%Y%m%d-%H%M%S')".log
#export ANSIBLE_DEBUG=True
[hevpi]
10.42.0.41
IPADDRESS
[local]
localhost
......@@ -14,7 +14,7 @@ hevpi0
hevpi1
[allpi]
10.42.0.41
IPADDRESS
nuliv
hevpi0
hevpi1
[hevpi]
localhost ansible_connection=local ansible_python_interpreter="/usr/bin/env python3"
[local]
localhost
[nuliv]
nuliv
[hevpi0]
hevpi0
[hevpi1]
hevpi1
[allpi]
localhost ansible_connection=local ansible_python_interpreter="/usr/bin/env python3"
nuliv
hevpi0
hevpi1
......@@ -53,14 +53,14 @@
- name: copy platformio script
template: src=platform-io.sh dest={{ download_dir }} owner=pi group=pi mode=0755
- name: copy hev-display script
template: src=hev-display.sh dest={{ download_dir }} owner=pi group=pi mode=0755
#- name: copy hev-display script
#template: src=hev-display.sh dest={{ download_dir }} owner=pi group=pi mode=0755
- name: copy apache script
template: src=setup_apache.sh dest={{ download_dir }} owner=pi group=pi mode=0755
#- name: copy apache script
#template: src=setup_apache.sh dest={{ download_dir }} owner=pi group=pi mode=0755
- name: install hev-display
command: "{{ download_dir }}/hev-display.sh"
#- name: install hev-display
#command: "{{ download_dir }}/hev-display.sh"
- name: update PATH
args:
......@@ -73,14 +73,15 @@
git:
repo: https://ohwr.org/project/hev.git
dest: "{{ ansible_env.HOME }}/hev"
version: "release/ui"
- name: install platformio
command: "{{ download_dir }}/platform-io.sh"
- name: pip install packages
pip:
executable: /usr/bin/pip3
name: "{{ pip_list }}"
#- name: pip install packages
#pip:
#executable: /usr/bin/pip3
#name: "{{ pip_list }}"
- name: pip install packages as root
pip:
......@@ -88,52 +89,71 @@
name: "{{ pip_list }}"
become: yes
- name: check pi version
shell: "pinout | grep 'BCM2711'"
register: pi4
ignore_errors: True
- name: install hev-display-pi4 via apt
apt:
name: hev-display-pi4
state: latest
update_cache: yes
when: pi4 is succeeded
become: yes
- name: install hev-display-pi1 via apt
apt:
name: hev-display-pi1
state: latest
update_cache: yes
when: pi4 is failed
become: yes
- name: install python requirements inside virtual environment
pip:
requirements: "{{ ansible_env.HOME }}/hev/requirements.txt"
virtualenv: "{{ ansible_env.HOME }}/hev/.hev_env"
virtualenv_python: python3.7
- name: link fonts for hev-display
file:
src: "/usr/share/fonts/truetype/dejavu"
dest: "/usr/local/qt5pi/lib/fonts"
state: link
- name: copy PySide2 install into virtualenv
copy:
src: "/usr/lib/python3/dist-packages/PySide2"
dest: "{{ ansible_env.HOME }}/hev/.hev_env/lib/python3.7/site-packages/"
remote_src: yes
become: yes
# - name: install hev-display systemd service
# copy:
# src: "{{ ansible_env.HOME }}/hev/hev-display/extras/systemd/hev-display.service"
# dest: "/usr/lib/systemd/system/hev-display.service"
# remote_src: yes
# become: yes
# - name: copy libscrc script
# template: src=libscrc.sh dest={{ download_dir }} owner=pi group=pi mode=0755
# - name: install libscrc
# command: "{{ download_dir }}/libscrc.sh"
#- name: check pi version
#shell: "pinout | grep 'BCM2711'"
#register: pi4
#ignore_errors: True
#- name: install hev-display-pi4 via apt
#apt:
#name: hev-display-pi4
#state: latest
#update_cache: yes
##when: pi4 is succeeded
#become: yes
#- name: install hev-display-pi1 via apt
#apt:
#name: hev-display-pi1
#state: latest
#update_cache: yes
#when: pi4 is failed
#become: yes
#- name: link fonts for hev-display
#file:
#src: "/usr/share/fonts/truetype/dejavu"
#dest: "/usr/local/qt5pi/lib/fonts"
#state: link
#become: yes
#- name: install hev-display systemd service
#copy:
#src: "{{ ansible_env.HOME }}/hev/hev-display/extras/systemd/hev-display.service"
#dest: "/usr/lib/systemd/system/hev-display.service"
#remote_src: yes
#become: yes
- name: mk user systemd dir
file:
path: "{{ ansible_env.HOME }}/.config/systemd/user/"
state: directory
- name: install hevserver systemd service
copy:
src: "{{ ansible_env.HOME }}/hev/utils/hevserver.service"
dest: "/usr/lib/systemd/system/hevserver.service"
remote_src: yes
become: yes
#- name: install hevserver systemd service
#copy:
#src: "{{ ansible_env.HOME }}/hev/utils/hevserver.service"
#dest: "/usr/lib/systemd/system/hevserver.service"
#remote_src: yes
#become: yes
- name: copy hev rules
copy:
......@@ -145,41 +165,41 @@
mode: 0644
become: yes
- name: enable hev-display systemd service
systemd:
enabled: yes
daemon_reload: yes
name: hev-display
become: yes
- name: enable hevserver systemd service
systemd:
enabled: yes
daemon_reload: yes
name: hevserver
become: yes
- name: link libts
file:
src: "/usr/lib/arm-linux-gnueabihf/libts.so.0"
dest: "/usr/lib/arm-linux-gnueabihf/libts-0.0.so.0"
state: link
become: yes
- name: Adding user pi to www-data
user:
name: pi
groups: www-data
append: yes
become: yes
- name: install hevconf into apache
copy:
src: "{{ ansible_env.HOME }}/hev/raspberry-backend/share/hev.conf"
dest: "/etc/apache2/sites-available/hev.conf"
remote_src: yes
become: yes
- name: setup apache
command: "{{ download_dir }}/setup_apache.sh"
become: yes
#- name: enable hev-display systemd service
#systemd:
#enabled: yes
#daemon_reload: yes
#name: hev-display
#become: yes
#- name: enable hevserver systemd service
#systemd:
#enabled: yes
#daemon_reload: yes
#name: hevserver
#become: yes
#- name: link libts
#file:
#src: "/usr/lib/arm-linux-gnueabihf/libts.so.0"
#dest: "/usr/lib/arm-linux-gnueabihf/libts-0.0.so.0"
#state: link
#become: yes
#- name: Adding user pi to www-data
#user:
#name: pi
#groups: www-data
#append: yes
#become: yes
#- name: install hevconf into apache
#copy:
#src: "{{ ansible_env.HOME }}/hev/raspberry-backend/share/hev.conf"
#dest: "/etc/apache2/sites-available/hev.conf"
#remote_src: yes
#become: yes
#- name: setup apache
#command: "{{ download_dir }}/setup_apache.sh"
#become: yes
---
# © Copyright CERN, Riga Technical University and University of Liverpool 2020.
# All rights not expressly granted are reserved.
#
# This file is part of hev-sw.
#
# hev-sw is free software: you can redistribute it and/or modify it under
# the terms of the GNU General Public Licence as published by the Free
# Software Foundation, either version 3 of the Licence, or (at your option)
# any later version.
#
# hev-sw is distributed in the hope that it will be useful, but WITHOUT
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public Licence
# for more details.
#
# You should have received a copy of the GNU General Public License along
# with hev-sw. If not, see <http://www.gnu.org/licenses/>.
#
# The authors would like to acknowledge the much appreciated support
# of all those involved with the High Energy Ventilator project
# (https://hev.web.cern.ch/).
- hosts: hevpi
remote_user: pi
vars:
download_dir: /home/pi/Downloads
repos:
- hev
tasks:
- name: include vars
include_vars: lists.yml
- name: apt update, apt upgrade
apt:
upgrade: yes
update_cache: yes
become: yes
- name: install software via apt
apt:
name: "{{ rpi_swlist }}"
state: latest
become: yes
- name: mk sw dir
file:
path: /home/pi/sw/bin
state: directory
- name: copy platformio script
template: src=platform-io.sh dest={{ download_dir }} owner=pi group=pi mode=0755
#- name: copy hev-display script
#template: src=hev-display.sh dest={{ download_dir }} owner=pi group=pi mode=0755
#- name: copy apache script
#template: src=setup_apache.sh dest={{ download_dir }} owner=pi group=pi mode=0755
#- name: install hev-display
#command: "{{ download_dir }}/hev-display.sh"
- name: update PATH
args:
chdir: /etc/profile.d
shell: echo "export PATH=${PATH}:{{ ansible_env.HOME }}/.platformio/penv/bin" > env.sh
become: yes
become_user: root
- name: install platformio
command: "{{ download_dir }}/platform-io.sh"
#- name: pip install packages
#pip:
#executable: /usr/bin/pip3
#name: "{{ pip_list }}"
- name: pip install packages as root
pip:
executable: /usr/bin/pip3
name: "{{ pip_list }}"
become: yes
- name: install python requirements inside virtual environment
pip:
requirements: "{{ playbook_dir }}/../../requirements.txt"
virtualenv: "{{ playbook_dir }}/../../.hev_env"
virtualenv_python: python3.7
- name: copy PySide2 install into virtualenv
copy:
src: "/usr/lib/python3/dist-packages/PySide2"
dest: "{{ playbook_dir }}/../../.hev_env/lib/python3.7/site-packages/"
remote_src: yes
become: yes
# - name: copy libscrc script
# template: src=libscrc.sh dest={{ download_dir }} owner=pi group=pi mode=0755
# - name: install libscrc
# command: "{{ download_dir }}/libscrc.sh"
#- name: check pi version
#shell: "pinout | grep 'BCM2711'"
#register: pi4
#ignore_errors: True
#- name: install hev-display-pi4 via apt
#apt:
#name: hev-display-pi4
#state: latest
#update_cache: yes
##when: pi4 is succeeded
#become: yes
#- name: install hev-display-pi1 via apt
#apt:
#name: hev-display-pi1
#state: latest
#update_cache: yes
#when: pi4 is failed
#become: yes
#- name: link fonts for hev-display
#file:
#src: "/usr/share/fonts/truetype/dejavu"
#dest: "/usr/local/qt5pi/lib/fonts"
#state: link
#become: yes
#- name: install hev-display systemd service
#copy:
#src: "{{ playbook_dir }}/../../hev-display/extras/systemd/hev-display.service"
#dest: "/usr/lib/systemd/system/hev-display.service"
#remote_src: yes
#become: yes
- name: mk user systemd dir
file:
path: "{{ ansible_env.HOME }}/.config/systemd/user/"
state: directory
#- name: install hevserver systemd service
#copy:
#src: "{{ playbook_dir }}/../../utils/hevserver.service"
#dest: "/usr/lib/systemd/system/hevserver.service"
#remote_src: yes
#become: yes
- name: copy hev rules
copy:
src: "{{ playbook_dir }}/../../utils/hev.rules"
dest: "/etc/udev/rules.d/88-hev.rules"
remote_src: yes
owner: root
group: root
mode: 0644
become: yes
#- name: enable hev-display systemd service
#systemd:
#enabled: yes
#daemon_reload: yes
#name: hev-display
#become: yes
#- name: enable hevserver systemd service
#systemd:
#enabled: yes
#daemon_reload: yes
#name: hevserver
#become: yes
#- name: link libts
#file:
#src: "/usr/lib/arm-linux-gnueabihf/libts.so.0"
#dest: "/usr/lib/arm-linux-gnueabihf/libts-0.0.so.0"
#state: link
#become: yes
#- name: Adding user pi to www-data
#user:
#name: pi
#groups: www-data
#append: yes
#become: yes
#- name: install hevconf into apache
#copy:
#src: "{{ playbook_dir }}/../../raspberry-backend/share/hev.conf"
#dest: "/etc/apache2/sites-available/hev.conf"
#remote_src: yes
#become: yes
#- name: setup apache
#command: "{{ download_dir }}/setup_apache.sh"
#become: yes
#!/bin/bash
cd Downloads
wget https://github.com/hex-in/libscrc/archive/v1.6.tar.gz
tar xzf v1.6.tar.gz
cd libscrc-1.6/
python3 setup.py build
sudo python3 setup.py install
......@@ -23,13 +23,12 @@ rpi_swlist:
- vim
- emacs
- cowsay
- openssh-server
- minicom
- python3-serial
- python3-flask
- python3
- sqlite3
- fonts-open-sans
- cmake
- libts0
- libglvnd-dev
......@@ -43,16 +42,45 @@ rpi_swlist:
- libjpeg-dev
- libglib2.0-dev
- socat
- emacs
- apache2
- libapache2-mod-wsgi-py3
- python3-pyqt5
- python3-pyqtgraph
- lxappearance
- qml-module-qtquick-virtualkeyboard
- libatlas-base-dev
- python3-pyside2.qt3dcore
- python3-pyside2.qt3dinput
- python3-pyside2.qt3dlogic
- python3-pyside2.qt3drender
- python3-pyside2.qtcharts
- python3-pyside2.qtconcurrent
- python3-pyside2.qtcore
- python3-pyside2.qtgui
- python3-pyside2.qthelp
- python3-pyside2.qtlocation
- python3-pyside2.qtmultimedia
- python3-pyside2.qtmultimediawidgets
- python3-pyside2.qtnetwork
- python3-pyside2.qtopengl
- python3-pyside2.qtpositioning
- python3-pyside2.qtprintsupport
- python3-pyside2.qtqml
- python3-pyside2.qtquick
- python3-pyside2.qtquickwidgets
- python3-pyside2.qtscript
- python3-pyside2.qtscripttools
- python3-pyside2.qtsensors
- python3-pyside2.qtsql
- python3-pyside2.qtsvg
- python3-pyside2.qttest
- python3-pyside2.qttexttospeech
- python3-pyside2.qtuitools
- python3-pyside2.qtwebchannel
- python3-pyside2.qtwebsockets
- python3-pyside2.qtwidgets
- python3-pyside2.qtx11extras
- python3-pyside2.qtxml
- python3-pyside2.qtxmlpatterns
- python3-pyside2uic
pip_list:
- libscrc
- pyserial_asyncio
- virtualenv
\ No newline at end of file
......@@ -21,7 +21,7 @@
# of all those involved with the High Energy Ventilator project
# (https://hev.web.cern.ch/).
cd $HOME || exit
curl -fsSL https://raw.githubusercontent.com/platformio/platformio-core-installer/master/get-platformio.py -o get-platformio.py
python3 get-platformio.py
......@@ -29,7 +29,7 @@ PATH=$PATH:$HOME/.platformio/penv/bin
proj=$HOME/blink
mkdir -p $proj
cd $proj
cd $proj || exit
pio project init -b nodemcu-32s
cat << EOF > src/main.cpp
#include <Arduino.h>
......@@ -60,5 +60,5 @@ pio lib --global install 5390 # RingBuffer
pio lib --global install 5418 # uCRC16Lib
pio lib --global install 5574 # INA2xx
pio lib --global install 820 # Adafruit MCP9808
pio run
#pio run
#pio run -t nobuild -t upload
......@@ -71,3 +71,7 @@ const int pwm_chan_inhale = 0;
const int pwm_chan_exhale = 1;
const int pwm_resolution = 16; // 8 bit resolution; up to 16 possible
const int pwm_frequency = 500; // frequency in Hz
const int pwm_chan_debug = 4;
const int pwm_resolution_debug = 8; // 8 bit resolution; up to 16 possible
const int pwm_frequency_debug = 500; // frequency in Hz
......@@ -41,6 +41,10 @@ BreathingLoop::BreathingLoop()
_ventilation_mode = VENTILATION_MODE::PC_AC;
_bl_state = BL_STATES::IDLE;
_bl_laststate = BL_STATES::IDLE;
_fill_state = FILL_STATES::VALVES_CLOSED;
_fill_laststate = FILL_STATES::VALVES_CLOSED;
_running = false;
_reset = false;
_standby = false;
......@@ -112,7 +116,9 @@ BreathingLoop::BreathingLoop()
_o2_valve_frac = 0;
_expected_fiO2 = 0.21;
_new_expected_fiO2 = 0.21;
_o2_frac_pressure = 0.;
_fiO2_est = 0.21;
_time_valve_closure = millis();
}
void BreathingLoop::initTargets()
......@@ -171,11 +177,17 @@ BreathingLoop::~BreathingLoop()
;
}
uint8_t BreathingLoop::getFsmState()
uint8_t BreathingLoop::getBreatheFSMState()
{
return static_cast<uint8_t>(_bl_state);
}
uint8_t BreathingLoop::getFillFSMState()
{
return static_cast<uint8_t>(_fill_state);
}
void BreathingLoop::updateReadings()
{
// calc pressure every 1ms
......@@ -217,23 +229,21 @@ void BreathingLoop::updateReadings()
_readings_avgs.pressure_o2_supply = adcToMillibarFloat((_readings_sums.pressure_o2_supply / _readings_N));
_readings_avgs.pressure_o2_regulated = adcToMillibarFloat((_readings_sums.pressure_o2_regulated / _readings_N));
_readings_avgs.pressure_diff_patient = adcToMillibarDPFloat((_readings_sums.pressure_diff_patient / _readings_N),_calib_avgs.pressure_diff_patient) ;
//_readings_avgs.o2_percent = adcToO2PercentFloat((_readings_sums.o2_percent / _readings_N));
_readings_avgs.o2_percent = adcToO2PercentFloat(static_cast<float>(analogRead(pin_o2_sensor)));
_readings_avgs.o2_percent = adcToO2PercentFloat((_readings_sums.o2_percent / _readings_N));
//_readings_avgs.o2_percent = adcToO2PercentFloat(static_cast<float>(analogRead(pin_o2_sensor)));
#endif
_pid.process_pressure = _readings_avgs.pressure_inhale; // Update the process pressure independent of the system state
// add Oscar code here:
if (getFsmState() == BL_STATES::INHALE){
if (getBreatheFSMState() == BL_STATES::INHALE){
//TODO
doPID();
_valves_controller.setPIDoutput(_pid.valve_duty_cycle);
_valves_controller.setValves(VALVE_STATE::CLOSED, VALVE_STATE::CLOSED, VALVE_STATE::PID, VALVE_STATE::FULLY_CLOSED, VALVE_STATE::CLOSED);
_valves_controller.setBreatheValves(VALVE_STATE::PID, VALVE_STATE::FULLY_CLOSED);
}
runningAvgs();
......@@ -243,7 +253,8 @@ void BreathingLoop::updateReadings()
resetReadingSums();
updateFromTargets();
// logging to cross check fio2 with TestChest
// logMsg(String(_targets_current->fiO2_percent)+","+String( _readings_avgs.o2_percent)+","+String(_fiO2_est * 100.f));
}
}
......@@ -277,16 +288,15 @@ void BreathingLoop::updateCycleReadings()
if (_bl_state == BL_STATES::BUFF_PRE_INHALE){
if(_cycle_done == false){
logMsg("hello\n");
uint32_t tnow = static_cast<uint32_t>(millis());
ledcWrite(pwm_chan_debug, uint8_t(_fiO2_est*122));
_cycle_index = (_cycle_index == CYCLE_AVG_READINGS-1 ) ? 0 : _cycle_index+1;
_cycle_readings.timestamp = tnow;
_cycle_readings.fiO2_percent = _readings_avgs.o2_percent;// FIXME
_cycle_readings.fiO2_percent =_readings_avgs.o2_percent;//
_running_inhale_minute_volume[_cycle_index] = _volume_inhale ;
_running_exhale_minute_volume[_cycle_index] = _volume_exhale ;
//logMsg(" I, E "+String(_volume_inhale)+ " "+String(_volume_exhale));
_total_cycle_duration[_cycle_index] = (
_measured_durations.buff_pre_inhale
+_measured_durations.inhale
......@@ -351,13 +361,11 @@ void BreathingLoop::updateCalculations() {
if (tnow - _calculations_time >= _calculations_timeout) {
_calculations.flow = getFlow(); // TODO: can be run every 1 ms instead of every arduino cycle
//_calculations.flow_calc = calculateFlow(_readings_avgs.timestamp, _readings_avgs.pressure_patient, _readings_avgs.pressure_buffer);
_calculations.flow_calc = calculateFlow(_readings_avgs.timestamp, _readings_avgs.pressure_patient, _readings_avgs.pressure_buffer);
_calculations.volume = getVolume();
_calculations_time = tnow;
}
_calculations.pressure_airway = getAirwayPressure();
// _calculations.flow_calc = calculateFlow(tnow, adcToMillibarFloat(_readings_raw.pressure_patient, _calib_avgs.pressure_patient), adcToMillibarFloat(_readings_raw.pressure_buffer, _calib_avgs.pressure_buffer));
}
void BreathingLoop::setVentilationMode(VENTILATION_MODE mode)
......@@ -440,7 +448,7 @@ void BreathingLoop::resetReadingSums()
}
//This is used to assign the transitions of the fsm
void BreathingLoop::FSM_assignment() {
void BreathingLoop::assignBreatheFSM() {
uint32_t tnow = static_cast<uint32_t>(millis());
if (tnow - _fsm_time >= _fsm_timeout) {
BL_STATES next_state;
......@@ -537,14 +545,14 @@ void BreathingLoop::FSM_assignment() {
}
}
void BreathingLoop::FSM_breathCycle()
void BreathingLoop::doBreatheFSM()
{
//bool en1 = _valves_controller.getValveParams().exhale_trigger_enable;
//bool en2 = _valves_controller.getValveParams().inhale_trigger_enable;
bool mand_ex = false;
bool mand_vol = false;
float o2_frac_pressure = _o2_valve_frac * _targets_current->buffer_upper_pressure;
// float o2_frac_pressure = _o2_valve_frac * _targets_current->buffer_upper_pressure;
// basic cycle for testing hardware
switch (_bl_state) {
......@@ -557,31 +565,31 @@ void BreathingLoop::FSM_breathCycle()
}
#ifdef EXHALE_VALVE_PROPORTIONAL
// proportional valve normally closed
_valves_controller.setValves(VALVE_STATE::CLOSED, VALVE_STATE::CLOSED, VALVE_STATE::FULLY_CLOSED, VALVE_STATE::FULLY_CLOSED, VALVE_STATE::CLOSED);
_valves_controller.setBreatheValves(VALVE_STATE::FULLY_CLOSED, VALVE_STATE::FULLY_CLOSED);
#else
// digital valve normally open
_valves_controller.setValves(VALVE_STATE::CLOSED, VALVE_STATE::CLOSED, VALVE_STATE::FULLY_CLOSED, VALVE_STATE::OPEN, VALVE_STATE::CLOSED);
_valves_controller.setBreatheValves(VALVE_STATE::FULLY_CLOSED, VALVE_STATE::OPEN);
#endif
initCalib();
break;
case BL_STATES::PRE_CALIBRATION :
_valves_controller.setValves(VALVE_STATE::CLOSED, VALVE_STATE::CLOSED, VALVE_STATE::CLOSED, VALVE_STATE::OPEN, VALVE_STATE::OPEN);
_valves_controller.setBreatheValves(VALVE_STATE::CLOSED, VALVE_STATE::OPEN);
_fsm_timeout = _states_durations.pre_calibration;
break;
case BL_STATES::CALIBRATION :
_valves_controller.setValves(VALVE_STATE::CLOSED, VALVE_STATE::CLOSED, VALVE_STATE::OPEN, VALVE_STATE::OPEN, VALVE_STATE::OPEN);
_valves_controller.setBreatheValves(VALVE_STATE::OPEN, VALVE_STATE::OPEN);
calibrate();
_fsm_timeout = _states_durations.calibration;
break;
case BL_STATES::BUFF_PREFILL:
// TODO - exhale settable; timeout expert settable
_calibrated = true;
_valves_controller.setValves(VALVE_STATE::CLOSED, VALVE_STATE::CLOSED, VALVE_STATE::CLOSED, VALVE_STATE::OPEN, VALVE_STATE::CLOSED);
_valves_controller.setBreatheValves(VALVE_STATE::CLOSED, VALVE_STATE::OPEN);
_fsm_timeout = _states_durations.buff_prefill;
break;
case BL_STATES::BUFF_FILL:
// TODO - exhale settable; timeout settable
_valves_controller.setValves(VALVE_STATE::OPEN, VALVE_STATE::OPEN, VALVE_STATE::CLOSED, VALVE_STATE::OPEN, VALVE_STATE::CLOSED);
_valves_controller.setBreatheValves(VALVE_STATE::CLOSED, VALVE_STATE::OPEN);
_fsm_timeout = _states_durations.buff_fill;
break;
case BL_STATES::BUFF_PRE_INHALE:
......@@ -592,7 +600,7 @@ void BreathingLoop::FSM_breathCycle()
// P_patient and p_diff_patient
// P_patient or p_diff_patient
// with thresholds on each
_valves_controller.setValves(VALVE_STATE::CLOSED, VALVE_STATE::CLOSED, VALVE_STATE::CLOSED, VALVE_STATE::CLOSED, VALVE_STATE::CLOSED);
_valves_controller.setBreatheValves(VALVE_STATE::CLOSED, VALVE_STATE::CLOSED);
switch (_ventilation_mode)
{
case FLUSH:
......@@ -616,7 +624,7 @@ void BreathingLoop::FSM_breathCycle()
break;
case BL_STATES::INHALE:
//_valves_controller.setValves(VALVE_STATE::CLOSED, VALVE_STATE::CLOSED, VALVE_STATE::OPEN, VALVE_STATE::CLOSED, VALVE_STATE::CLOSED);//Comment this line for the PID control during inhale
// The inhale valve will be opened in the updateReadings function (PID)
_fsm_timeout = _states_durations.inhale;
_inhale_triggered = false; // reset inhale trigger
......@@ -626,17 +634,15 @@ void BreathingLoop::FSM_breathCycle()
mand_ex = exhaleTrigger();
// mand_vol = volumeTrigger(); // disable for now
_mandatory_exhale = mand_ex ; // & mand_vol;
//logMsg("inhale "+String(_valley_flow_time)+" "+_measured_durations.inhale+ " "+_measured_durations.exhale+" " +_fsm_timeout);
break;
case BL_STATES::PAUSE:
_valves_controller.setValves(VALVE_STATE::CLOSED, VALVE_STATE::CLOSED, VALVE_STATE::CLOSED, VALVE_STATE::CLOSED, VALVE_STATE::CLOSED);
_valves_controller.setBreatheValves(VALVE_STATE::CLOSED, VALVE_STATE::CLOSED);
_fsm_timeout = _states_durations.pause;
_valley_flow = 100000; // reset valley after exhale
_valley_flow_time = millis(); // reset valley after exhale
//logMsg("pause "+String(_valley_flow_time)+" "+_measured_durations.inhale+ " "+_measured_durations.exhale+" " +_fsm_timeout);
_states_durations.exhale = calculateDurationExhale();
//logMsg("pause x "+String(_valley_flow_time)+" "+_measured_durations.inhale+ " "+_measured_durations.exhale+" " +_fsm_timeout +" "+_states_durations.exhale);
doO2ValveFrac(_targets_current->fiO2_percent, _targets_current->buffer_upper_pressure);
// doO2ValveFrac(_targets_current->fiO2_percent, _targets_current->buffer_upper_pressure);
break;
case BL_STATES::EXHALE:
_peak_flow = -100000; // reset peak after inhale
......@@ -644,23 +650,8 @@ void BreathingLoop::FSM_breathCycle()
_fsm_timeout = _states_durations.exhale;
_expected_fiO2 = _new_expected_fiO2;
_valves_controller.setBreatheValves(VALVE_STATE::CLOSED, VALVE_STATE::OPEN);
if(doExhalePurge()){
_valves_controller.setValves(VALVE_STATE::CLOSED, VALVE_STATE::CLOSED, VALVE_STATE::CLOSED, VALVE_STATE::OPEN, VALVE_STATE::OPEN);
// fill buffer to required pressure or timeout ; close valves 10ms before timeout.
} else if((_readings_avgs.pressure_buffer >= _targets_current->buffer_upper_pressure) || (millis() - _fsm_time >= (_fsm_timeout - 10))){
_valves_controller.setValves(VALVE_STATE::CLOSED, VALVE_STATE::CLOSED, VALVE_STATE::CLOSED, VALVE_STATE::OPEN, VALVE_STATE::CLOSED);
} else if(_readings_avgs.pressure_buffer < _targets_current->buffer_lower_pressure){
if(_readings_avgs.pressure_buffer <= o2_frac_pressure ) {
// fill O2
_valves_controller.setValves(VALVE_STATE::CLOSED, VALVE_STATE::OPEN, VALVE_STATE::CLOSED, VALVE_STATE::OPEN, VALVE_STATE::CLOSED);
} else {
// fill AIR
_valves_controller.setValves(VALVE_STATE::OPEN, VALVE_STATE::CLOSED, VALVE_STATE::CLOSED, VALVE_STATE::OPEN, VALVE_STATE::CLOSED);
}
}
//logMsg("exhale "+String(_peak_flow_time)+" "+_measured_durations.inhale+ " "+_measured_durations.exhale+" " +_fsm_timeout);
measurePEEP();
digitalWrite(pin_led_red, LOW);
_mandatory_inhale = inhaleTrigger();
......@@ -668,31 +659,127 @@ void BreathingLoop::FSM_breathCycle()
_pressure_patient_fitter.resetCalculation(_peak_flow_time);
break;
case BL_STATES::STANDBY:
_valves_controller.setValves(VALVE_STATE::CLOSED, VALVE_STATE::CLOSED, VALVE_STATE::CLOSED, VALVE_STATE::OPEN, VALVE_STATE::CLOSED);
_valves_controller.setBreatheValves(VALVE_STATE::CLOSED, VALVE_STATE::OPEN);
_fsm_timeout = 1000;
break;
case BL_STATES::BUFF_PURGE:
_valves_controller.setValves(VALVE_STATE::CLOSED, VALVE_STATE::CLOSED, VALVE_STATE::CLOSED, VALVE_STATE::OPEN, VALVE_STATE::OPEN);
_valves_controller.setBreatheValves(VALVE_STATE::CLOSED, VALVE_STATE::OPEN);
_fsm_timeout = _states_durations.buff_purge;
break;
case BL_STATES::BUFF_FLUSH:
_valves_controller.setValves(VALVE_STATE::CLOSED, VALVE_STATE::CLOSED, VALVE_STATE::FULLY_OPEN, VALVE_STATE::OPEN, VALVE_STATE::CLOSED);
_valves_controller.setBreatheValves(VALVE_STATE::FULLY_OPEN, VALVE_STATE::OPEN);
_fsm_timeout = _states_durations.buff_flush;
break;
case BL_STATES::STOP:
// TODO : require a reset command to go back to idle
_valves_controller.setValves(VALVE_STATE::CLOSED, VALVE_STATE::CLOSED, VALVE_STATE::FULLY_CLOSED, VALVE_STATE::OPEN, VALVE_STATE::CLOSED);
_valves_controller.setBreatheValves(VALVE_STATE::FULLY_CLOSED, VALVE_STATE::OPEN);
_fsm_timeout = 1000;
break;
default:
// TODO - shouldn't get here: raise alarm
break;
}
//logMsg("fsm timeout " + String(_fsm_timeout) + " state "+String(_bl_state));;
safetyCheck();
assignFillFSM();
measureDurations();
}
void BreathingLoop::assignFillFSM()
{
switch(_bl_state){
case BL_STATES::EXHALE:
if (_bl_laststate != BL_STATES::EXHALE){
_fill_state = determineFillMode();
logMsg("Fill state: " + String(_fill_state));
}
break;
case BL_STATES::BUFF_FILL:
_fill_state = FILL_STATES::AIR_FILL;
break;
case BL_STATES::PRE_CALIBRATION:
_fill_state = FILL_STATES::PURGE;
break;
case BL_STATES::CALIBRATION:
_fill_state = FILL_STATES::PURGE;
break;
case BL_STATES::BUFF_PURGE:
_fill_state = FILL_STATES::PURGE;
break;
default:
_fill_state = FILL_STATES::VALVES_CLOSED;
}
}
void BreathingLoop::doFillFSM()
{
switch (_fill_state) {
case FILL_STATES::VALVES_CLOSED:
_valves_controller.setFillValves(VALVE_STATE::CLOSED, VALVE_STATE::CLOSED, VALVE_STATE::CLOSED);
break;
case FILL_STATES::AIR_FILL:
// The second condition is probably not needed, since if it is true _bl_state would already have changed. TODO if it is the case
if((_readings_avgs.pressure_buffer >= _targets_current->buffer_upper_pressure) || (millis() - _fsm_time >= (_fsm_timeout - 10))){
_valves_controller.setFillValves(VALVE_STATE::CLOSED, VALVE_STATE::CLOSED, VALVE_STATE::CLOSED);
} else if(_readings_avgs.pressure_buffer < _targets_current->buffer_lower_pressure){
// fill AIR
_valves_controller.setFillValves(VALVE_STATE::OPEN, VALVE_STATE::CLOSED, VALVE_STATE::CLOSED);
}
break;
case FILL_STATES::PURGE:
//purge
_valves_controller.setFillValves(VALVE_STATE::CLOSED, VALVE_STATE::CLOSED, VALVE_STATE::OPEN);
break;
case FILL_STATES::INCREASE_O2:
if(_readings_avgs.pressure_buffer >= _targets_current->buffer_upper_pressure){
_valves_controller.setFillValves(VALVE_STATE::CLOSED, VALVE_STATE::CLOSED, VALVE_STATE::CLOSED);
} else if(_readings_avgs.pressure_buffer < _targets_current->buffer_lower_pressure){
if ((_readings_avgs.pressure_buffer > _p_to_purge) && (millis() - _t_start_purge < _t_max_purge)){
// purge
_valves_controller.setFillValves(VALVE_STATE::CLOSED, VALVE_STATE::CLOSED, VALVE_STATE::OPEN);
}else{
_p_to_purge = _targets_current->buffer_upper_pressure; //do this to avoid getting into if clause again
// fill o2
_valves_controller.setFillValves(VALVE_STATE::CLOSED, VALVE_STATE::OPEN, VALVE_STATE::CLOSED);
}
}
break;
case FILL_STATES::DECREASE_O2:
if(_readings_avgs.pressure_buffer >= _targets_current->buffer_upper_pressure){
_valves_controller.setFillValves(VALVE_STATE::CLOSED, VALVE_STATE::CLOSED, VALVE_STATE::CLOSED);
} else if(_readings_avgs.pressure_buffer < _targets_current->buffer_lower_pressure){
if ((_readings_avgs.pressure_buffer > _p_to_purge) && (millis() - _t_start_purge < _t_max_purge)){
// purge
_valves_controller.setFillValves(VALVE_STATE::CLOSED, VALVE_STATE::CLOSED, VALVE_STATE::OPEN);
}else{
_p_to_purge = _targets_current->buffer_upper_pressure; //do this to avoid getting into if clause again
// fill air
_valves_controller.setFillValves(VALVE_STATE::OPEN, VALVE_STATE::CLOSED, VALVE_STATE::CLOSED);
}
}
break;
case FILL_STATES::MAINTAIN_O2:
if(_readings_avgs.pressure_buffer >= _targets_current->buffer_upper_pressure){
_valves_controller.setFillValves(VALVE_STATE::CLOSED, VALVE_STATE::CLOSED, VALVE_STATE::CLOSED);
_finished_filling = true;
} else if(_readings_avgs.pressure_buffer < _targets_current->buffer_lower_pressure || !_finished_filling){
if (_readings_avgs.pressure_buffer < _o2_frac_pressure){
// fill O2
_valves_controller.setFillValves(VALVE_STATE::CLOSED, VALVE_STATE::OPEN, VALVE_STATE::CLOSED);
}else if(_valves_controller.getO2Valve()){
// close O2 valve before opening air valve
_valves_controller.setFillValves(VALVE_STATE::CLOSED, VALVE_STATE::CLOSED, VALVE_STATE::CLOSED);
}else if (millis() - _time_valve_closure > 20){
// fill air
_valves_controller.setFillValves(VALVE_STATE::OPEN, VALVE_STATE::CLOSED, VALVE_STATE::CLOSED);
}
}
break;
default:
// TODO - shouldn't get here: raise alarm
break;
}
}
void BreathingLoop::measureDurations( ) {
if (_bl_state != _bl_laststate) {
uint32_t tnow = static_cast<uint32_t>(millis());
......@@ -794,6 +881,7 @@ bool BreathingLoop::getRunning()
void BreathingLoop::calibrate()
{
_fiO2_est = 0.21; // reset (as of now, always start with pure air in buffer)
// get pressure_air_regulated over last sec of 10s calc mean
uint32_t tnow = static_cast<uint32_t>(millis());
if (tnow - _calib_time >= _calib_timeout ) {
......@@ -959,16 +1047,18 @@ float BreathingLoop::calculateFlow(const uint32_t &current_time, const float &pr
float BreathingLoop::getFlow(){
const float temperature = 298.0;
const float pressure = 1030.0;
const float calibration_factor=1.40 ;// adjusted to make VTE=VTI
float l2nl = (temperature *1013.25)/(pressure * 273.15 ) ;
float dp_raw = _readings_avgs.pressure_diff_patient;
float flowtmp;
float fudge_factor1 = 1.15; // we scale to test chest
flowtmp = pow(dp_raw,3)*0.1512-3.3422*pow(dp_raw,2)+dp_raw*41.657; // this is in slm (standard liter per minute)
_flow = flowtmp * l2nl; // now expressed in l/min
_flow = calibration_factor * flowtmp * l2nl; // now expressed in l/min
if (_calibrated == true){
return _flow;
......@@ -1019,12 +1109,8 @@ float BreathingLoop::getVolume()
float flow = _calculations.flow; //getFlow(); // flow is now in l/min
float vol = flow*1000.0 / (60 * 100) ; //1000 l-> ml; 60s ; 100 = 10ms measure -> 1s
float fudge_factor3 = 0.75; // based on test chest flow to vol measurements
_volume += vol * nl2l; // real KH
//_volume += fudge_factor3 * vol * nl2l; // real KH
//_volume = _flow_fitter.extrapolate(millis()); // will return correct extrapolation of max float value
//_volume += vol //* nl2l /100;
if (_calibrated == true){
return _volume;
......@@ -1077,7 +1163,7 @@ void BreathingLoop::doPID(){
//Checking minium and maximum duty cycle
float minimum_open_frac = 0.53; //Minimum opening to avoid vibrations on the valve control
float minimum_open_frac = 0.52; //Minimum opening to avoid vibrations on the valve control
float maximum_open_frac = 0.74; //Maximum opening for the PID control
//_pid.valve_duty_cycle = _pid.proportional + _pid.integral + (_targets_current->pid_gain * _pid.Kd * _pid.derivative) + minimum_open_frac;
......@@ -1281,30 +1367,30 @@ void BreathingLoop::tsigReset()
void BreathingLoop::doO2ValveFrac(float desired_fiO2, float pressure_change)
{
// this assumes o2 and air valves fill at same rates, AND that we fill sequentially - o2, then air.
// void BreathingLoop::doO2ValveFrac(float desired_fiO2, float pressure_change)
// {
// // this assumes o2 and air valves fill at same rates, AND that we fill sequentially - o2, then air.
// pressure_change is proportional to the amount of volume exhanged in a cycle
// I think this equals tidal inhale volume
// // pressure_change is proportional to the amount of volume exhanged in a cycle
// // I think this equals tidal inhale volume
// we calculate amount of o2 : air to input
// if we have to fill 300 mbar, we fill up to (o2_frac * 300) mbar of o2, then (1-o2_frac)*300 mbar air
// // we calculate amount of o2 : air to input
// // if we have to fill 300 mbar, we fill up to (o2_frac * 300) mbar of o2, then (1-o2_frac)*300 mbar air
// we estimate the current o2 fraction, rather than wait for o2 sensor
// we should have an alarm if o2 sensor and o2 expectation is wrong after 1 min
// // we estimate the current o2 fraction, rather than wait for o2 sensor
// // we should have an alarm if o2 sensor and o2 expectation is wrong after 1 min
float o2change = (desired_fiO2*(1000+pressure_change) - _expected_fiO2 *1000 -0.21*pressure_change)/(0.79*pressure_change);
if (o2change > 1){
o2change = 1;
} else if (o2change < 0){
o2change = 0;
}
_new_expected_fiO2 = _expected_fiO2*1000 + o2change*pressure_change + 0.21*(1-o2change)*pressure_change;
_o2_valve_frac = o2change;
// float o2change = (desired_fiO2*(1000+pressure_change) - _expected_fiO2 *1000 -0.21*pressure_change)/(0.79*pressure_change);
// if (o2change > 1){
// o2change = 1;
// } else if (o2change < 0){
// o2change = 0;
// }
// _new_expected_fiO2 = _expected_fiO2*1000 + o2change*pressure_change + 0.21*(1-o2change)*pressure_change;
// _o2_valve_frac = o2change;
// airValveFrac = 1.0 - O2ValveFrac
}
// // airValveFrac = 1.0 - O2ValveFrac
// }
bool BreathingLoop::doExhalePurge()
{
......@@ -1320,3 +1406,110 @@ bool BreathingLoop::doExhalePurge()
}
return false;
}
void BreathingLoop::updateO2Concentration()
{
// Check for valves state
bool vin_air, vin_o2, vpurge;
uint8_t vinhale, vexhale;
float p_atm = 1013.15; // [mbar]
float p_buff_now = _readings_avgs.pressure_buffer + p_atm;
float delta_p = p_buff_now - _pressure_before_filling;
_valves_controller.getValves(vin_air, vin_o2, vinhale, vexhale, vpurge); // TODO create function to only read air_in and O2_in
// check if state changed
if(vin_air == _valve_air_last_state && vin_o2 == _valve_O2_last_state)
{
// states did not change -> do nothing
}else if (vin_air != _valve_air_last_state && vin_o2 != _valve_O2_last_state)
{
logMsg("Both valves changed state");
// should not happen - raise alarm. For now do nothing
}else if (vin_o2 != _valve_O2_last_state){
if(vin_o2){
// O2 valve from closed->open
_pressure_before_filling = p_buff_now;
}else{
// O2 valve from open->closed
_time_valve_closure = millis();
if (delta_p < 0){
_fill_state = FILL_STATES::AIR_FILL; // safety precaution: when during O2 filling not enough pressure is provided, fill with air
}else{
// _fiO2_est = (delta_p + _fiO2_est * _pressure_before_filling) / p_buff_now; //old
_fiO2_est = _fiO2_est + (1 - _fiO2_est) * (delta_p / p_buff_now);
}
}
}else if (vin_air != _valve_air_last_state){
if(vin_air){
// air valve from closed->open
_pressure_before_filling = p_buff_now;
}else{
// air valve from open->closed
_time_valve_closure = millis();
// TODO check this calculation
if (delta_p < 0){
_fill_state = FILL_STATES::AIR_FILL;
}else{
// _fiO2_est = 1 - (delta_p * 0.79 +(1 - _fiO2_est) * _pressure_before_filling) / p_buff_now;
_fiO2_est = 0.21 + (_fiO2_est - 0.21) * (p_buff_now - delta_p) / p_buff_now;
}
}
}
_valve_O2_last_state = vin_o2;
_valve_air_last_state = vin_air;
if(_fiO2_est <= 0.21)
{
_fiO2_est = 0.211; //NEVER equal to 0.21 since a division by 0 could occur
}else if(_fiO2_est >=1){
// something went wrong, raise error
_fiO2_est = 0.99; //NEVER equal to 1 since a division by 0 could occur
}
// _readings_avgs.o2_percent = _fiO2_est * 100;
}
uint8_t BreathingLoop::determineFillMode()
{
/*
* The derivation of this control can be found in oxygen_control_analytics.pdf
* TODO find atmospheric pressure during calibration
*
*/
float p_buff_now = _readings_avgs.pressure_buffer;
float p_buff_upper = _targets_current->buffer_upper_pressure;
float fiO2_desired = _targets_current->fiO2_percent / 100;
float p_atm = 997; // [mbar]
float p_max_purge = 150;
float delta_fiO2 = fiO2_desired - _fiO2_est;
uint8_t next_fill_state;
if (fiO2_desired < 0.22 && delta_fiO2 > -0.1){
// If fiO2_est is below 32% fill with air without purging
next_fill_state = FILL_STATES::AIR_FILL;
}else if (delta_fiO2 > 0.){
float dp_purge = (p_atm + p_buff_upper) * delta_fiO2 / (1 - _fiO2_est);
if (dp_purge < (p_buff_upper - p_max_purge)){
_p_to_purge = p_buff_upper - dp_purge;
}else{
_p_to_purge = p_max_purge;
}
_t_max_purge = 0.5 * _states_durations.exhale;
_t_start_purge = millis();
next_fill_state = FILL_STATES::INCREASE_O2;
}else if (delta_fiO2 < -0.05){ // tolerance of 5%
float dp_purge = (p_atm + p_buff_upper) * -1 * delta_fiO2 / (_fiO2_est - .21);
if (dp_purge < (p_buff_upper - p_max_purge)){
_p_to_purge = p_buff_upper - dp_purge;
}else{
_p_to_purge = p_max_purge;
}
_t_max_purge = 0.5 * _states_durations.exhale;
_t_start_purge = millis();
next_fill_state = FILL_STATES::DECREASE_O2;
}else{
_o2_frac_pressure = p_buff_now + (fiO2_desired - 0.21) * (p_buff_upper - p_buff_now) / 0.79;
_finished_filling = false;
next_fill_state = FILL_STATES::MAINTAIN_O2;
}
return next_fill_state;
}
......@@ -42,9 +42,12 @@ class BreathingLoop
public:
BreathingLoop();
~BreathingLoop();
uint8_t getFsmState();
void FSM_assignment();
void FSM_breathCycle();
uint8_t getBreatheFSMState();
void assignBreatheFSM();
void doBreatheFSM();
uint8_t getFillFSMState();
void assignFillFSM();
void doFillFSM();
void doStart();
void doStop();
void doReset();
......@@ -54,6 +57,7 @@ public:
void updateRawReadings();
void updateCycleReadings();
void updateCalculations();
void updateO2Concentration();
readings<float> getReadingAverages();
readings<float> getRawReadings();
calculations<float> getCalculations();
......@@ -72,6 +76,7 @@ public:
uint8_t valvePurgeEnabled();
uint8_t inhaleTriggerEnabled();
uint8_t exhaleTriggerEnabled();
uint8_t determineFillMode();
void setVentilationMode(VENTILATION_MODE mode);
VENTILATION_MODE getVentilationMode();
......@@ -110,6 +115,16 @@ public:
};
// states
enum FILL_STATES : uint8_t {
VALVES_CLOSED = 0,
AIR_FILL = 1,
PURGE = 2,
MAINTAIN_O2 = 3,
INCREASE_O2 = 4,
DECREASE_O2 = 5,
O2_FILL = 6
};
private:
......@@ -118,6 +133,7 @@ private:
uint32_t _fsm_timeout;
VENTILATION_MODE _ventilation_mode;
BL_STATES _bl_state, _bl_laststate;
FILL_STATES _fill_state, _fill_laststate;
uint32_t _lasttime;
bool _running;
......@@ -244,6 +260,16 @@ private:
float _expected_fiO2;
float _new_expected_fiO2;
float _fiO2_est;
float _o2_frac_pressure;
float _p_to_purge;
float _t_max_purge;
float _t_start_purge;
bool _valve_air_last_state;
bool _valve_O2_last_state;
float _pressure_before_filling;
float _time_valve_closure;
float _finished_filling;
};
......
......@@ -107,7 +107,7 @@ void UILoop::reportFastReadings()
//readings<float> readings = _breathing_loop->getRawReadings();
_fast_data.timestamp = static_cast<uint32_t>(readings.timestamp);
_fast_data.fsm_state = _breathing_loop->getFsmState();
_fast_data.fsm_state = _breathing_loop->getBreatheFSMState();
_fast_data.pressure_air_supply = readings.pressure_air_supply;
_fast_data.pressure_air_regulated = readings.pressure_air_regulated;
......@@ -116,8 +116,7 @@ void UILoop::reportFastReadings()
_fast_data.pressure_patient = readings.pressure_patient;
_fast_data.temperature_buffer = readings.temperature_buffer;
_fast_data.pressure_o2_supply = readings.pressure_o2_supply;
//_fast_data.pressure_o2_regulated = readings.pressure_o2_regulated;
_fast_data.pressure_o2_regulated = readings.o2_percent;
_fast_data.pressure_o2_regulated = readings.pressure_o2_regulated;
_fast_data.pressure_diff_patient = readings.pressure_diff_patient;
pid_variables &pid = _breathing_loop->getPIDVariables();
......
......@@ -72,7 +72,7 @@ ValvesController::ValvesController()
_valve_params.inhale_duty_cycle = 0;
_valve_params.inhale_open_max = MAX_VALVE_FRAC_OPEN;
_valve_params.inhale_open_min = 0.51;
_valve_params.inhale_open_min = 0.52;
_valve_params.valve_air_in_enable = 1;
_valve_params.valve_o2_in_enable = 1;
_valve_params.valve_purge_enable = 1;
......@@ -169,44 +169,31 @@ void ValvesController::setInhaleOpenMax(float value)
}
void ValvesController::setValves(bool vin_air, bool vin_o2, uint8_t vinhale,
uint8_t vexhale, bool vpurge)
void ValvesController::setBreatheValves(uint8_t vinhale, uint8_t vexhale)
{
digitalWrite(_air_in.pin, vin_air * _valve_params.valve_air_in_enable);
digitalWrite(_o2_in.pin, vin_o2 * _valve_params.valve_o2_in_enable);
digitalWrite(_purge.pin, vpurge * _valve_params.valve_purge_enable);
// float dc = 0;
switch(vinhale){
case VALVE_STATE::FULLY_CLOSED:
setPWMValve(_inhale.pin, 0.0);
break;
case VALVE_STATE::FULLY_OPEN:
setPWMValve(_inhale.pin, _valve_params.inhale_open_max);
// dc = _valve_params.inhale_open_max;
break;
case VALVE_STATE::OPEN:
setPWMValve(_inhale.pin, _valve_params.inhale_open_max);
// dc = _valve_params.inhale_open_max;
break;
case VALVE_STATE::CALIB_OPEN:
setPWMValve(_inhale.pin, _valve_params.inhale_open_max);
// dc = _valve_params.inhale_open_max;
break;
case VALVE_STATE::CLOSED:
setPWMValve(_inhale.pin, _valve_params.inhale_open_min);
// dc = _valve_params.inhale_open_min;
break;
case VALVE_STATE::PID:
// placeholder - this should be replaced by:
//doPID(_inhale.pin);
setPWMValve(_inhale.pin, _PID_output);//_inhale_open_max);
// dc = _PID_output;
setPWMValve(_inhale.pin, _PID_output);
break;
default:
break;
}
//logMsg(" setValve "+String(vinhale) +" " +String(dc)+" "+String(millis()));
if(_exhale.proportional == true){
switch(vexhale){
case VALVE_STATE::FULLY_CLOSED:
......@@ -236,10 +223,19 @@ void ValvesController::setValves(bool vin_air, bool vin_o2, uint8_t vinhale,
}
// save the state
_air_in.state = vin_air;
_o2_in.state = vin_o2;
_inhale.state = vinhale;
_exhale.state = vexhale;
}
void ValvesController::setFillValves(bool vin_air, bool vin_o2, bool vpurge)
{
digitalWrite(_air_in.pin, vin_air * _valve_params.valve_air_in_enable);
digitalWrite(_o2_in.pin, vin_o2 * _valve_params.valve_o2_in_enable);
digitalWrite(_purge.pin, vpurge * _valve_params.valve_purge_enable);
// save the state
_air_in.state = vin_air;
_o2_in.state = vin_o2;
_purge.state = vpurge;
}
......@@ -254,6 +250,10 @@ void ValvesController::getValves(bool &vin_air, bool &vin_o2, uint8_t &vinhale,
vpurge = _purge.state ;
}
bool ValvesController::getO2Valve()
{
return _o2_in.state;
}
void ValvesController::enableO2InValve(bool en)
{
......
......@@ -60,10 +60,13 @@ public:
~ValvesController();
void setupINA(INA_Class *ina, uint8_t num_devices);
void setPWMValve(int pin, float frac_open);
void setValves(bool vin_air, bool vin_o2, uint8_t vinhale,
uint8_t vexhale, bool vpurge);
void getValves(bool &vin_air, bool &vin_o2, uint8_t &vinhale,
uint8_t &vexhale, bool &vpurge);
void setFillValves(bool vin_air, bool vin_o2, bool vpurge);
void setBreatheValves(uint8_t vinhale, uint8_t vexhale);
uint32_t calcValveDutyCycle(uint32_t pwm_resolution, float frac_open);
valve_params& getValveParams();
......@@ -75,6 +78,7 @@ public:
void setInhaleOpenMin(float value);
void setInhaleOpenMax(float value);
bool getO2Valve();
void updateIV(valve &v);
void updateAllIV();
IV_readings<float>* getIVReadings();
......
......@@ -222,7 +222,7 @@ float adcToMillibarDPFloat(float adc, float offset)
// https://docs.rs-online.com/7d77/0900766b81568899.pdf
float PCB_Gain = 2.; // real voltage is two times higher thant the measured in the PCB (there is a voltage divider)
float ADC_to_mVoltage_Gain = 0.788; // this is the measured gain
float ADC_to_mVoltage_Gain = 0.768; // this is the measured gain
float ADC_offset = 162.; // this is the measured offset
float Aout = PCB_Gain * (ADC_to_mVoltage_Gain * adc + ADC_offset) ;
float Vdd = 5000;
......@@ -240,12 +240,13 @@ float adcToO2PercentFloat(float adc, float offset)
{
// calibration should flush air only (21% o2) or o2 only (100%) for N secs
float PCB_Gain = 4. ; // real voltage is 4 times higher thant the measured in the PCB (there is a voltage divider)
float PCB_Gain = 5.54 ; // real voltage is 4 times higher thant the measured in the PCB (there is a voltage divider)
float Sensor_Gain = 100./10000. ; // the sensor gain is 100 % / 10000 mVolts
float ADC_to_Voltage_Gain = 3300./4096.0 ; // maximum Voltage of 3.3V for 4096 ADC counts - (It might need recalibration?)
float ADC_to_mVoltage_Gain = 0.788; // this is the measured gain
float ADC_offset = 162.; // this is the measured offset
float o2pc = PCB_Gain * Sensor_Gain * ADC_to_Voltage_Gain * (adc - offset);
float o2pc = PCB_Gain * Sensor_Gain * (ADC_to_mVoltage_Gain * adc);
return o2pc;
}
......
......@@ -103,6 +103,10 @@ void setup()
pinMode(pin_led_yellow, OUTPUT);
pinMode(pin_led_red, OUTPUT);
// use channel for 4 of PWM generator to output calculated FiO2
ledcSetup(pwm_chan_debug, pwm_frequency_debug, pwm_resolution_debug); // channel 4, Frequency 500, 8bit resolution
ledcAttachPin(pin_spare_2, pwm_chan_debug);
//pinMode(pin_buzzer, OUTPUT);
comms.beginSerial();
......@@ -137,8 +141,9 @@ void setup()
void loop()
{
breathing_loop.FSM_assignment();
breathing_loop.FSM_breathCycle();
breathing_loop.assignBreatheFSM();
breathing_loop.doBreatheFSM();
breathing_loop.doFillFSM(); // assignFillFSM() is in doBreatheFSM()
alarm_loop.fireAlarms();
......@@ -162,6 +167,7 @@ void loop()
breathing_loop.updateRawReadings();
breathing_loop.updateCycleReadings();
breathing_loop.updateCalculations();
breathing_loop.updateO2Concentration();
// update alarm values
// TODO assign more values
alarm_loop.updateValues(breathing_loop.getReadingAverages(), breathing_loop.getCycleReadings());
......
[MASTER]
# A comma-separated list of package or module names from where C extensions may
# be loaded. Extensions are loading into the active Python interpreter and may
# run arbitrary code.
extension-pkg-whitelist=PySide2, PyQt5
# Specify a score threshold to be exceeded before program exits with error.
fail-under=5.0
# Add files or directories to the blacklist. They should be base names, not
# paths.
ignore=CVS, .cache, .vscode, ansible, arduino, battery, BM_SCRATCH, env, raspberry-backend, raspberry-dataserver
# Add files or directories matching the regex patterns to the blacklist. The
# regex matches against base names, not paths.
ignore-patterns=
# Python code to execute, usually for sys.path manipulation such as
# pygtk.require().
#init-hook=
# Use multiple processes to speed up Pylint. Specifying 0 will auto-detect the
# number of processors available to use.
jobs=0
# Control the amount of potential inferred values when inferring a single
# object. This can help the performance when dealing with large functions or
# complex, nested conditions.
limit-inference-results=100
# List of plugins (as comma separated values of python module names) to load,
# usually to register additional checkers.
load-plugins=
# Pickle collected data for later comparisons.
persistent=yes
# When enabled, pylint would attempt to guess common misconfiguration and emit
# user-friendly hints instead of false-positive error messages.
suggestion-mode=yes
# Allow loading of arbitrary C extensions. Extensions are imported into the
# active Python interpreter and may run arbitrary code.
unsafe-load-any-extension=no
[MESSAGES CONTROL]
# Only show warnings with the listed confidence levels. Leave empty to show
# all. Valid levels: HIGH, INFERENCE, INFERENCE_FAILURE, UNDEFINED.
confidence=
# Disable the message, report, category or checker with the given id(s). You
# can either give multiple identifiers separated by comma (,) or put this
# option multiple times (only on the command line, not in the configuration
# file where it should appear only once). You can also use "--disable=all" to
# disable everything first and then reenable specific checks. For example, if
# you want to run only the similarities checker, you can use "--disable=all
# --enable=similarities". If you want to run only the classes checker, but have
# no Warning level messages displayed, use "--disable=all --enable=classes
# --disable=W".
disable=print-statement,
parameter-unpacking,
unpacking-in-except,
old-raise-syntax,
backtick,
long-suffix,
old-ne-operator,
old-octal-literal,
import-star-module-level,
non-ascii-bytes-literal,
raw-checker-failed,
bad-inline-option,
locally-disabled,
file-ignored,
suppressed-message,
useless-suppression,
deprecated-pragma,
use-symbolic-message-instead,
apply-builtin,
basestring-builtin,
buffer-builtin,
cmp-builtin,
coerce-builtin,
execfile-builtin,
file-builtin,
long-builtin,
raw_input-builtin,
reduce-builtin,
standarderror-builtin,
unicode-builtin,
xrange-builtin,
coerce-method,
delslice-method,
getslice-method,
setslice-method,
no-absolute-import,
old-division,
dict-iter-method,
dict-view-method,
next-method-called,
metaclass-assignment,
indexing-exception,
raising-string,
reload-builtin,
oct-method,
hex-method,
nonzero-method,
cmp-method,
input-builtin,
round-builtin,
intern-builtin,
unichr-builtin,
map-builtin-not-iterating,
zip-builtin-not-iterating,
range-builtin-not-iterating,
filter-builtin-not-iterating,
using-cmp-argument,
eq-without-hash,
div-method,
idiv-method,
rdiv-method,
exception-message-attribute,
invalid-str-codec,
sys-max-int,
bad-python3-import,
deprecated-string-function,
deprecated-str-translate-call,
deprecated-itertools-function,
deprecated-types-field,
next-method-defined,
dict-items-not-iterating,
dict-keys-not-iterating,
dict-values-not-iterating,
deprecated-operator-function,
deprecated-urllib-function,
xreadlines-attribute,
deprecated-sys-function,
exception-escape,
comprehension-escape
# Enable the message, report, category or checker with the given id(s). You can
# either give multiple identifier separated by comma (,) or put this option
# multiple time (only on the command line, not in the configuration file where
# it should appear only once). See also the "--disable" option for examples.
enable=
[REPORTS]
# Python expression which should return a score less than or equal to 10. You
# have access to the variables 'error', 'warning', 'refactor', and 'convention'
# which contain the number of messages in each category, as well as 'statement'
# which is the total number of statements analyzed. This score is used by the
# global evaluation report (RP0004).
evaluation=10.0 - ((float(5 * error + warning + refactor + convention) / statement) * 10)
# Template used to display messages. This is a python new-style format string
# used to format the message information. See doc for all details.
#msg-template=
# Set the output format. Available formats are text, parseable, colorized, json
# and msvs (visual studio). You can also give a reporter class, e.g.
# mypackage.mymodule.MyReporterClass.
output-format=text
# Tells whether to display a full report or only the messages.
reports=no
# Activate the evaluation score.
score=yes
[REFACTORING]
# Maximum number of nested blocks for function / method body
max-nested-blocks=5
# Complete name of functions that never returns. When checking for
# inconsistent-return-statements if a never returning function is called then
# it will be considered as an explicit return statement and no message will be
# printed.
never-returning-functions=sys.exit
[BASIC]
# Naming style matching correct argument names.
argument-naming-style=snake_case
# Regular expression matching correct argument names. Overrides argument-
# naming-style.
#argument-rgx=
# Naming style matching correct attribute names.
attr-naming-style=snake_case
# Regular expression matching correct attribute names. Overrides attr-naming-
# style.
#attr-rgx=
# Bad variable names which should always be refused, separated by a comma.
bad-names=foo,
bar,
baz,
toto,
tutu,
tata
# Bad variable names regexes, separated by a comma. If names match any regex,
# they will always be refused
bad-names-rgxs=
# Naming style matching correct class attribute names.
class-attribute-naming-style=any
# Regular expression matching correct class attribute names. Overrides class-
# attribute-naming-style.
#class-attribute-rgx=
# Naming style matching correct class names.
class-naming-style=PascalCase
# Regular expression matching correct class names. Overrides class-naming-
# style.
#class-rgx=
# Naming style matching correct constant names.
const-naming-style=UPPER_CASE
# Regular expression matching correct constant names. Overrides const-naming-
# style.
#const-rgx=
# Minimum line length for functions/classes that require docstrings, shorter
# ones are exempt.
docstring-min-length=-1
# Naming style matching correct function names.
function-naming-style=snake_case
# Regular expression matching correct function names. Overrides function-
# naming-style.
#function-rgx=
# Good variable names which should always be accepted, separated by a comma.
good-names=i,
j,
k,
ex,
Run,
_
# Good variable names regexes, separated by a comma. If names match any regex,
# they will always be accepted
good-names-rgxs=
# Include a hint for the correct naming format with invalid-name.
include-naming-hint=no
# Naming style matching correct inline iteration names.
inlinevar-naming-style=any
# Regular expression matching correct inline iteration names. Overrides
# inlinevar-naming-style.
#inlinevar-rgx=
# Naming style matching correct method names.
method-naming-style=snake_case
# Regular expression matching correct method names. Overrides method-naming-
# style.
#method-rgx=
# Naming style matching correct module names.
module-naming-style=snake_case
# Regular expression matching correct module names. Overrides module-naming-
# style.
#module-rgx=
# Colon-delimited sets of names that determine each other's naming style when
# the name regexes allow several styles.
name-group=
# Regular expression which should only match function or class names that do
# not require a docstring.
no-docstring-rgx=^_
# List of decorators that produce properties, such as abc.abstractproperty. Add
# to this list to register other decorators that produce valid properties.
# These decorators are taken in consideration only for invalid-name.
property-classes=abc.abstractproperty
# Naming style matching correct variable names.
variable-naming-style=snake_case
# Regular expression matching correct variable names. Overrides variable-
# naming-style.
#variable-rgx=
[FORMAT]
# Expected format of line ending, e.g. empty (any line ending), LF or CRLF.
expected-line-ending-format=
# Regexp for a line that is allowed to be longer than the limit.
ignore-long-lines=^\s*(# )?<?https?://\S+>?$
# Number of spaces of indent required inside a hanging or continued line.
indent-after-paren=4
# String used as indentation unit. This is usually " " (4 spaces) or "\t" (1
# tab).
indent-string=' '
# Maximum number of characters on a single line.
max-line-length=100
# Maximum number of lines in a module.
max-module-lines=1000
# Allow the body of a class to be on the same line as the declaration if body
# contains single statement.
single-line-class-stmt=no
# Allow the body of an if to be on the same line as the test if there is no
# else.
single-line-if-stmt=no
[LOGGING]
# The type of string formatting that logging methods do. `old` means using %
# formatting, `new` is for `{}` formatting.
logging-format-style=old
# Logging modules to check that the string format arguments are in logging
# function parameter format.
logging-modules=logging
[MISCELLANEOUS]
# List of note tags to take in consideration, separated by a comma.
notes=FIXME,
XXX,
TODO
# Regular expression of note tags to take in consideration.
#notes-rgx=
[SIMILARITIES]
# Ignore comments when computing similarities.
ignore-comments=yes
# Ignore docstrings when computing similarities.
ignore-docstrings=yes
# Ignore imports when computing similarities.
ignore-imports=no
# Minimum lines number of a similarity.
min-similarity-lines=4
[SPELLING]
# Limits count of emitted suggestions for spelling mistakes.
max-spelling-suggestions=4
# Spelling dictionary name. Available dictionaries: none. To make it work,
# install the python-enchant package.
spelling-dict=
# List of comma separated words that should not be checked.
spelling-ignore-words=
# A path to a file that contains the private dictionary; one word per line.
spelling-private-dict-file=
# Tells whether to store unknown words to the private dictionary (see the
# --spelling-private-dict-file option) instead of raising a message.
spelling-store-unknown-words=no
[STRING]
# This flag controls whether inconsistent-quotes generates a warning when the
# character used as a quote delimiter is used inconsistently within a module.
check-quote-consistency=no
# This flag controls whether the implicit-str-concat should generate a warning
# on implicit string concatenation in sequences defined over several lines.
check-str-concat-over-line-jumps=no
[TYPECHECK]
# List of decorators that produce context managers, such as
# contextlib.contextmanager. Add to this list to register other decorators that
# produce valid context managers.
contextmanager-decorators=contextlib.contextmanager
# List of members which are set dynamically and missed by pylint inference
# system, and so shouldn't trigger E1101 when accessed. Python regular
# expressions are accepted.
generated-members=
# Tells whether missing members accessed in mixin class should be ignored. A
# mixin class is detected if its name ends with "mixin" (case insensitive).
ignore-mixin-members=yes
# Tells whether to warn about missing members when the owner of the attribute
# is inferred to be None.
ignore-none=yes
# This flag controls whether pylint should warn about no-member and similar
# checks whenever an opaque object is returned when inferring. The inference
# can return multiple potential results while evaluating a Python object, but
# some branches might not be evaluated, which results in partial inference. In
# that case, it might be useful to still emit no-member and other checks for
# the rest of the inferred objects.
ignore-on-opaque-inference=yes
# List of class names for which member attributes should not be checked (useful
# for classes with dynamically set attributes). This supports the use of
# qualified names.
ignored-classes=optparse.Values,thread._local,_thread._local
# List of module names for which member attributes should not be checked
# (useful for modules/projects where namespaces are manipulated during runtime
# and thus existing member attributes cannot be deduced by static analysis). It
# supports qualified module names, as well as Unix pattern matching.
ignored-modules=
# Show a hint with possible names when a member name was not found. The aspect
# of finding the hint is based on edit distance.
missing-member-hint=yes
# The minimum edit distance a name should have in order to be considered a
# similar match for a missing member name.
missing-member-hint-distance=1
# The total number of similar names that should be taken in consideration when
# showing a hint for a missing member.
missing-member-max-choices=1
# List of decorators that change the signature of a decorated function.
signature-mutators=
[VARIABLES]
# List of additional names supposed to be defined in builtins. Remember that
# you should avoid defining new builtins when possible.
additional-builtins=
# Tells whether unused global variables should be treated as a violation.
allow-global-unused-variables=yes
# List of strings which can identify a callback function by name. A callback
# name must start or end with one of those strings.
callbacks=cb_,
_cb
# A regular expression matching the name of dummy variables (i.e. expected to
# not be used).
dummy-variables-rgx=_+$|(_[a-zA-Z0-9_]*[a-zA-Z0-9]+?$)|dummy|^ignored_|^unused_
# Argument names that match this expression will be ignored. Default to name
# with leading underscore.
ignored-argument-names=_.*|^ignored_|^unused_
# Tells whether we should check for unused import in __init__ files.
init-import=no
# List of qualified module names which can have objects that can redefine
# builtins.
redefining-builtins-modules=six.moves,past.builtins,future.builtins,builtins,io
[CLASSES]
# List of method names used to declare (i.e. assign) instance attributes.
defining-attr-methods=__init__,
__new__,
setUp,
__post_init__
# List of member names, which should be excluded from the protected access
# warning.
exclude-protected=_asdict,
_fields,
_replace,
_source,
_make
# List of valid names for the first argument in a class method.
valid-classmethod-first-arg=cls
# List of valid names for the first argument in a metaclass class method.
valid-metaclass-classmethod-first-arg=cls
[DESIGN]
# Maximum number of arguments for function / method.
max-args=5
# Maximum number of attributes for a class (see R0902).
max-attributes=7
# Maximum number of boolean expressions in an if statement (see R0916).
max-bool-expr=5
# Maximum number of branch for function / method body.
max-branches=12
# Maximum number of locals for function / method body.
max-locals=15
# Maximum number of parents for a class (see R0901).
max-parents=7
# Maximum number of public methods for a class (see R0904).
max-public-methods=20
# Maximum number of return / yield for function / method body.
max-returns=6
# Maximum number of statements in function / method body.
max-statements=50
# Minimum number of public methods for a class (see R0903).
min-public-methods=2
[IMPORTS]
# List of modules that can be imported at any level, not just the top level
# one.
allow-any-import-level=
# Allow wildcard imports from modules that define __all__.
allow-wildcard-with-all=no
# Analyse import fallback blocks. This can be used to support both Python 2 and
# 3 compatible code, which means that the block might have code that exists
# only in one or another interpreter, leading to false positives when analysed.
analyse-fallback-blocks=no
# Deprecated modules which should not be used, separated by a comma.
deprecated-modules=optparse,tkinter.tix
# Create a graph of external dependencies in the given file (report RP0402 must
# not be disabled).
ext-import-graph=
# Create a graph of every (i.e. internal and external) dependencies in the
# given file (report RP0402 must not be disabled).
import-graph=
# Create a graph of internal dependencies in the given file (report RP0402 must
# not be disabled).
int-import-graph=
# Force import order to recognize a module as part of the standard
# compatibility libraries.
known-standard-library=
# Force import order to recognize a module as part of a third party library.
known-third-party=enchant
# Couples of modules and preferred modules, separated by a comma.
preferred-modules=
[EXCEPTIONS]
# Exceptions that will emit a warning when being caught. Defaults to
# "BaseException, Exception".
overgeneral-exceptions=BaseException,
Exception
......@@ -202,7 +202,7 @@ class ClientPlots(QtWidgets.QMainWindow):
client = HEVClient(polling=False) # just use hevclient for requests
await asyncio.sleep(2)
# trigger an alarm
await client.send_request("CMD", cmdtype="SET_THRESHOLD_MIN", cmd="APNEA",param=0)
await client._send_request("CMD", cmdtype="SET_THRESHOLD_MIN", cmd="APNEA", param=0)
while True:
await asyncio.sleep(60)
except Exception as e:
......
......@@ -27,8 +27,11 @@ from dataclasses import dataclass, asdict, field, fields
from typing import Any, ClassVar, Dict
import logging
import binascii
logging.basicConfig(level=logging.INFO,
format='%(asctime)s - %(levelname)s - %(message)s')
logging.basicConfig(
level=logging.INFO,
format="%(asctime)s - %(levelname)s - %(message)s (%(filename)s line %(lineno)d: %(module)s.%(funcName)s)",
)
# VERSIONING
# change version in PayloadFormat for all data
......@@ -57,6 +60,12 @@ class CMD_TYPE(Enum):
SET_PERSONAL = 15
GET_THRESHOLD_MIN = 16
GET_THRESHOLD_MAX = 17
TEST_AUDIO_ALARM = 18
SKIP_NEXT_CALIBRATION = 19
DO_CALIBRATION = 20
MUTE_ALARM = 21
RESET_ALARM = 22
@unique
......@@ -67,6 +76,7 @@ class CMD_GENERAL(Enum):
STANDBY = 4
GET_PERSONAL = 5
# Taken from the FSM doc. Correct as of 1400 on 20200417
@unique
class CMD_SET_DURATION(Enum):
......@@ -81,6 +91,7 @@ class CMD_SET_DURATION(Enum):
PAUSE = 9
EXHALE = 10
@unique
class VENTILATION_MODE(Enum):
UNKNOWN = 0
......@@ -93,6 +104,7 @@ class VENTILATION_MODE(Enum):
FLUSH = 7
CURRENT = 100 # not settable
@unique
class CMD_SET_VALVE(Enum):
AIR_IN_ENABLE = 1
......@@ -104,6 +116,7 @@ class CMD_SET_VALVE(Enum):
INHALE_TRIGGER_ENABLE = 7
EXHALE_TRIGGER_ENABLE = 8
@unique
class CMD_SET_PID(Enum):
KP = 1
......@@ -114,6 +127,7 @@ class CMD_SET_PID(Enum):
NSTEPS = 6
MAX_PATIENT_PRESSURE = 7
@unique
class CMD_SET_TARGET(Enum):
INSPIRATORY_PRESSURE = 1
......@@ -125,12 +139,13 @@ class CMD_SET_TARGET(Enum):
INHALE_TIME = 7
INHALE_TRIGGER_THRESHOLD = 8
EXHALE_TRIGGER_THRESHOLD = 9
#PID_GAIN = 10
# PID_GAIN = 10
# for debugging only; not for UIs
INHALE_TRIGGER_ENABLE = 11
EXHALE_TRIGGER_ENABLE = 12
VOLUME_TRIGGER_ENABLE = 13
@unique
# class CMD_SET_PERSONAL(Enum):
# NAME = 1
......@@ -139,12 +154,14 @@ class CMD_SET_TARGET(Enum):
# HEIGHT = 4
# WEIGHT = 5
@unique
class ALARM_TYPE(Enum):
PRIORITY_LOW = 1
PRIORITY_MEDIUM = 2
PRIORITY_HIGH = 3
@unique
class ALARM_CODES(Enum):
UNKNOWN = 0
......@@ -173,6 +190,15 @@ class ALARM_CODES(Enum):
O2_FAIL = 23 # HP
PRESSURE_SENSOR_FAULT = 24 # HP
ARDUINO_FAIL = 25 # HP
ALARM_TEST = 26 # HP
HIGH_VME = 27 # MP
LOW_VME = 28 # MP
HIGH_VMI = 29 # MP
LOW_VMI = 30 # MP
EXTENDED_HIGH_PRESSURE = 31 # HP
ALARMS_COUNT = 32
class CMD_MAP(Enum):
GENERAL = CMD_GENERAL
......@@ -193,6 +219,7 @@ class CMD_MAP(Enum):
GET_TARGETS = VENTILATION_MODE
# SET_PERSONAL = CMD_SET_PERSONAL
@unique
class BL_STATES(Enum):
UNKNOWN = 0
......@@ -210,6 +237,7 @@ class BL_STATES(Enum):
BUFF_FLUSH = 12
STANDBY = 13
@unique
class PAYLOAD_TYPE(IntEnum):
UNSET = 0
......@@ -226,12 +254,17 @@ class PAYLOAD_TYPE(IntEnum):
BATTERY = 11
LOOP_STATUS = 12
PERSONAL = 13
ALARM_MUTE = 14
BAD_THRESHOLD = 15
class HEVVersionError(Exception):
pass
@dataclass
class PayloadFormat():
class PayloadFormat:
# class variables excluded from init args and output dict
_RPI_VERSION: ClassVar[int] = field(default=0xB6, init=False, repr=False)
_dataStruct: ClassVar[Any] = field(default=Struct("<BIB"), init=False, repr=False)
......@@ -249,7 +282,7 @@ class PayloadFormat():
1: DataFormat,
2: ReadbackFormat,
3: CycleFormat,
#4: ThresholdFormat,
# 4: ThresholdFormat,
5: CommandFormat,
6: AlarmFormat,
7: DebugFormat,
......@@ -258,7 +291,7 @@ class PayloadFormat():
10: TargetFormat,
11: BatteryFormat,
12: LoopStatusFormat,
13: PersonalFormat
13: PersonalFormat,
}
ReturnType = DATA_TYPE_TO_CLASS[rec_bytes[5]]
payload_obj = ReturnType()
......@@ -276,24 +309,35 @@ class PayloadFormat():
def toByteArray(self) -> None:
self.version = self._RPI_VERSION
self._byteArray = self._dataStruct.pack(*[
self._byteArray = self._dataStruct.pack(
*[
v.value if isinstance(v, IntEnum) or isinstance(v, Enum) else v
for v in asdict(self).values()
])
]
)
# check for mismatch between pi and microcontroller version
def checkVersion(self):
if self._RPI_VERSION != self.version :
raise HEVVersionError('Version Mismatch', "PI:", self._RPI_VERSION, "uC:", self.version)
if self._RPI_VERSION != self.version:
raise HEVVersionError(
"Version Mismatch", "PI:", self._RPI_VERSION, "uC:", self.version
)
def getSize(self) -> int:
return len(self.byteArray)
def getType(self) -> Any:
return self.payload_type if isinstance(self.payload_type, IntEnum) else PAYLOAD_TYPE(self.payload_type)
return (
self.payload_type
if isinstance(self.payload_type, IntEnum)
else PAYLOAD_TYPE(self.payload_type)
)
def getDict(self) -> Dict:
return {k: v.name if isinstance(v, IntEnum) or isinstance(v, Enum) else v for k, v in asdict(self).items()}
return {
k: v.name if isinstance(v, IntEnum) or isinstance(v, Enum) else v
for k, v in asdict(self).items()
}
# =======================================
......@@ -319,25 +363,24 @@ class DataFormat(PayloadFormat):
ambient_temperature: int = 0
airway_pressure: float = 0.0
flow: float = 0.0
flow_calc : float = 0.0
volume : float = 0.0
target_pressure : float = 0.0 ##
flow_calc: float = 0.0
volume: float = 0.0
target_pressure: float = 0.0 ##
process_pressure: float = 0.0
valve_duty_cycle: float = 0.0
proportional : float = 0.0
integral : float = 0.0 ##
derivative : float = 0.0
proportional: float = 0.0
integral: float = 0.0 ##
derivative: float = 0.0
# for receiving DataFormat from microcontroller
# fill the struct from a byteArray,
def fromByteArray(self, byteArray):
#logging.info(f"bytearray size {len(byteArray)} ")
#logging.info(binascii.hexlify(byteArray))
# logging.info(f"bytearray size {len(byteArray)} ")
# logging.info(binascii.hexlify(byteArray))
tmp_state = 0
tmp_payload_type = 0
(self.version,
(
self.version,
self.timestamp,
tmp_payload_type,
tmp_state,
......@@ -356,12 +399,12 @@ class DataFormat(PayloadFormat):
self.flow,
self.flow_calc,
self.volume,
self.target_pressure ,
self.target_pressure,
self.process_pressure,
self.valve_duty_cycle,
self.proportional ,
self.integral ,
self.derivative
self.proportional,
self.integral,
self.derivative,
) = self._dataStruct.unpack(byteArray)
self.fsm_state = BL_STATES(tmp_state)
......@@ -378,7 +421,6 @@ class ReadbackFormat(PayloadFormat):
_dataStruct = Struct("<BIBHHHHHHHHHHffBBBBBBBBBBBffffffB")
payload_type: PAYLOAD_TYPE = PAYLOAD_TYPE.READBACK
duration_pre_calibration: int = 0
duration_calibration: int = 0
duration_buff_purge: int = 0
......@@ -407,19 +449,20 @@ class ReadbackFormat(PayloadFormat):
peep: float = 0.0
inhale_exhale_ratio: float = 0.0
kp : float = 0.0
ki : float = 0.0
kd : float = 0.0
pid_gain : float = 0.0
max_patient_pressure : int = 0
kp: float = 0.0
ki: float = 0.0
kd: float = 0.0
pid_gain: float = 0.0
max_patient_pressure: int = 0
# for receiving DataFormat from microcontroller
# fill the struct from a byteArray,
def fromByteArray(self, byteArray):
#logging.info(f"bytearray size {len(byteArray)} ")
#logging.info(binascii.hexlify(byteArray))
# logging.info(f"bytearray size {len(byteArray)} ")
# logging.info(binascii.hexlify(byteArray))
tmp_mode = 0
tmp_payload_type = 0
(self.version,
(
self.version,
self.timestamp,
tmp_payload_type,
self.duration_pre_calibration,
......@@ -447,11 +490,11 @@ class ReadbackFormat(PayloadFormat):
self.exhale_trigger_enable,
self.peep,
self.inhale_exhale_ratio,
self.kp ,
self.ki ,
self.kd ,
self.pid_gain ,
self.max_patient_pressure
self.kp,
self.ki,
self.kd,
self.pid_gain,
self.max_patient_pressure,
) = self._dataStruct.unpack(byteArray)
self.checkVersion()
......@@ -490,10 +533,11 @@ class CycleFormat(PayloadFormat):
# for receiving DataFormat from microcontroller
# fill the struct from a byteArray,
def fromByteArray(self, byteArray):
#logging.info(f"bytearray size {len(byteArray)} ")
#logging.info(binascii.hexlify(byteArray))
# logging.info(f"bytearray size {len(byteArray)} ")
# logging.info(binascii.hexlify(byteArray))
tmp_payload_type = 0
(self.version,
(
self.version,
self.timestamp,
tmp_payload_type,
self.respiratory_rate,
......@@ -512,12 +556,14 @@ class CycleFormat(PayloadFormat):
self.fiO2_percent,
self.apnea_index,
self.apnea_time,
self.mandatory_breath) = self._dataStruct.unpack(byteArray)
self.mandatory_breath,
) = self._dataStruct.unpack(byteArray)
self.checkVersion()
self.payload_type = PAYLOAD_TYPE(tmp_payload_type)
self._byteArray = byteArray
# =======================================
# debug data payload; this can change
# =======================================
......@@ -527,38 +573,40 @@ class DebugFormat(PayloadFormat):
_dataStruct = Struct("<BIBfffffffff")
payload_type: PAYLOAD_TYPE = PAYLOAD_TYPE.DEBUG
kp : float = 0.0
ki : float = 0.0
kd : float = 0.0
target_pressure : float = 0.0 ##
kp: float = 0.0
ki: float = 0.0
kd: float = 0.0
target_pressure: float = 0.0 ##
process_pressure: float = 0.0
valve_duty_cycle: float = 0.0
proportional : float = 0.0
integral : float = 0.0 ##
derivative : float = 0.0
proportional: float = 0.0
integral: float = 0.0 ##
derivative: float = 0.0
# for receiving DataFormat from microcontroller
# fill the struct from a byteArray,
def fromByteArray(self, byteArray):
tmp_payload_type = 0
(self.version,
(
self.version,
self.timestamp,
tmp_payload_type,
self.kp ,
self.ki ,
self.kd ,
self.target_pressure ,
self.kp,
self.ki,
self.kd,
self.target_pressure,
self.process_pressure,
self.valve_duty_cycle,
self.proportional ,
self.integral ,
self.derivative
self.proportional,
self.integral,
self.derivative,
) = self._dataStruct.unpack(byteArray)
self.checkVersion()
self.payload_type = PAYLOAD_TYPE(tmp_payload_type)
self._byteArray = byteArray
# =======================================
# debug data payload; this can change
# =======================================
......@@ -568,33 +616,36 @@ class LoopStatusFormat(PayloadFormat):
_dataStruct = Struct("<BIBffIIBBB")
payload_type: PAYLOAD_TYPE = PAYLOAD_TYPE.LOOP_STATUS
duration_loop : float = 0.0
duration_loop_max : float = 0.0
dropped_send : int = 0
dropped_receive : int = 0
buffer_alarm : int = 0
buffer_cmd : int = 0
buffer_data : int = 0
duration_loop: float = 0.0
duration_loop_max: float = 0.0
dropped_send: int = 0
dropped_receive: int = 0
buffer_alarm: int = 0
buffer_cmd: int = 0
buffer_data: int = 0
# for receiving DataFormat from microcontroller
# fill the struct from a byteArray,
def fromByteArray(self, byteArray):
tmp_payload_type = 0
(self.version,
(
self.version,
self.timestamp,
tmp_payload_type,
self.duration_loop ,
self.duration_loop,
self.duration_loop_max,
self.dropped_send ,
self.dropped_receive ,
self.buffer_alarm ,
self.buffer_cmd ,
self.buffer_data ,
self.dropped_send,
self.dropped_receive,
self.buffer_alarm,
self.buffer_cmd,
self.buffer_data,
) = self._dataStruct.unpack(byteArray)
self.checkVersion()
self.payload_type = PAYLOAD_TYPE(tmp_payload_type)
self._byteArray = byteArray
# =======================================
# thresholds eata payload
# =======================================
......@@ -608,53 +659,55 @@ class IVTFormat(PayloadFormat):
_dataStruct = Struct("<BIBffffffffffBBBBBf")
payload_type: PAYLOAD_TYPE = PAYLOAD_TYPE.IVT
inhale_current : float = 0.0
exhale_current : float = 0.0
purge_current : float = 0.0
air_in_current : float = 0.0
o2_in_current : float = 0.0
inhale_voltage : float = 0.0
exhale_voltage : float = 0.0
purge_voltage : float = 0.0
air_in_voltage : float = 0.0
o2_in_voltage : float = 0.0
inhale_i2caddr : int = 0.0
exhale_i2caddr : int = 0.0
purge_i2caddr : int = 0.0
air_in_i2caddr : int = 0.0
o2_in_i2caddr : int = 0.0
system_temp : float = 0.0
inhale_current: float = 0.0
exhale_current: float = 0.0
purge_current: float = 0.0
air_in_current: float = 0.0
o2_in_current: float = 0.0
inhale_voltage: float = 0.0
exhale_voltage: float = 0.0
purge_voltage: float = 0.0
air_in_voltage: float = 0.0
o2_in_voltage: float = 0.0
inhale_i2caddr: int = 0.0
exhale_i2caddr: int = 0.0
purge_i2caddr: int = 0.0
air_in_i2caddr: int = 0.0
o2_in_i2caddr: int = 0.0
system_temp: float = 0.0
# for receiving DataFormat from microcontroller
# fill the struct from a byteArray,
def fromByteArray(self, byteArray):
#logging.info(f"bytearray size {len(byteArray)} ")
#logging.info(binascii.hexlify(byteArray))
# logging.info(f"bytearray size {len(byteArray)} ")
# logging.info(binascii.hexlify(byteArray))
tmp_payload_type = 0
(self.version,
(
self.version,
self.timestamp,
tmp_payload_type,
self.inhale_current ,
self.exhale_current ,
self.purge_current ,
self.air_in_current ,
self.o2_in_current ,
self.inhale_voltage ,
self.exhale_voltage ,
self.purge_voltage ,
self.air_in_voltage ,
self.o2_in_voltage ,
self.inhale_i2caddr ,
self.exhale_i2caddr ,
self.purge_i2caddr ,
self.air_in_i2caddr ,
self.o2_in_i2caddr ,
self.system_temp
self.inhale_current,
self.exhale_current,
self.purge_current,
self.air_in_current,
self.o2_in_current,
self.inhale_voltage,
self.exhale_voltage,
self.purge_voltage,
self.air_in_voltage,
self.o2_in_voltage,
self.inhale_i2caddr,
self.exhale_i2caddr,
self.purge_i2caddr,
self.air_in_i2caddr,
self.o2_in_i2caddr,
self.system_temp,
) = self._dataStruct.unpack(byteArray)
self.checkVersion()
self.payload_type = PAYLOAD_TYPE(tmp_payload_type)
self._byteArray = byteArray
# =======================================
# Target data payload
# =======================================
......@@ -663,49 +716,50 @@ class TargetFormat(PayloadFormat):
_dataStruct = Struct("<BIBBfffffffBBBffff")
payload_type: PAYLOAD_TYPE = PAYLOAD_TYPE.TARGET
mode : int = 0
inspiratory_pressure : float = 0.0
ie_ratio : float = 0.0
volume : float = 0.0
respiratory_rate : float = 0.0
peep : float = 0.0
fiO2_percent : float = 0.0
inhale_time : float = 0
inhale_trigger_enable : int = 0
exhale_trigger_enable : int = 0
volume_trigger_enable : int = 0
inhale_trigger_threshold : float = 0.0
exhale_trigger_threshold : float = 0.0
buffer_upper_pressure : float = 0.0
buffer_lower_pressure : float = 0.0
#pid_gain : float = 0.0
mode: int = 0
inspiratory_pressure: float = 0.0
ie_ratio: float = 0.0
volume: float = 0.0
respiratory_rate: float = 0.0
peep: float = 0.0
fiO2_percent: float = 0.0
inhale_time: float = 0
inhale_trigger_enable: int = 0
exhale_trigger_enable: int = 0
volume_trigger_enable: int = 0
inhale_trigger_threshold: float = 0.0
exhale_trigger_threshold: float = 0.0
buffer_upper_pressure: float = 0.0
buffer_lower_pressure: float = 0.0
# pid_gain : float = 0.0
# for receiving DataFormat from microcontroller
# fill the struct from a byteArray,
def fromByteArray(self, byteArray):
#logging.info(f"bytearray size {len(byteArray)} ")
#logging.info(binascii.hexlify(byteArray))
# logging.info(f"bytearray size {len(byteArray)} ")
# logging.info(binascii.hexlify(byteArray))
tmp_payload_type = 0
tmp_mode = 0
(self.version,
(
self.version,
self.timestamp,
tmp_payload_type,
tmp_mode ,
tmp_mode,
self.inspiratory_pressure,
self.ie_ratio ,
self.volume ,
self.respiratory_rate ,
self.peep ,
self.fiO2_percent ,
self.inhale_time ,
self.inhale_trigger_enable ,
self.exhale_trigger_enable ,
self.volume_trigger_enable ,
self.inhale_trigger_threshold ,
self.exhale_trigger_threshold ,
self.ie_ratio,
self.volume,
self.respiratory_rate,
self.peep,
self.fiO2_percent,
self.inhale_time,
self.inhale_trigger_enable,
self.exhale_trigger_enable,
self.volume_trigger_enable,
self.inhale_trigger_threshold,
self.exhale_trigger_threshold,
self.buffer_upper_pressure,
self.buffer_lower_pressure
#self.pid_gain
# self.pid_gain
) = self._dataStruct.unpack(byteArray)
self.checkVersion()
......@@ -713,6 +767,7 @@ class TargetFormat(PayloadFormat):
self.mode = VENTILATION_MODE(tmp_mode)
self._byteArray = byteArray
# =======================================
# Personal data payload
# =======================================
......@@ -721,23 +776,24 @@ class PersonalFormat(PayloadFormat):
_dataStruct = Struct("<BIB60s20sBcBB")
payload_type: PAYLOAD_TYPE = PAYLOAD_TYPE.PERSONAL
name : str = ""
patient_id : str = ""
age : int = 0
sex : str = ""
height : int = 0
weight : int = 0
name: str = ""
patient_id: str = ""
age: int = 0
sex: str = ""
height: int = 0
weight: int = 0
# for receiving DataFormat from microcontroller
# fill the struct from a byteArray,
def fromByteArray(self, byteArray):
#logging.info(f"bytearray size {len(byteArray)} ")
#logging.info(binascii.hexlify(byteArray))
# logging.info(f"bytearray size {len(byteArray)} ")
# logging.info(binascii.hexlify(byteArray))
tmp_payload_type = 0
tmp_name = None
tmp_id = None
tmp_sex = None
(self.version,
(
self.version,
self.timestamp,
tmp_payload_type,
tmp_name,
......@@ -745,14 +801,17 @@ class PersonalFormat(PayloadFormat):
self.age,
tmp_sex,
self.height,
self.weight) = self._dataStruct.unpack(byteArray)
self.weight,
) = self._dataStruct.unpack(byteArray)
self.checkVersion()
self.payload_type = PAYLOAD_TYPE(tmp_payload_type)
self.name = tmp_name.decode().rstrip('\0')
self.patient_id = tmp_id.decode().rstrip('\0')
self.name = tmp_name.decode().rstrip("\0")
self.patient_id = tmp_id.decode().rstrip("\0")
self.sex = tmp_sex.decode()
self._byteArray = byteArray
# =======================================
# Log msg payload
# =======================================
......@@ -761,24 +820,27 @@ class LogMsgFormat(PayloadFormat):
_dataStruct = Struct("<BIB50s")
payload_type: PAYLOAD_TYPE = PAYLOAD_TYPE.LOGMSG
message : str = ""
message: str = ""
# for receiving DataFormat from microcontroller
# fill the struct from a byteArray,
def fromByteArray(self, byteArray):
#logging.info(f"bytearray size {len(byteArray)} ")
#logging.info(binascii.hexlify(byteArray))
# logging.info(f"bytearray size {len(byteArray)} ")
# logging.info(binascii.hexlify(byteArray))
tmp_payload_type = 0
tmp_chararray = ""
(self.version,
(
self.version,
self.timestamp,
tmp_payload_type,
tmp_chararray) = self._dataStruct.unpack(byteArray)
tmp_chararray,
) = self._dataStruct.unpack(byteArray)
self.checkVersion()
self.message = tmp_chararray.decode('ascii')
self.message = tmp_chararray.decode("ascii")
self.payload_type = PAYLOAD_TYPE(tmp_payload_type)
self._byteArray = byteArray
# =======================================
# BATTERY data payload
# =======================================
......@@ -787,33 +849,35 @@ class BatteryFormat(PayloadFormat):
_dataStruct = Struct("<BIBbbbbbbb")
payload_type: PAYLOAD_TYPE = PAYLOAD_TYPE.BATTERY
bat : int = 0
ok : int = 0
alarm : int = 0
rdy2buf : int = 0
bat85 : int = 0
prob_elec : int = 0
dummy : int = 0
bat: int = 0
ok: int = 0
alarm: int = 0
rdy2buf: int = 0
bat85: int = 0
prob_elec: int = 0
dummy: int = 0
# fill the struct from a byteArray,
def fromByteArray(self, byteArray):
tmp_payload_type = 0
(self.version,
(
self.version,
self.timestamp,
tmp_payload_type,
self.bat ,
self.ok ,
self.alarm ,
self.rdy2buf ,
self.bat85 ,
self.prob_elec ,
self.dummy ,
self.bat,
self.ok,
self.alarm,
self.rdy2buf,
self.bat85,
self.prob_elec,
self.dummy,
) = self._dataStruct.unpack(byteArray)
self.checkVersion()
self.payload_type = PAYLOAD_TYPE(tmp_payload_type)
self._byteArray = byteArray
# =======================================
# cmd type payload
# =======================================
......@@ -830,12 +894,14 @@ class CommandFormat(PayloadFormat):
cmd = 0
code = 0
tmp_payload_type = 0
(self.version,
(
self.version,
self.timestamp,
tmp_payload_type,
cmd,
code,
self.param) = self._dataStruct.unpack(byteArray)
self.param,
) = self._dataStruct.unpack(byteArray)
self.checkVersion()
self.payload_type = PAYLOAD_TYPE(tmp_payload_type)
......@@ -857,18 +923,22 @@ class AlarmFormat(PayloadFormat):
param: float = 0.0
def __eq__(self, other):
return (self.alarm_type == other.alarm_type) and (self.alarm_code == other.alarm_code)
return (self.alarm_type == other.alarm_type) and (
self.alarm_code == other.alarm_code
)
def fromByteArray(self, byteArray):
alarm = 0
priority = 0
tmp_payload_type = 0
(self.version,
(
self.version,
self.timestamp,
tmp_payload_type,
priority,
alarm,
self.param) = self._dataStruct.unpack(byteArray)
self.param,
) = self._dataStruct.unpack(byteArray)
self.checkVersion()
self.alarm_type = ALARM_TYPE(priority)
......
......@@ -22,7 +22,6 @@
# (https://hev.web.cern.ch/).
# Communication protocol between rasp and arduino based on HDLC format
# author Peter Svihra <peter.svihra@cern.ch>
......@@ -39,24 +38,27 @@ from collections import deque
import binascii
import logging
logging.basicConfig(level=logging.DEBUG, format='%(asctime)s - %(levelname)s - %(message)s')
logging.basicConfig(
level=logging.DEBUG, format="%(asctime)s - %(levelname)s - %(message)s"
)
# communication class that governs talking between devices
class CommsControl():
def __init__(self, port, baudrate = 115200, queueSizeReceive = 16, queueSizeSend = 16):
class CommsControl:
def __init__(self, port, baudrate=115200, queueSizeReceive=16, queueSizeSend=16):
self._serial = None
self.openSerial(port, baudrate)
# send queues are FIFO ring-buffers of the defined size
self._alarms = deque(maxlen = queueSizeSend)
self._commands = deque(maxlen = queueSizeSend)
self._data = deque(maxlen = queueSizeSend)
self._alarms = deque(maxlen=queueSizeSend)
self._commands = deque(maxlen=queueSizeSend)
self._data = deque(maxlen=queueSizeSend)
self._dw_lock = threading.Lock() # data write lock
# received queue and observers to be notified on update
self._payloadrecv = deque(maxlen = queueSizeReceive)
self._payloadrecv = deque(maxlen=queueSizeReceive)
self._observers = []
self._dr_lock = threading.Lock() # data read lock
......@@ -80,9 +82,11 @@ class CommsControl():
threading.Thread(target=self.sender, daemon=True).start()
# open serial port
def openSerial(self, port, baudrate = 115200, timeout = 2):
def openSerial(self, port, baudrate=115200, timeout=2):
if port is not None:
self._serial = serial.Serial(port = port, baudrate=baudrate, timeout = timeout, dsrdtr = True)
self._serial = serial.Serial(
port=port, baudrate=baudrate, timeout=timeout, dsrdtr=True
)
else:
try:
self._serial.close()
......@@ -94,9 +98,9 @@ class CommsControl():
while self._sending:
self._datavalid.wait()
if self._serial is not None:
self.sendQueue(self._alarms , 10)
self.sendQueue(self._alarms, 10)
self.sendQueue(self._commands, 50)
self.sendQueue(self._data , 200)
self.sendQueue(self._data, 200)
if self.finishedSending():
self._datavalid.clear()
......@@ -127,7 +131,7 @@ class CommsControl():
def sendQueue(self, queue, timeout):
with self._dw_lock:
if len(queue) > 0:
logging.debug(f'Queue length: {len(queue)}')
logging.debug(f"Queue length: {len(queue)}")
current_time = int(round(time.time() * 1000))
if current_time > (self._timeLastTransmission + timeout):
self._timeLastTransmission = current_time
......@@ -161,23 +165,29 @@ class CommsControl():
# TODO: this could be written in more pythonic way
# force read byte by byte
self._received.append(byte)
# logging.info(byte)
# logging.info(byte)
# find starting flag of the packet
if byte == bytes([0x7E]) and ((len(self._received) < self._receivedStart + 6) or not self._foundStart ):
if byte == bytes([0x7E]) and (
(len(self._received) < self._receivedStart + 6) or not self._foundStart
):
self._foundStart = True
self._receivedStart = len(self._received)
# find ending flag of the packet
elif byte == bytes([0x7E]) :
elif byte == bytes([0x7E]):
decoded = self.decoder(self._received, self._receivedStart)
if decoded is not None:
logging.debug(binascii.hexlify(decoded))
tmp_comms = CommsFormat.commsFromBytes(decoded)
if tmp_comms.compareCrc():
control = tmp_comms.getData()[tmp_comms.getControl()+1]
self._sequence_receive = (tmp_comms.getData()[tmp_comms.getControl()] >> 1) & 0x7F
control = tmp_comms.getData()[tmp_comms.getControl() + 1]
self._sequence_receive = (
tmp_comms.getData()[tmp_comms.getControl()] >> 1
) & 0x7F
# get type of payload and corresponding queue
payload_type = self.getInfoType(tmp_comms.getData()[tmp_comms.getAddress()])
payload_type = self.getInfoType(
tmp_comms.getData()[tmp_comms.getAddress()]
)
queue = self.getQueue(payload_type)
# get type of packet
......@@ -191,14 +201,20 @@ class CommsControl():
self.finishPacket(queue)
else:
sequence_receive = ((control >> 1) & 0x7F) + 1
address = tmp_comms.getData()[tmp_comms.getAddress():tmp_comms.getControl()]
address = tmp_comms.getData()[
tmp_comms.getAddress() : tmp_comms.getControl()
]
if self.receivePacket(payload_type, tmp_comms):
logging.debug("Preparing ACK")
comms_response = CommsFormat.CommsACK(address = address[0])
comms_response = CommsFormat.CommsACK(
address=address[0]
)
else:
logging.debug("Preparing NACK")
comms_response = CommsFormat.CommsNACK(address = address[0])
comms_response = CommsFormat.CommsNACK(
address=address[0]
)
comms_response.setSequenceReceive(sequence_receive)
self.sendPacket(comms_response)
......@@ -239,7 +255,9 @@ class CommsControl():
with self._dw_lock:
if len(queue) > 0:
# 0x7F to deal with possible overflows (0 should follow after 127)
if ((queue[0].getSequenceSend() + 1) & 0x7F) == self._sequence_receive:
if (
(queue[0].getSequenceSend() + 1) & 0x7F
) == self._sequence_receive:
self._sequence_send = (self._sequence_send + 1) % 128
queue.popleft()
except:
......@@ -264,9 +282,9 @@ class CommsControl():
payload = CommsCommon.IVTFormat()
elif data_type == CommsCommon.PAYLOAD_TYPE.DEBUG:
payload = CommsCommon.DebugFormat()
elif data_type == CommsCommon.PAYLOAD_TYPE.TARGET
elif data_type == CommsCommon.PAYLOAD_TYPE.TARGET:
payload = CommsCommon.TargetFormat()
elif data_type == CommsCommon.PAYLOAD_TYPE.PERSONAL
elif data_type == CommsCommon.PAYLOAD_TYPE.PERSONAL:
payload = CommsCommon.PersonalFormat()
elif data_type == CommsCommon.PAYLOAD_TYPE.THRESHOLDS:
# FIXME: nothing yet defined, TBD!!
......@@ -277,7 +295,11 @@ class CommsControl():
return False
try:
payload.fromByteArray(comms_packet.getData()[comms_packet.getInformation():comms_packet.getFcs()])
payload.fromByteArray(
comms_packet.getData()[
comms_packet.getInformation() : comms_packet.getFcs()
]
)
except Exception:
raise
else:
......@@ -288,14 +310,16 @@ class CommsControl():
# escape any 0x7D or 0x7E with 0x7D and swap bit 5
def escapeByte(self, byte):
if byte == 0x7D or byte == 0x7E:
return [0x7D, byte ^ (1<<5)]
return [0x7D, byte ^ (1 << 5)]
else:
return [byte]
# encoding data according to the protocol - escape any 0x7D or 0x7E with 0x7D and swap 5 bit
def encoder(self, data):
try:
stream = [escaped for byte in data[1:-1] for escaped in self.escapeByte(byte)]
stream = [
escaped for byte in data[1:-1] for escaped in self.escapeByte(byte)
]
result = bytearray([data[0]] + stream + [data[-1]])
return result
except:
......@@ -306,10 +330,16 @@ class CommsControl():
try:
packets = data[start:-1]
indRemove = [idx for idx in range(len(packets)) if packets[idx] == bytes([0x7D])]
indChange = [idx+1 for idx in indRemove]
indRemove = [
idx for idx in range(len(packets)) if packets[idx] == bytes([0x7D])
]
indChange = [idx + 1 for idx in indRemove]
stream = [packets[idx][0] ^ (1<<5) if idx in indChange else packets[idx][0] for idx in range(len(packets)) if idx not in indRemove]
stream = [
packets[idx][0] ^ (1 << 5) if idx in indChange else packets[idx][0]
for idx in range(len(packets))
if idx not in indRemove
]
result = bytearray([data[start - 1][0]] + stream + [data[-1][0]])
return result
except:
......@@ -345,8 +375,9 @@ class CommsControl():
for callback in self._observers:
callback(payloadrecv[0])
# start as interactive session to be able to send and receive
if __name__ == "__main__" :
if __name__ == "__main__":
# example dependant
class Dependant(object):
def __init__(self, lli):
......@@ -370,11 +401,15 @@ if __name__ == "__main__" :
except:
pass
comms = CommsControl(port = port)
comms = CommsControl(port=port)
example = Dependant(comms)
cmd = CommsCommon.CommandFormat(cmd_type = CommsCommon.CMD_TYPE.GENERAL.value, cmd_code = CommsCommon.CMD_GENERAL.START.value, param=0)
cmd = CommsCommon.CommandFormat(
cmd_type=CommsCommon.CMD_TYPE.GENERAL.value,
cmd_code=CommsCommon.CMD_GENERAL.START.value,
param=0,
)
time.sleep(4)
comms.writePayload(cmd)
print('sent cmd start')
print("sent cmd start")
print(cmd)
......@@ -15,7 +15,7 @@
# for more details.
#
# You should have received a copy of the GNU General Public License along
# with hev-sw. If not, see <http://www.gnu.org/licenses/>.
# with hev-sw. If not, see <http://www.gnu.org/licenses/> .
#
# The authors would like to acknowledge the much appreciated support
# of all those involved with the High Energy Ventilator project
......@@ -33,18 +33,23 @@ import threading
from typing import List, Dict, Union
from CommsCommon import PayloadFormat, PAYLOAD_TYPE
import logging
logging.basicConfig(level=logging.INFO,
format='%(asctime)s - %(levelname)s - %(message)s')
logging.basicConfig(
level=logging.INFO, format="%(asctime)s - %(levelname)s - %(message)s"
)
# use /dev/shm (in memory tmpfs) to hold the data, should be stable over Flask shenanigans when restaring scripts
import mmap
import pickle
import os
mmFileName = "/dev/shm/HEVClient_lastData.mmap"
class HEVPacketError(Exception):
pass
class HEVClient(object):
def __init__(self, polling=True):
super(HEVClient, self).__init__()
......@@ -61,14 +66,16 @@ class HEVClient(object):
self._lock = threading.Lock() # lock for the database
self._mmFile = None
if( os.access( mmFileName, os.F_OK ) ):
if os.access(mmFileName, os.F_OK):
self._mmFile = open(mmFileName, "a+b")
else:
self._mmFile = open(mmFileName, "x+b")
self._mmFile.write(b'0' * 10000) # ~10kb is enough I hope for one event
self._mmFile.write(b"0" * 10000) # ~10kb is enough I hope for one event
self._mmMap = mmap.mmap(self._mmFile.fileno(), 0) # Map to in memory object
self._mmMap.seek(0)
self._mmMap.write(pickle.dumps('Data not yet set')) # ensure no old or unset data in file
self._mmMap.write(
pickle.dumps("Data not yet set")
) # ensure no old or unset data in file
self._mmMap.flush()
# start polling in another thread unless told otherwise
......@@ -89,6 +96,8 @@ class HEVClient(object):
async def polling(self) -> None:
"""open persistent connection with server"""
writer = None
data = None
while True:
try:
reader, writer = await asyncio.open_connection("127.0.0.1", 54320)
......@@ -96,11 +105,11 @@ class HEVClient(object):
# grab data from the socket as soon as it is available and dump it in the db
while self._polling:
try:
data = await reader.readuntil(separator=b'\0')
data = await reader.readuntil(separator=b"\0")
data = data[:-1] # snip off nullbyte
payload = json.loads(data.decode("utf-8"))
if payload["type"] == "keepalive":
#Still alive
# Still alive
continue
elif payload["type"] == "DATA":
with self._lock:
......@@ -136,26 +145,39 @@ class HEVClient(object):
raise HEVPacketError("Invalid broadcast type")
self._alarms = payload["alarms"]
#self._personal = payload["personal"]
# self._personal = payload["personal"]
self.get_updates(payload) # callback function to be overridden
except json.decoder.JSONDecodeError:
logging.warning(f"Could not decode packet: {data}")
except KeyError:
raise
# close connection
writer.close()
await writer.wait_closed()
except ConnectionRefusedError as e:
logging.error(str(e) + " - is the microcontroller running?")
await asyncio.sleep(2)
except Exception as e:
# warn and reopen connection
logging.error(e)
await asyncio.sleep(2)
finally:
# close connection
if writer is not None:
writer.close()
await writer.wait_closed()
def get_updates(self, payload) -> None:
"""Overrideable function called after receiving data from the socket, with that data as an argument"""
pass
async def send_request(self, reqtype, cmdtype:str=None, cmd: str=None, param: str=None, alarm: str=None, personal: str=None) -> bool:
async def _send_request(
self,
reqtype,
cmdtype: str = None,
cmd: str = None,
param: str = None,
alarm: str = None,
personal: str = None,
) -> bool:
# open connection and send packet
reader, writer = await asyncio.open_connection("127.0.0.1", 54321)
payload = {"type": reqtype}
......@@ -177,13 +199,13 @@ class HEVClient(object):
raise HEVPacketError("Invalid packet type")
logging.info(payload)
packet = json.dumps(payload).encode() + b'\0'
packet = json.dumps(payload).encode() + b"\0"
writer.write(packet)
await writer.drain()
# wait for acknowledge
data = await reader.readuntil(separator=b'\0')
data = await reader.readuntil(separator=b"\0")
data = data[:-1]
try:
data = json.loads(data.decode("utf-8"))
......@@ -202,19 +224,24 @@ class HEVClient(object):
logging.warning(f"Request type {reqtype} failed")
return False
def send_cmd(self, cmdtype:str, cmd: str, param: Union[float,int]=None) -> bool:
def send_cmd(self, cmdtype: str, cmd: str, param: Union[float, int] = None) -> bool:
# send a cmd and wait to see if it's valid
#print(cmdtype, cmd, param)
return asyncio.run(self.send_request("CMD", cmdtype=cmdtype, cmd=cmd, param=param))
# print(cmdtype, cmd, param)
try:
return asyncio.run(
self._send_request("CMD", cmdtype=cmdtype, cmd=cmd, param=param)
)
except ConnectionRefusedError as error:
logging.error(str(error) + " - is the microcontroller running?")
def ack_alarm(self, alarm: str) -> bool:
# acknowledge alarm to remove it from the hevserver list
return asyncio.run(self.send_request("ALARM", alarm=alarm))
return asyncio.run(self._send_request("ALARM", alarm=alarm))
#def send_personal(self, personal: Dict[str, str]=None ) -> bool:
# def send_personal(self, personal: Dict[str, str]=None ) -> bool:
def send_personal(self, personal: str) -> bool:
# acknowledge alarm to remove it from the hevserver list
return asyncio.run(self.send_request("PERSONAL", personal=personal))
return asyncio.run(self._send_request("PERSONAL", personal=personal))
def get_values(self) -> Dict:
# get sensor values from db
......@@ -223,12 +250,12 @@ class HEVClient(object):
fastdata = pickle.load(self._mmFile)
except pickle.UnpicklingError as e:
logging.warning(f"Unpicking error {e}")
return None
if(type(fastdata) is dict):
return None # Should return empty dict here?
if type(fastdata) is dict:
return fastdata
else:
logging.warning("Missing fastdata")
return None
return None # Should return empty dict here?
def get_readback(self) -> Dict:
# get readback from db
......@@ -264,7 +291,6 @@ if __name__ == "__main__":
# just import hevclient and do something like the following
hevclient = HEVClient()
time.sleep(2)
print(hevclient.send_cmd("GENERAL", "START"))
# Play with sensor values and alarms
......@@ -273,7 +299,7 @@ if __name__ == "__main__":
alarms = hevclient.get_alarms() # returns a list of alarms currently ongoing
print(values)
if values is None:
i = i+1 if i > 0 else 0
i = i + 1 if i > 0 else 0
else:
print(f"Values: {json.dumps(values, indent=4)}")
print(f"Alarms: {alarms}")
......
......@@ -40,21 +40,37 @@ from pathlib import Path
from hevtestdata import HEVTestData
from CommsLLI import CommsLLI
from BatteryLLI import BatteryLLI
from CommsCommon import PAYLOAD_TYPE, CMD_TYPE, CMD_GENERAL, CMD_SET_DURATION, VENTILATION_MODE, ALARM_TYPE, ALARM_CODES, CMD_MAP, CommandFormat, AlarmFormat, PersonalFormat
from CommsCommon import (
PAYLOAD_TYPE,
CMD_TYPE,
CMD_GENERAL,
CMD_SET_DURATION,
VENTILATION_MODE,
ALARM_TYPE,
ALARM_CODES,
CMD_MAP,
CommandFormat,
AlarmFormat,
PersonalFormat,
)
from collections import deque
from serial.tools import list_ports
from typing import List
from struct import error as StructError
import logging
logging.basicConfig(format='%(asctime)s - %(levelname)s - %(message)s')
logging.basicConfig(format="%(asctime)s - %(levelname)s - %(message)s")
logging.getLogger().setLevel(logging.INFO)
class HEVPacketError(Exception):
pass
class HEVAlreadyRunning(Exception):
pass
class HEVServer(object):
def __init__(self, comms_lli, battery_lli):
self._alarms = []
......@@ -89,16 +105,16 @@ class HEVServer(object):
PAYLOAD_TYPE.ALARM,
PAYLOAD_TYPE.DEBUG,
PAYLOAD_TYPE.IVT,
#PAYLOAD_TYPE.LOGMSG,
# PAYLOAD_TYPE.LOGMSG,
PAYLOAD_TYPE.PERSONAL,
PAYLOAD_TYPE.CMD
PAYLOAD_TYPE.CMD,
]
if payload_type in whitelist:
# fork data to broadcast threads
for queue in self._broadcasts.values():
self.push_to(queue, payload)
#if payload_type == PAYLOAD_TYPE.PERSONAL : print("PERSONAL")
# if payload_type == PAYLOAD_TYPE.PERSONAL : print("PERSONAL")
elif payload_type in PAYLOAD_TYPE:
# valid payload but ignored
pass
......@@ -106,7 +122,6 @@ class HEVServer(object):
# invalid packet, don't throw exception just log and pop
logging.error(f"Received invalid packet, ignoring: {payload}")
async def handle_battery(self) -> None:
while True:
try:
......@@ -126,10 +141,11 @@ class HEVServer(object):
except HEVPacketError as e:
logging.error(e)
async def handle_request(self, reader: asyncio.StreamReader, writer: asyncio.StreamWriter) -> None:
async def handle_request(
self, reader: asyncio.StreamReader, writer: asyncio.StreamWriter
) -> None:
# listen for queries on the request socket
data = await reader.readuntil(separator=b'\0')
data = await reader.readuntil(separator=b"\0")
data = data[:-1] # snip off nullbyte
request = json.loads(data.decode("utf-8"))
......@@ -144,12 +160,14 @@ class HEVServer(object):
reqcmdtype = request["cmdtype"]
reqparam = request["param"] if request["param"] is not None else 0
command = CommandFormat(cmd_type=CMD_TYPE[reqcmdtype].value,
command = CommandFormat(
cmd_type=CMD_TYPE[reqcmdtype].value,
cmd_code=CMD_MAP[reqcmdtype].value[reqcmd].value,
param=reqparam)
param=reqparam,
)
#print('***sending cmd ')
#print(command)
# print('***sending cmd ')
# print(command)
self._comms_lli.writePayload(command)
# processed and sent to controller, send ack to GUI since it's in enum
......@@ -164,7 +182,7 @@ class HEVServer(object):
elif reqtype == "CYCLE":
# ignore for the minute
pass
#elif reqtype == "LOGSMG":
# elif reqtype == "LOGSMG":
# # ignore for the minute
# pass
elif reqtype == "TARGET":
......@@ -178,7 +196,14 @@ class HEVServer(object):
sex = request["sex"].encode()
height = int(request["height"])
weight = int(request["weight"])
pfmt = PersonalFormat(name=name, patient_id=patient_id, age=age, sex=sex, height=height, weight=weight)
pfmt = PersonalFormat(
name=name,
patient_id=patient_id,
age=age,
sex=sex,
height=height,
weight=weight,
)
self._comms_lli.writePayload(pfmt)
payload = {"type": "ack"}
pass
......@@ -194,9 +219,11 @@ class HEVServer(object):
reqalarm_code = ALARM_CODES[request["alarm_code"]]
reqparam = request["param"] if request["param"] is not None else 0
alarm_to_ack = AlarmFormat(alarm_type=ALARM_TYPE[reqalarm_type],
alarm_to_ack = AlarmFormat(
alarm_type=ALARM_TYPE[reqalarm_type],
alarm_code=reqalarm_code,
param=reqparam)
param=reqparam,
)
try:
# delete alarm if it exists
with self._dblock:
......@@ -205,7 +232,9 @@ class HEVServer(object):
self._alarms.remove(alarm)
payload = {"type": "ack"}
except NameError as e:
raise HEVPacketError(f"Alarm could not be removed. May have been removed already. {e}")
raise HEVPacketError(
f"Alarm could not be removed. May have been removed already. {e}"
)
else:
raise HEVPacketError(f"Invalid request type")
except (NameError, KeyError, HEVPacketError, StructError) as e:
......@@ -217,12 +246,14 @@ class HEVServer(object):
exit(1)
# send reply and close connection
packet = json.dumps(payload).encode() + b'\0'
packet = json.dumps(payload).encode() + b"\0"
writer.write(packet)
await writer.drain()
writer.close()
async def handle_broadcast(self, reader: asyncio.StreamReader, writer: asyncio.StreamWriter) -> None:
async def handle_broadcast(
self, reader: asyncio.StreamReader, writer: asyncio.StreamWriter
) -> None:
# log address
addr = writer.get_extra_info("peername")
logging.info(f"Broadcasting to {addr!r}")
......@@ -249,10 +280,14 @@ class HEVServer(object):
broadcast_packet[data_type] = payload.getDict()
broadcast_packet["alarms"] = [alarm.getDict() for alarm in alarms] if alarms is not None else []
broadcast_packet["alarms"] = (
[alarm.getDict() for alarm in alarms] if alarms is not None else []
)
self._broadcasts[bindex].task_done()
logging.info(f"Send data for timestamp: {broadcast_packet[data_type]['timestamp']}")
logging.info(
f"Send data for timestamp: {broadcast_packet[data_type]['timestamp']}"
)
logging.debug(f"Send: {json.dumps(broadcast_packet,indent=4)}")
try:
......@@ -269,8 +304,7 @@ class HEVServer(object):
del self._broadcasts[bindex]
async def serve_request(self, ip: str, port: int) -> None:
server = await asyncio.start_server(
self.handle_request, ip, port)
server = await asyncio.start_server(self.handle_request, ip, port)
# get address for log
addr = server.sockets[0].getsockname()
......@@ -280,8 +314,7 @@ class HEVServer(object):
await server.serve_forever()
async def serve_broadcast(self, ip: str, port: int) -> None:
server = await asyncio.start_server(
self.handle_broadcast, ip, port)
server = await asyncio.start_server(self.handle_broadcast, ip, port)
# get address for log
addr = server.sockets[0].getsockname()
......@@ -301,6 +334,7 @@ class HEVServer(object):
tasks = [b1, r1, b2, poll, battery]
await asyncio.gather(*tasks, return_exceptions=True)
def getArduinoPort():
# get arduino serial port
port_device = None
......@@ -311,7 +345,7 @@ def getArduinoPort():
logging.debug(vidpid)
if port.manufacturer and "ARDUINO" in port.manufacturer.upper():
port_device = port.device
elif vidpid == "10C4:EA60" :
elif vidpid == "10C4:EA60":
port_device = port.device
if port_device is None:
logging.critical(f"Arduino disconnected")
......@@ -331,14 +365,29 @@ if __name__ == "__main__":
tasks = [] # asyncio tasks
loop = asyncio.get_event_loop()
try:
#parser to allow us to pass arguments to hevserver
parser = argparse.ArgumentParser(description='Arguments to run hevserver')
parser.add_argument('-i', '--inputFile', type=str, default = '', help='Load data from file')
parser.add_argument('-d', '--debug', action='count', default=0, help='Show debug output')
parser.add_argument('--use-test-data', action='store_true', help='Use test data source')
parser.add_argument('--use-dump-data', action='store_true', help='Use dump data source')
parser.add_argument('--dump', type=int, default=0, help='Dump NUM raw data packets to file')
parser.add_argument('-o', '--dumpfile', type=str, default = '', help='File to dump to')
# parser to allow us to pass arguments to hevserver
parser = argparse.ArgumentParser(description="Arguments to run hevserver")
parser.add_argument(
"-i", "--inputFile", type=str, default="", help="Load data from file"
)
parser.add_argument(
"-d", "--debug", action="count", default=0, help="Show debug output"
)
parser.add_argument(
"--use-test-data", action="store_true", help="Use test data source"
)
parser.add_argument(
"--use-dump-data", action="store_true", help="Use dump data source"
)
parser.add_argument(
"--mcu-tty", type=str, default="", help="Specify tty serial device for MCU"
)
parser.add_argument(
"--dump", type=int, default=0, help="Dump NUM raw data packets to file"
)
parser.add_argument(
"-o", "--dumpfile", type=str, default="", help="File to dump to"
)
args = parser.parse_args()
if args.debug == 0:
logging.getLogger().setLevel(logging.WARNING)
......@@ -358,16 +407,18 @@ if __name__ == "__main__":
pass
else:
if psutil.pid_exists(pid):
raise HEVAlreadyRunning(f"hevserver is already running. To kill it run:\n $ kill {pid}")
raise HEVAlreadyRunning(
f"hevserver is already running. To kill it run:\n $ kill {pid}"
)
with open(pidfile, 'w') as f:
with open(pidfile, "w") as f:
f.write(str(mypid))
if args.use_test_data:
comms_lli = HEVTestData()
logging.info(f"Using test data source")
elif args.inputFile != '':
if args.inputFile[-1-3:] == '.txt':
elif args.inputFile != "":
if args.inputFile[-1 - 3 :] == ".txt":
# just ignore actual filename and read from both valid inputfiles
comms_lli = hevfromtxt.hevfromtxt()
else:
......@@ -376,7 +427,12 @@ if __name__ == "__main__":
# initialise low level interface
try:
if args.use_dump_data:
port_device = '/dev/shm/ttyEMU0'
port_device = "/dev/shm/ttyEMU0"
elif args.mcu_tty is not "":
if not os.path.exists(args.mcu_tty):
logging.critical("MCU tty does not exist")
exit(1)
port_device = args.mcu_tty
else:
port_device = getArduinoPort()
connected = arduinoConnected()
......@@ -385,7 +441,7 @@ if __name__ == "__main__":
if args.dump == 0:
comms_lli = CommsLLI(loop)
elif args.dump > 0:
if args.dumpfile == '':
if args.dumpfile == "":
logging.critical("No dump file specified")
raise KeyboardInterrupt # fake ctrl+c
logging.warning(f"Dumping {args.dump} packets to {args.dumpfile}")
......
This source diff could not be displayed because it is too large. You can view the blob instead.
# libscrc==1.5 # installed via git below
git+https://github.com/hex-in/libscrc.git@v1.5
GitPython==3.1.14
numpy==1.19.5
psutil==5.8.0
pyqtgraph==0.11.1
pyserial==3.5
pyserial-asyncio==0.5
# PySide2==5.15.2 # installed via apt
pytest==6.2.1
# shiboken2==5.15.2 # shiboken2 is a dependancy for PySide2, however, with Pyside2 installed via apt is no longer needed.
# virtualenv==20.4.0 # installed via ansible
{"version": 182, "timestamp": 816562, "type": "ALARM", "alarm_type": "PRIORITY_MEDIUM", "alarm_code": "HIGH_VTE", "param": 24868.025390625}
{"version": 182, "timestamp": 0, "type": "BATTERY", "bat": 0, "ok": 0, "alarm": 0, "rdy2buf": 0, "bat85": 0, "prob_elec": 0, "dummy": false}
{"version": 182, "timestamp": 816103, "type": "DATA", "fsm_state": "INHALE", "pressure_air_supply": 13, "pressure_air_regulated": 365.1913757324219, "pressure_o2_supply": 45, "pressure_o2_regulated": 0.0322265625, "pressure_buffer": 239.28102111816406, "pressure_inhale": 17.676271438598633, "pressure_patient": 16.122142791748047, "temperature_buffer": 659, "pressure_diff_patient": 0.5741281509399414, "ambient_pressure": 0, "ambient_temperature": 0, "airway_pressure": 7.061350345611572, "flow": 34.1150016784668, "flow_calc": 0.9805641174316406, "volume": 46869.6953125, "target_pressure": 17.0, "process_pressure": 17.676271438598633, "valve_duty_cycle": 0.5721527934074402, "proportional": -0.0013525428948923945, "integral": 0.04374659061431885, "derivative": -0.12062221765518188}
{
"version": 182,
"timestamp": 815263,
"type": "READBACK",
"duration_pre_calibration": 6000,
"duration_calibration": 4000,
"duration_buff_purge": 600,
"duration_buff_flush": 600,
"duration_buff_prefill": 100,
"duration_buff_fill": 600,
"duration_buff_pre_inhale": 0,
"duration_inhale": 1000,
"duration_pause": 10,
"duration_exhale": 2990,
"valve_air_in": 0.0,
"valve_o2_in": 0.0,
"valve_inhale": 0,
"valve_exhale": 1,
"valve_purge": 0,
"ventilation_mode": "PC_AC",
"valve_inhale_percent": 0,
"valve_exhale_percent": 0,
"valve_air_in_enable": 1,
"valve_o2_in_enable": 1,
"valve_purge_enable": 1,
"inhale_trigger_enable": 1,
"exhale_trigger_enable": 0,
"peep": 3.167065143585205,
"inhale_exhale_ratio": 0.33779263496398926,
"kp": 0.0010000000474974513,
"ki": 0.0005000000237487257,
"kd": 0.0010000000474974513,
"pid_gain": 2.0,
"max_patient_pressure": 45
}
{
"version": 182,
"timestamp": 544077,
"type": "TARGET",
"mode": "CURRENT",
"inspiratory_pressure": 17.0,
"ie_ratio": 0.33779263496398926,
"volume": 400.0,
"respiratory_rate": 15.0,
"peep": 5.0,
"fiO2_percent": 21.0,
"inhale_time": 1.0,
"inhale_trigger_enable": 1,
"exhale_trigger_enable": 0,
"volume_trigger_enable": 0,
"inhale_trigger_threshold": 5.0,
"exhale_trigger_threshold": 25.0,
"buffer_upper_pressure": 300.0,
"buffer_lower_pressure": 285.0
}
\ No newline at end of file
#!/bin/bash
# This shell script uses ansible, apt-get, and pip to install all the requirements to run the HEV UI.
set -e
# Define colours
RED='\033[0;31m'
YELLOW='\033[1;33m'
ITALIC='\033[3m'
NC='\033[0m' # No Color or Syntax
# Create a hosts file from default
hostsfile="ansible/playbooks/hosts"
# create and write hosts file function
function create_hostsfile {
# Replace current hostfile with the default
rm -f $hostsfile
# Get users raspberry pi / VM IP address
echo "What is the IP address for your Raspberry Pi / VM you wish to setup?"
echo -e "${ITALIC}NOTE: If you use a non-standard SSH port (22), add the port to your IP address as such: ${YELLOW}IPADDRESS:PORT${NC}"
echo -e "${ITALIC}NOTE: If you wish to run the ansible installation locally, please input: ${YELLOW}localhost${NC}"
read -r ipaddr
# Add the IP address into hosts file
if [[ $ipaddr == "" ]]; then
echo -e "${RED}ERROR:${NC} user input for IP Address was blank. Please rerun and enter IP Address."
exit 1
elif [[ $ipaddr == "localhost" ]]; then
cp -rp ansible/playbooks/hosts.local $hostsfile
local=True
echo "Installing locally at $repo_location."
else
cp -rp ansible/playbooks/hosts.default $hostsfile
if [[ "$OSTYPE" == "darwin"* ]]; then
sed -i '' "s/IPADDRESS/$ipaddr/g" $hostsfile
else
sed -i "s/IPADDRESS/$ipaddr/g" $hostsfile
fi
echo "User inputtted IP Address added to $hostsfile."
fi
}
# Check if local flag has been used
cli_flag1=$1
if [[ $cli_flag1 == 'CI' ]]; then
cp -rp ansible/playbooks/hosts.local $hostsfile
local=True
repo_location=$(pwd)
echo "Installing locally at $repo_location."
elif [[ $cli_flag1 == '' ]]; then
# Check if in repo and move to top level of repo
if [[ -d $(git rev-parse --git-dir 2> /dev/null) ]]; then
cd "$(git rev-parse --show-toplevel)"
repo_location=$(pwd)
else
echo "ERROR: Not a git directory. Please run setup.sh from the HEV repository."
exit 1
fi
# Get the pi / vm ip address from user
if [[ -f $hostsfile ]]; then
# Check if hosts file already exists
echo -e "${YELLOW}$hostsfile${NC} already exists, override and create a new hosts file? [y/n]"
read -r yn
case $yn in
[Yy]* )
create_hostsfile;;
[Nn]* ) if [[ $(sed -n 2p ansible/playbooks/hosts) == *"localhost"* ]]; then
local=True
else
ipaddr=$(sed -n 2p ansible/playbooks/hosts) # copy IP address in hosts file to variable ipaddr
fi;;
* ) echo "Please answer yes or no."; exit 1;;
esac
else
create_hostsfile
fi
else
echo "ERROR: Only CLI input accepted is 'local' for local installation."
exit 1
fi
# Run ansible playbooks for both local and remote
if [[ $local == True ]]; then # Local run
# Create local variables for ansible installation
echo "Using ansible to setup local HEV install."
cd ansible
source hev-ansible.sh
# Run ansible playbooks
cd playbooks
# Create local repo variable
ansible-playbook firstboot.yml
ansible-playbook install_software_local.yml
# Clean up
cd "$(git rev-parse --show-toplevel)"
echo
echo "SETUP FINISHED"
echo -e "${YELLOW}Rasperberry Pi / VM must be rebooted for changes to take effect.${NC}"
echo "Please run 'sudo reboot'."
echo
else # Remote run
# Create local variables for ansible installation
echo "Using ansible to setup remote Raspberry Pi / VM."
cd ansible
source hev-ansible.sh
# Run ansible playbooks
cd playbooks
ansible-playbook firstboot.yml
ansible-playbook install_software.yml
# Clean up
cd "$(git rev-parse --show-toplevel)"
# Request to reboot raspberry pi / VM
echo
echo "SETUP FINISHED"
echo -e "${YELLOW}Rasperberry Pi / VM must be rebooted for changes to take effect.${NC}"
echo "Please run 'ssh pi@$ipaddr \"sudo reboot\"'."
echo
fi
\ No newline at end of file
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment