"""
===========
Connections
===========
"""
import sys
import os
import json
import time
import types
import pickle
import shutil
import logging
import requests
from openbci_stream.acquisition import (
Cyton,
CytonBase,
wifi,
restart_services,
)
from PySide6.QtCore import Qt, Signal, QThread, Slot, QTimer
from PySide6.QtGui import QCursor, QIcon
from PySide6.QtWidgets import QApplication
from ..dialogs import Dialogs
from ...extensions import properties as prop
# from ...extensions.data_analysis.utils import thread_this
########################################################################
[docs]class OpenBCIThread(QThread):
"""Handle the OpenBCI connection."""
connection_ok = Signal()
connection_fail = Signal(object)
disconnected_ok = Signal()
connected = False
# ----------------------------------------------------------------------
[docs] def stop(self) -> None:
"""Kill the trhrad."""
self.terminate()
# ----------------------------------------------------------------------
[docs] def run(self) -> None:
"""Connect and configure OpenBCI board."""
if sum(self.channels_assignations) < len(self.montage):
self.connection_fail.emit(
[
'* The number of electrodes defined in montage not correspon with the number of boards available.'
]
)
return
try:
self.openbci = Cyton(
self.mode,
self.endpoint,
host=self.host,
capture_stream=False,
daisy=self.daisy,
montage=self.montage,
streaming_package_size=self.streaming_package_size,
number_of_channels=self.channels_assignations,
)
except Exception as msg:
logging.warning(msg)
self.connection_fail.emit([])
return
self.session_settings(
self.montage,
self.bias,
self.gain,
self.srb1,
self.adsinput,
self.srb2,
self.sample_rate,
)
if getattr(self.openbci, 'is_recycled', False):
self.connected = True
self.start_stream_()
self.connection_ok.emit()
return
try:
self.openbci.command(self.sample_rate)
self.openbci.command(self.boardmode)
except TimeoutError:
self.connection_fail.emit(
['* OpenBCI board could not be connected the same network.']
)
return
# Some time this command not take effect
boardmode_setted = False
for _ in range(10):
response = self.openbci.command(self.boardmode)
if response and all([b'Success:' in r for r in response]):
self.boardmode_s = CytonBase.BOARD_MODE_VALUE[self.boardmode]
logging.info(f'Bardmode setted in "{self.boardmode_s}" mode')
boardmode_setted = True
break
time.sleep(0.1)
# boardmode = self.openbci.boardmode
if not boardmode_setted:
logging.warning('Boardmode not setted!')
self.boardmode_s = None
# self.session_settings(self.montage, self.bias,
# self.gain, self.srb1, self.adsinput, self.srb2, self.sample_rate)
if self.checkBox_send_leadoff:
self.openbci.leadoff_impedance(
self.montage, pchan=self.pchan, nchan=self.nchan
)
if self.checkBox_test_signal:
test_signal = self.comboBox_test_signal
test_signal = getattr(
CytonBase, f"TEST_{test_signal.replace(' ', '_')}"
)
self.openbci.command(test_signal)
# else:
# used_channels = set(self.montage.keys())
# if used_channels:
# self.openbci.activate_channel(used_channels)
# # all_channels = set(range(1, 17 if prop.DAISY else 9))
# # deactivated = all_channels.difference(used_channels)
# # if deactivated:
# # self.openbci.deactivate_channel(deactivated)
self.start_stream_()
# ----------------------------------------------------------------------
def start_stream_(self):
""""""
if not self.streaming():
try:
self.openbci.start_stream()
self.connected = True
self.connection_ok.emit()
if self.mode == 'wifi':
self.openbci.set_latency(self.tcp_latency)
except Exception as e:
if 'NoBrokersAvailable' in str(e):
self.connection_fail.emit(
[
'* Kafka is not running on the remote acquisition system.'
]
)
# ----------------------------------------------------------------------
[docs] def session_settings(self, *args) -> None:
"""Create a session setting to reuse it after an impedance measurement."""
if hasattr(self, 'last_settings'):
(
channels,
bias,
gain,
srb1,
adsinput,
srb2,
sample_rate,
) = self.last_settings
elif args:
self.last_settings = args
channels, bias, gain, srb1, adsinput, srb2, sample_rate = args
else:
return
response = self.openbci.command(sample_rate)
logging.warning(response)
if self.checkBox_default_settings:
self.openbci.command(self.openbci.DEFAULT_CHANNELS_SETTINGS)
else:
self.openbci.channel_settings(
channels,
power_down=CytonBase.POWER_DOWN_ON,
gain=gain,
input_type=adsinput,
bias=bias,
srb2=[f'{s}'.encode() for s in srb2],
srb1=srb1,
)
# ----------------------------------------------------------------------
[docs] def disconnect(self) -> None:
"""Disconnect OpenBCI."""
try:
# self.openbci.stop_stream()
self.openbci.close()
except:
pass
self.disconnected_ok.emit()
self.connected = False
########################################################################
[docs]class Connection:
"""Widget that handle the OpenBCI connection."""
# ----------------------------------------------------------------------
def __init__(self, core):
""""""
self.parent_frame = core.main
self.core = core
# self.parent.pushButton_disconnect.hide()
self.openbci = OpenBCIThread()
self.openbci.streaming = lambda: getattr(
self.core, 'streaming', False
)
self.openbci.connection_ok.connect(self.connection_ok)
self.openbci.connection_fail.connect(self.connection_fail)
self.openbci.disconnected_ok.connect(self.disconnected_ok)
self.channels_assignations = {}
self.config = {
'mode': self.parent_frame.comboBox_connection_mode,
'check1': self.parent_frame.checkBox_board1,
'check2': self.parent_frame.checkBox_board2,
'check3': self.parent_frame.checkBox_board3,
'check4': self.parent_frame.checkBox_board4,
'port1': self.parent_frame.comboBox_port1,
'port2': self.parent_frame.comboBox_port2,
'port3': self.parent_frame.comboBox_port3,
'port4': self.parent_frame.comboBox_port4,
'ip1': self.parent_frame.comboBox_ip1,
'ip2': self.parent_frame.comboBox_ip2,
'ip3': self.parent_frame.comboBox_ip3,
'ip4': self.parent_frame.comboBox_ip4,
'host': self.parent_frame.comboBox_host,
'acquisition_sample_rate': self.parent_frame.comboBox_sample_rate,
'streaming_sample_rate': self.parent_frame.comboBox_streaming_sample_rate,
'boardmode': self.parent_frame.comboBox_boardmode,
'gain': self.parent_frame.comboBox_gain,
'input_type': self.parent_frame.comboBox_input_type,
'bias': self.parent_frame.comboBox_bias,
'srb1': self.parent_frame.comboBox_srb1,
'srb2': self.parent_frame.comboBox_srb2,
'pchan': self.parent_frame.comboBox_pchan,
'nchan': self.parent_frame.comboBox_nchan,
'test_signal_type': self.parent_frame.comboBox_test_signal,
'test_signal': self.parent_frame.checkBox_test_signal,
'tcp_latency': self.parent_frame.spinBox_tcp_latency,
}
self.load_config()
self.update_connections()
self.update_environ()
self.connect()
self.core.config.connect_widgets(self.update_config, self.config)
QTimer().singleShot(1000, self.autoconnect)
# ----------------------------------------------------------------------
def restart_services(self):
""""""
if json.loads(os.getenv('BCISTREAM_RASPAD')):
try:
restart_services(prop.HOST)
logging.warning(f'Restarting services on {prop.HOST}')
except:
logging.warning(
f'Impossible to restart services on {prop.HOST}'
)
# ----------------------------------------------------------------------
[docs] def on_focus(self) -> None:
"""Try to autoconnect."""
# if getattr(self.core, 'streaming', False) and not self.openbci.connected:
# self.openbci_connect()
for i in range(1, 5):
combo = getattr(self.parent_frame, f'comboBox_ip{i}')
target = getattr(self.parent_frame, f'pushButton_ip{i}')
check = getattr(self.parent_frame, f'checkBox_board{i}')
if check.isChecked():
self.request_wifi(i, target, combo, no_cursor=True)()
# ----------------------------------------------------------------------
[docs] def load_config(self) -> None:
"""Load widgets."""
self.core.config.load_widgets('connection', self.config)
self.core.update_kafka(self.parent_frame.comboBox_host.currentText())
# ----------------------------------------------------------------------
[docs] def update_config(self, *args, **kwargs) -> None:
"""Save widgets status."""
self.core.config.save_widgets('connection', self.config)
# ----------------------------------------------------------------------
[docs] def connect(self) -> None:
"""Connect events."""
self.parent_frame.pushButton_connect.clicked.connect(self.on_connect)
self.parent_frame.comboBox_connection_mode.activated.connect(
self.update_connections
)
# self.parent_frame.comboBox_host.textActivated.connect(
# self.load_config)
# CheckWiFiModule
for i in range(1, 5):
combo = getattr(self.parent_frame, f'comboBox_ip{i}')
target = getattr(self.parent_frame, f'pushButton_ip{i}')
# combo.textActivated.connect(
# self.request_wifi(i, target, combo))
target.setStyleSheet(
"""*{
background-color: transparent !important;
}"""
)
getattr(
self.parent_frame, f'pushButton_update_wifi{i}'
).clicked.connect(self.request_wifi(i, target, combo))
getattr(
self.parent_frame, f'checkBox_board{i}'
).stateChanged.connect(self.wrap_clear_text(target))
# check = getattr(self.parent_frame, f'checkBox_board{i}')
# if check.isChecked():
# self.request_wifi(i, target, combo)()
# ----------------------------------------------------------------------
def wrap_clear_text(self, target):
""""""
def wrapped():
target.setText('')
return wrapped
# ----------------------------------------------------------------------
def request_wifi(self, board, target, combo, no_cursor=False):
""""""
target.setText('')
target.hide()
# @thread_this
def inset():
if not no_cursor:
QApplication.setOverrideCursor(QCursor(Qt.WaitCursor))
ip = combo.currentText()
combo.valid = False
try:
target.setStyleSheet(
"""*{
background-color: transparent !important;
}"""
)
response = wifi(
self.parent_frame.comboBox_host.currentText(), ip
)
if response['board_connected']:
target.setText(
f"connected ({response['num_channels']} ch)"
)
target.setProperty('class', 'success icon_button')
combo.valid = True
self.channels_assignations[board] = response[
'num_channels'
]
else:
target.setText('connected (no board detected)')
target.setProperty('class', 'danger icon_button')
if board in self.channels_assignations:
del self.channels_assignations[board]
target.style().unpolish(target)
target.style().polish(target)
target.update()
except:
target.setStyleSheet(
"""*{
background-color: transparent !important;
}"""
)
target.setText('No WiFi module found')
target.setProperty('class', 'danger icon_button')
target.style().unpolish(target)
target.style().polish(target)
target.update()
if board in self.channels_assignations:
del self.channels_assignations[board]
target.show()
if not no_cursor:
QApplication.restoreOverrideCursor()
return inset
# ----------------------------------------------------------------------
[docs] def update_connections(self) -> None:
"""Set widgets for connection modes."""
if (
'serial'
in self.parent_frame.comboBox_connection_mode.currentText().lower()
):
for index in range(
1, self.parent_frame.comboBox_sample_rate.count()
):
self.parent_frame.comboBox_sample_rate.model().item(
index
).setEnabled(False)
if self.parent_frame.comboBox_sample_rate.currentIndex() >= 1:
self.parent_frame.comboBox_sample_rate.setCurrentIndex(0)
for index in range(
3, self.parent_frame.comboBox_streaming_sample_rate.count()
):
self.parent_frame.comboBox_streaming_sample_rate.model().item(
index
).setEnabled(
False
)
if (
self.parent_frame.comboBox_streaming_sample_rate.currentIndex()
>= 3
):
self.parent_frame.comboBox_streaming_sample_rate.setCurrentIndex(
2
)
# self.parent_frame.label_port_ip.setText('Port')
for i in range(1, 5):
getattr(self.parent_frame, f'comboBox_ip{i}').hide()
getattr(self.parent_frame, f'comboBox_port{i}').show()
getattr(
self.parent_frame, f'pushButton_update_wifi{i}'
).hide()
getattr(self.parent_frame, f'pushButton_ip{i}').hide()
# self.parent_frame.comboBox_ip.hide()
# self.parent_frame.comboBox_port.show()
self.parent_frame.label_latency.setEnabled(False)
self.parent_frame.spinBox_tcp_latency.setEnabled(False)
else:
for index in range(
1, self.parent_frame.comboBox_sample_rate.count()
):
self.parent_frame.comboBox_sample_rate.model().item(
index
).setEnabled(True)
for index in range(
3, self.parent_frame.comboBox_streaming_sample_rate.count()
):
self.parent_frame.comboBox_streaming_sample_rate.model().item(
index
).setEnabled(
True
)
# self.parent_frame.label_port_ip.setText('IP')
for i in range(1, 5):
getattr(self.parent_frame, f'comboBox_ip{i}').show()
getattr(self.parent_frame, f'comboBox_port{i}').hide()
getattr(
self.parent_frame, f'pushButton_update_wifi{i}'
).show()
getattr(self.parent_frame, f'pushButton_ip{i}').show()
# self.parent_frame.comboBox_ip.show()
# self.parent_frame.comboBox_port.hide()
self.parent_frame.label_latency.setEnabled(True)
self.parent_frame.spinBox_tcp_latency.setEnabled(True)
# ----------------------------------------------------------------------
def autoconnect(self) -> None:
""""""
self.core.calculate_offset()
if (
getattr(self.core, 'streaming', False)
and not self.openbci.connected
):
self.on_focus()
if self.openbci_connect():
self.connection_ok()
# ----------------------------------------------------------------------
[docs] def on_connect(self, toggled: bool) -> None:
"""Event to handle connection."""
self.core.calculate_offset()
self.restart_services()
if (
getattr(self.core, 'streaming', False)
and not self.openbci.connected
):
self.openbci_connect()
else:
if toggled: # Connect
QApplication.setOverrideCursor(QCursor(Qt.WaitCursor))
self.core.update_kafka(
self.parent_frame.comboBox_host.currentText()
)
# self.restart_services()
self.openbci_connect()
else: # Disconnect
self.openbci.disconnect()
# ----------------------------------------------------------------------
[docs] def openbci_connect(self) -> bool:
"""Recollect values from GUI."""
if (
'serial'
in self.parent_frame.comboBox_connection_mode.currentText().lower()
):
mode = 'serial'
endpoint = [
getattr(self.parent_frame, f'comboBox_port{i}').currentText()
for i in range(1, 5)
if getattr(
getattr(self.parent_frame, f'comboBox_ip{i}'),
'valid',
False,
)
]
os.environ['BCISTREAM_CONNECTION'] = json.dumps('serial')
else:
mode = 'wifi'
endpoint = [
getattr(self.parent_frame, f'comboBox_ip{i}').currentText()
for i in range(1, 5)
if getattr(
getattr(self.parent_frame, f'comboBox_ip{i}'),
'valid',
False,
)
]
os.environ['BCISTREAM_CONNECTION'] = json.dumps('wifi')
host = self.parent_frame.comboBox_host.currentText()
sample_rate = self.parent_frame.comboBox_sample_rate.currentText()
sample_rate = getattr(CytonBase, f"SAMPLE_RATE_{sample_rate}SPS")
streaming_sample_rate = (
self.parent_frame.comboBox_streaming_sample_rate.currentText()
)
boardmode = self.parent_frame.comboBox_boardmode.currentText()
boardmode = getattr(CytonBase, f"BOARD_MODE_{boardmode}")
gain = self.parent_frame.comboBox_gain.currentText()
gain = getattr(CytonBase, f'GAIN_{gain}')
adsinput = self.parent_frame.comboBox_input_type.currentText()
adsinput = getattr(CytonBase, f'ADSINPUT_{adsinput}')
bias = self.parent_frame.comboBox_bias.currentText()
bias = getattr(CytonBase, f"BIAS_{bias}")
srb1 = self.parent_frame.comboBox_srb1.currentText()
srb1 = getattr(CytonBase, f'SRB1_{srb1}')
srb2 = self.parent_frame.comboBox_srb2.currentText()
srb2 = getattr(CytonBase, f'SRB2_{srb2}')
pchan = self.parent_frame.comboBox_pchan.currentText()
pchan = getattr(CytonBase, pchan.replace(' ', '_'))
nchan = self.parent_frame.comboBox_nchan.currentText()
nchan = getattr(CytonBase, nchan.replace(' ', '_'))
tcp_latency = self.parent_frame.spinBox_tcp_latency.value()
# channels = self.core.montage.get_mne_montage().ch_names
# self.openbci = OpenBCIThread()
# self.openbci.connection_ok.connect(self.connection_ok)
# self.openbci.connection_fail.connect(self.connection_fail)
# self.openbci.disconnected_ok.connect(self.disconnected_ok)
self.openbci.mode = mode
self.openbci.endpoint = endpoint
self.openbci.host = host
self.openbci.tcp_latency = tcp_latency
self.openbci.montage = prop.CHANNELS
self.openbci.streaming_package_size = int(streaming_sample_rate)
self.openbci.sample_rate = sample_rate
self.openbci.boardmode = boardmode
self.openbci.adsinput = adsinput
self.openbci.bias = bias
self.openbci.srb1 = srb1
# self.openbci.srb2 = srb2
self.openbci.srb2 = prop.MONTAGE_TYPE
self.openbci.pchan = pchan
self.openbci.nchan = nchan
self.openbci.gain = gain
for i in range(1, 5):
check = getattr(self.parent_frame, f'checkBox_board{i}')
# combo = getattr(self.parent_frame, f'comboBox_ip{i}')
# target = getattr(self.parent_frame, f'pushButton_ip{i}')
if (not check.isChecked()) and (i in self.channels_assignations):
del self.channels_assignations[i]
self.openbci.channels_assignations = [
e[1] for e in sorted(list(self.channels_assignations.items()))
]
self.openbci.daisy = [
True if chs == 16 else False
for chs in self.openbci.channels_assignations
]
os.environ['BCISTREAM_DAISY'] = json.dumps(all(self.openbci.daisy))
os.environ['BCISTREAM_CHANNELS_BY_BOARD'] = json.dumps(
self.openbci.channels_assignations
)
# self.openbci.checkBox_send_leadoff = self.parent_frame.checkBox_send_leadoff.isChecked()
self.openbci.checkBox_send_leadoff = (
self.parent_frame.groupBox_leadoff_impedance.isChecked()
)
# self.openbci.checkBox_default_settings = self.parent_frame.checkBox_default_settings.isChecked()
self.openbci.checkBox_default_settings = (
not self.parent_frame.groupBox_settings.isChecked()
)
self.openbci.checkBox_test_signal = (
self.parent_frame.checkBox_test_signal.isChecked()
)
self.openbci.comboBox_test_signal = (
self.parent_frame.comboBox_test_signal.currentText()
)
self.parent_frame.pushButton_connect.setText('Connecting...')
self.parent_frame.pushButton_connect.setEnabled(False)
self.update_environ()
self.openbci.start()
return True
# ----------------------------------------------------------------------
[docs] def update_environ(self) -> None:
"""Update environment variables."""
os.environ['BCISTREAM_HOST'] = json.dumps(
self.parent_frame.comboBox_host.currentText()
)
sps = self.parent_frame.comboBox_sample_rate.currentText()
if 'k' in sps.lower():
sps = int(sps.lower().replace('k', '')) * 1000
else:
sps = int(sps)
os.environ['BCISTREAM_SAMPLE_RATE'] = json.dumps(sps)
os.environ['BCISTREAM_STREAMING_PACKAGE_SIZE'] = json.dumps(
int(
self.parent_frame.comboBox_streaming_sample_rate.currentText()
)
)
os.environ['BCISTREAM_BOARDMODE'] = json.dumps(
self.parent_frame.comboBox_boardmode.currentText().lower()
)
# ----------------------------------------------------------------------
# @Slot()
[docs] def connection_ok(self) -> None:
"""Event for OpenBCI connected."""
QApplication.restoreOverrideCursor()
self.parent_frame.pushButton_connect.setText('Disconnect')
self.parent_frame.pushButton_connect.setEnabled(True)
self.parent_frame.pushButton_connect.setChecked(True)
if '--local' in sys.argv:
dst_config = os.path.join(os.environ['BCISTREAM_ROOT'],
'python_scripts', 'bciframework_server', 'bciframework.config')
src_config = os.path.join(
os.getenv('BCISTREAM_ROOT'), 'assets', 'bciframework.default')
else:
dst_config = os.path.join(
os.environ['BCISTREAM_HOME'], 'python_scripts', 'bciframework_server', 'bciframework.config')
src_config = os.path.join(
os.getenv('BCISTREAM_HOME'), '.bciframework')
if os.path.exists(dst_config):
os.remove(dst_config)
shutil.copyfile(src_config, dst_config)
# ----------------------------------------------------------------------
[docs] @Slot()
def connection_fail(self, reasons):
"""Event for OpenBCI failed connection."""
QApplication.restoreOverrideCursor()
checks = []
if reasons:
checks.extend(reasons)
else:
if (
'serial'
in self.parent_frame.comboBox_connection_mode.currentText().lower()
):
checks.extend(
[
'* Check that USB dongle were connected.',
'* Verify serial permissions.',
]
)
else:
checks.extend(
[
f"* The WiFi moudule could be running in a different IP",
]
)
if self.parent_frame.comboBox_host.currentText() != 'localhost':
checks.extend(
[
f'* The server could not be running, or running on a different IP that {self.parent_frame.comboBox_host.currentText()}',
'* This machine must have access to the server or running on the same network.',
'* The daemons `stream_eeg` `stream_rpyc` could not be running', # todo
]
)
if hasattr(self.core, 'conection_message'):
checks.extend([self.core.conection_message])
self.core.conection_message = ""
checks = '\n'.join(checks)
Dialogs.critical_message(
self.parent_frame, 'Connection error', f"{checks}"
)
self.parent_frame.pushButton_connect.setText('Connect')
self.parent_frame.pushButton_connect.setEnabled(True)
self.parent_frame.pushButton_connect.setChecked(False)
# ----------------------------------------------------------------------
[docs] @Slot()
def disconnected_ok(self) -> None:
"""OpenBCI disconnected."""
self.core.stop_kafka()
if self.parent_frame.checkBox_test_signal.isChecked():
self.parent_frame.groupBox_settings.setEnabled(False)
self.parent_frame.groupBox_leadoff_impedance.setEnabled(False)
self.parent_frame.pushButton_connect.setText('Connect')
self.parent_frame.pushButton_connect.setEnabled(True)
self.parent_frame.pushButton_connect.setChecked(False)
self.core.status_bar(right_message=('Disconnected', None))
self.parent_frame.checkBox_view_impedances.setChecked(False)
self.parent_frame.stackedWidget_montage.setCurrentIndex(0)