class ExportViewTarget(object): def __init__(self, project, filename, parent): self.mutex = QMutex() self.project = project self.filename = filename self.parent = parent self.surface = QOffscreenSurface() self.surface.setFormat(QSurfaceFormat.defaultFormat()) self.surface.create() def destroy(self): self.surface.destroy() def render(self, view, uniforms=dict()): self.project.setCameraMode('view') self.project.setCameraViewRef(view) self.project.setCameraAtOrigin() self.project.render(self.ctx.functions(), view.info.width, view.info.height, uniforms) depth = self.project.renderer.depthmap(self.ctx.functions(), view.info.width, view.info.height) return depth def __enter__(self): self.mutex.lock() self.ctx = QOpenGLContext() self.ctx.setShareContext(self.parent) self.ctx.create() if not self.ctx.makeCurrent(self.surface): raise Exception("cannot make context current") self.project.renderer.lock() def __exit__(self, type, value, tb): self.project.renderer.unlock() self.ctx.doneCurrent() del self.ctx self.mutex.unlock()
class Barrier: """ Implement a barrier, such that threads block until other monitored threads reach a specific location. The barrier can be used multiple times (it is reinitialized after the threads passed). See https://stackoverflow.com/questions/9637374/qt-synchronization-barrier/9639624#9639624 """ def __init__(self, count): self.count = count self.origCount = count self.mutex = QMutex() self.condition = QWaitCondition() def wait(self): """ Wait until all monitored threads called wait. :return: None """ self.mutex.lock() self.count -= 1 if self.count > 0: self.condition.wait(self.mutex) else: self.count = self.origCount self.condition.wakeAll() self.mutex.unlock()
class TestThread(QThread): startRecording = Signal() def __init__(self, config, aruco_tester_widget, acs_control, parent=None): QThread.__init__(self, parent) self.config = config self.aruco_tester_widget = aruco_tester_widget self.image_miner = aruco_tester_widget.image_miner self.acs_control = acs_control self.startRecording.connect(self.aruco_tester_widget.onRecordClicked) self.recordingStopped = QWaitCondition() self.mutex = QMutex() @Slot() def wake(self): self.recordingStopped.wakeAll() pass def run(self): print('starting...') base_dir = os.path.dirname(os.path.realpath(__file__)) config = self.config['config'] angles = self.config['angles'] j = 0 self.image_miner.set_video_capture_property('CAP_PROP_BRIGHTNESS', 50.5/100.) self.image_miner.set_video_capture_property('CAP_PROP_CONTRAST', 50.5/100.) self.image_miner.set_video_capture_property('CAP_PROP_SATURATION', 50.5/100.) prop = 'CAP_PROP_BRIGHTNESS' print(prop) start = config[prop]['start'] end = config[prop]['end'] step = config[prop]['step'] for i in range(start, end+1, step): print(i) self.image_miner.set_video_capture_property(prop, i/100.) series_dir = os.path.join(base_dir, str(prop), str(i), str(self.aruco_tester_widget.use_board)) j += 1 for angle in angles: fileName = os.path.join(series_dir, str(angle)) print(angle) self.acs_control.pa(angle) time.sleep(5) self.mutex.lock() self.startRecording.emit() print('wait for recording to stop') self.recordingStopped.wait(self.mutex) print('saving data to {}'.format(fileName)) self.aruco_tester_widget.broadcaster.saveToFile(fileName) self.mutex.unlock() self.acs_control.pa(0) time.sleep(10)
class SingleRunnableManager(QObject): def __init__(self, thread_pool: QThreadPool): """ Runs a runnable on a thread pool, canceling the previous runnable if it is still enqueued. Parameters ---------- thread_pool The thread pool that will be used to run the runnable. """ super().__init__() self._thread_pool = thread_pool self._current_runnable = None self._lock = QMutex() def remove_runnable(self) -> None: """ Removes the current runnable without canceling its execution. """ self._lock.lock() try: del self._current_runnable self._current_runnable = None gc.collect() finally: self._lock.unlock() def _cancel(self) -> None: if self._current_runnable is not None: self._thread_pool.cancel(self._current_runnable) del self._current_runnable self._current_runnable = None gc.collect() def start(self, runnable: QRunnable) -> None: """ Cancel the current runnable (if any) and enqueue the runnable for execution on the thread pool. Parameters ---------- runnable The runnable to run """ self._lock.lock() try: self._cancel() self._thread_pool.start(runnable) finally: self._lock.unlock()
class QIpScanner(QObject): networkIpReceived = Signal(str, str, str) def __init__(self, parent=None): super().__init__(parent) self.isScanning = False self.worker = _Worker(self.networkIpReceived, self) self.mutex = QMutex() @Slot(result=bool) def isRunning(self): return self.worker.isRunning() @Slot() def scan(self): self.mutex.lock() if not self.worker.isRunning(): self.worker.start() self.mutex.unlock()
class SignalThread(QThread): def __init__(self, parent=None): QThread.__init__(self, parent) self.waitcond = QWaitCondition() self.mutex = QMutex() self.isstopped = False def trigger(self): """lock first to make sure the QThread is actually waiting for a signal""" self.mutex.lock() self.waitcond.wakeOne() self.mutex.unlock() def stopThread(self): self.mutex.lock() self.isstopped = True self.waitcond.wakeOne() self.mutex.unlock() def run(self): self.mutex.lock() while not self.isstopped: # just wait, and trigger every time we receive a signal self.waitcond.wait(self.mutex) if not self.isstopped: self.emit(SIGNAL("triggerSignal()")) self.mutex.unlock()
class DetailViewer(QMainWindow): def __init__(self, parent=None): super(DetailViewer, self).__init__(parent) self.ui = detailViewer_ui.Ui_MainWindow() self.ui.setupUi(self) self.__deviceModel = DeviceModel(self) self.__folderModel = FolderModel(self) self.ui.deviceTreeView.setModel(self.__deviceModel) self.ui.folderTreeView.setModel(self.__folderModel) self.__deviceEventCatched = ServerEventCatcher( [sth.DevicesConfigurationEvent]) self.__deviceEventCatched.event_arrived.connect(self.process_event) self.__folderEventCatched = ServerEventCatcher( [sth.FoldersConfigurationEvent]) self.__folderEventCatched.event_arrived.connect(self.process_event) self.__setterMutex = QMutex() self.__server = None def set_server(self, server: lserver.Server): self.__setterMutex.lock() try: if self.__server is not None: self.__server.eventQueueEater.remove_event_provessor( self.__deviceEventCatched) self.__server.eventQueueEater.remove_event_provessor( self.__folderEventCatched) self.__deviceModel.clear_elements() self.__server = server if self.__server is not None: try: for devid, device in self.__server.syncthingHandler.get_devices( ).result().items(): self.__deviceModel.add_element(device) for fid, folder in self.__server.syncthingHandler.get_folders( ).result().items(): self.__folderModel.add_element(folder) except sth.ConfigNotInSyncError: pass self.__server.eventQueueEater.add_event_processor( self.__deviceEventCatched) self.__server.eventQueueEater.add_event_processor( self.__folderEventCatched) finally: self.__setterMutex.unlock() @Slot(object) def process_event(self, event): """ make sure to :param event: :return: """ print('all is good', event) if isinstance(event, sth.DevicesAddedEvent): for device in event.devices(): self.__deviceModel.add_element(device) elif isinstance(event, sth.DevicesRemovedEvent): for device in event.devices(): self.__deviceModel.remove_element(device) elif isinstance(event, sth.DevicesChangedEvent) or isinstance( event, sth.DevicesVolatileDataChangedEvent): for device in event.devices(): self.__deviceModel.update_element(device) elif isinstance(event, sth.FoldersAddedEvent): for folder in event.folders(): self.__folderModel.add_element(folder) elif isinstance(event, sth.FoldersRemovedEvent): for folder in event.folders(): self.__folderModel.remove_element(folder) elif isinstance(event, sth.FoldersConfigurationChangedEvent) or isinstance( event, sth.FoldersVolatileDataChangedEvent): for folder in event.folders(): self.__folderModel.update_element(folder)
class Client_API(QObject): loggedIn = pyqtSignal(dict) login_failed = pyqtSignal(str, str) registered = pyqtSignal() registration_failed = pyqtSignal(str, str) timeout_error = pyqtSignal() request_timeout = 10 read_timeout = 60 def __init__(self, cfg, tracker=None, server_addr=None, events_server_addr=None, sharing_server_addr=None, upload_server_addr=None, parent=None): logger.debug("Initializing API server client...") QObject.__init__(self, parent=parent) self._sessions = dict() self.server_addr = server_addr if server_addr \ else API_URI.format(cfg.host) self.events_server_addr = events_server_addr if events_server_addr \ else API_EVENTS_URI.format(cfg.host) self.sharing_server_addr = sharing_server_addr if sharing_server_addr \ else API_SHARING_URI.format(cfg.host) self.upload_server_addr = upload_server_addr if upload_server_addr \ else API_UPLOAD_URI.format(cfg.host) self._tracker = tracker self.ip_addr = None self.cfg = cfg self.node_sign = None self._ip_lock = QMutex(parent=self) self._os_name, self._is_server = get_os_name_and_is_server() def emit_loggedIn(self, user_email, password_hash, user_id, node_id, servers, license_type, remote_actions, last_event_uuid): ''' Prepares data and emits loggedIn signal @param server Info on servers as returned by API server [dict] ''' data = { 'user_email': user_email, 'password_hash': password_hash, 'user_hash': self.cfg.user_hash, 'node_hash': self.cfg.node_hash, 'user_id': user_id, 'node_id': node_id, 'servers': servers, 'license_type': license_type, 'remote_actions': remote_actions, 'last_event_uuid': last_event_uuid, } # Emit signal self.loggedIn.emit(data) def signup(self, fullname, email, password): """ Registers new user on the API server @param fullname Users full name [unicode] @param email Users email address [str] @param password Users password [str] """ signed, res = self._signup(fullname, email, password) if not signed: if res and 'errcode' in res and \ res['errcode'] == 'ERROR_NODEHASH_EXIST': self.cfg.set_settings( {'node_hash': sha512(str(uuid4())).hexdigest()}) return self.signup(fullname, email, password) if signed: self.registered.emit() if self._tracker: self._tracker.session_signup() else: error = 'Network Error' if res is None else res['errcode'] self.registration_failed.emit( error, res.get('info', 'Unknown error') if res else 'Unknown error') if self._tracker: self._tracker.session_signup_failed(error) return signed, res def _signup(self, fullname, email, password): if self._tracker: self._tracker.session_signup_start() data = { 'node_devicetype': 'desktop', 'node_ostype': get_platform(), 'node_osname': self._os_name, 'node_name': get_device_name(), 'fullname': fullname, 'user_email': email, 'user_password': password, } signed = False _res = self.create_request(action='signup', data=data) if _res and "success" in _res['result'] and 'user_hash' in _res: self.cfg.set_settings({'user_hash': _res['user_hash']}) signed = True logger.info("Registered successfully") return signed, _res def login(self, login=None, password=None, user_hash=None): """ Authorizes user on the API server @param login Users email address [str] @param password Users password [str] """ logged_in, res = self._login(login, password, user_hash) if logged_in: license_type = res.get('license_type', None) license_type = license_type_constant_from_string(license_type) remote_actions = res.get('remote_actions', None) servers = list(map(self._strip_server_info, res.get('servers', []))) try: logger.verbose("Servers: '%s'", servers) except AttributeError: pass res['servers'] = servers # Signalize on successful logging in self.emit_loggedIn( user_email=login if login else self.cfg.user_email, password_hash=password if password else self.cfg.user_password_hash, user_id=res.get('user_id', None), node_id=res.get('node_id', None), servers=servers, license_type=license_type, remote_actions=remote_actions, last_event_uuid=res.get('last_event_uuid', None)) if self._tracker: self._tracker.session_login(license_type) else: error = 'Network Error' if res is None else res['errcode'] self.login_failed.emit( error, res.get('info', 'Unknown error') if res else 'Unknown error') if self._tracker: self._tracker.session_login_failed(error) return logged_in, res def _login(self, login, password, user_hash): data = { 'node_devicetype': 'desktop', 'node_ostype': get_platform(), 'node_osname': self._os_name, 'node_name': get_device_name(), 'user_email': login, 'user_password': password, 'user_hash': user_hash, 'is_server': self._is_server, } # Signalize login attempt started (for statistics) if self._tracker: self._tracker.session_login_start() logged_in = False _res = self.create_request(action='login', data=data) if _res is None: if self._tracker: self._tracker.session_login_failed("response is None") return logged_in, _res if 'user_hash' not in _res: if self._tracker: self._tracker.session_login_failed("missing user_hash") logger.error("Server not returned user_hash") return logged_in, _res # Registered successfully if "success" in _res['result']: logged_in = True self.cfg.set_settings({'user_hash': _res['user_hash']}) logger.info("Logged in successfully") return logged_in, _res def _strip_server_info(self, server_info): return dict(map(self._strip_info, server_info.items())) def _strip_info(self, item): key, value = item if isinstance(value, str): value = value.strip() return key, value def logout(self): self.create_request(action='logout') def generate_node_sign(self): """ Returns node_sign parameter to be passed to API server @return Node sign value [str] """ s = str(self.cfg.node_hash + str(self.ip_addr)) s = s.encode() s = sha512(s) return s.hexdigest() def update_node_sign(self): self.update_ip() self.node_sign = self.generate_node_sign() def change_password(self, old_password, new_password): data = {'old_password': old_password, 'new_password': new_password} return self.create_request(action='changepassword', data=data) def get_ip(self): data = {'get': 'candidate'} encoded = self.make_json_data(action='stun', data=data) response = self.make_post(url=self.server_addr, data=encoded) try: response = json.loads(response) if response['result'] != 'success': return None except: return None try: return int(response['info']) except Exception as e: logger.error("Invalid response '%s' (%s)", e, response) return None def update_ip(self): self._ip_lock.lock() try: self.ip_addr = self.get_ip() finally: self._ip_lock.unlock() @staticmethod def make_json_data(action, data=()): ''' Retuns complete request data encoded in JSON format @param action Name of request action [str] @param data Request data [dict] @return JSON encoded request data [string] ''' data = {} if not data else data try: encoded = json.dumps({'action': action, 'data': data}) except Exception as e: logger.error( "Failed to encoded request '%s' data %s into JSON format (%s)", action, repr(data), e) raise return encoded def get_request_data(self, action, data=(), force_update=False): """ Add auth data to request data given and return its JSON encoded version @param action Request data 'action' field [string] @param data Request data 'data' field [dict] @return JSON-encoded request data [string] """ data = {} if not data else data if not self.node_sign or force_update: self.update_node_sign() # Add auth data if action not in ('signup', 'login'): data['user_hash'] = self.cfg.user_hash data['node_hash'] = self.cfg.node_hash data['node_sign'] = self.node_sign return self.make_json_data(action=action, data=data) def get_upload_request_data(self, fields, callback, force_update=False): """ Add auth data to upload request fields given and return multipart/form data object @param fields Request data 'fields' [dict] @param callback Callback to be called on file chunk read [Function] @param force_update Instructs to update node sign [bool] @return multipart/form data object [MultipartEncoderMonitor] """ if not self.node_sign or force_update: self.update_node_sign() # Add auth data fields['user_hash'] = self.cfg.user_hash fields['node_hash'] = self.cfg.node_hash fields['node_sign'] = self.node_sign e = encoder.MultipartEncoder(fields) m = encoder.MultipartEncoderMonitor(e, callback) return m def _create_request(self, action, server_addr, data=(), recurse_max=3, enrich_data=True, headers=()): data = {} if not data else data encoded = self.get_request_data(action, data) \ if enrich_data else data try: response = self.make_post(url=server_addr, data=encoded, headers=headers) response = json.loads(response) except Exception as e: logger.error("Server response parsing failed with '%s'", e) if self._tracker: tb = traceback.format_list(traceback.extract_stack()) self._tracker.error(tb, str(e)) return None except: return None if 'result' not in response: if self._tracker: tb = traceback.format_list(traceback.extract_stack()) self._tracker.error(tb, 'Result not in response') if recurse_max > 0: self.update_node_sign() return self._create_request(action=action, server_addr=server_addr, data=data, recurse_max=recurse_max - 1) else: return None if response['result'] == 'error': info = response.get('info', "") errcode = response.get('errcode', "") if self._tracker: tb = traceback.format_list(traceback.extract_stack()) error = 'info: {}, debug: {}'.format(info, response.get('debug', "")) self._tracker.error(tb, error) if info == 'flst' and recurse_max > 0: self.update_node_sign() return self._create_request(action=action, server_addr=server_addr, data=data, recurse_max=recurse_max - 1) if errcode in ('SIGNATURE_INVALID', 'NODE_SIGN_NOT_FOUND') and \ recurse_max > 0: self.update_node_sign() return self._create_request(action=action, server_addr=server_addr, data=data, recurse_max=recurse_max - 1) if errcode == 'TIMEOUT': self.timeout_error.emit() return None else: return response return response def create_request(self, action, data=(), recurse_max=5): return self._create_request(action, self.server_addr, data, recurse_max) def create_event_request(self, action, data=(), recurse_max=3): return self._create_request(action, self.events_server_addr, data, recurse_max) def create_sharing_request(self, action, data=(), recurse_max=3): return self._create_request(action, self.sharing_server_addr, data, recurse_max) def create_upload_request(self, data, recurse_max=3): headers = {'Content-Type': data.content_type} return self._create_request("", self.upload_server_addr, data, recurse_max, enrich_data=False, headers=headers) def make_post(self, url, data, headers=()): try: logger.verbose("Sending POST request to '%s' with data '%s'", url, data) except AttributeError: pass session = self._get_or_create_session(current_thread()) try: kwargs = dict(timeout=(self.request_timeout, self.read_timeout)) if headers: kwargs['headers'] = headers _res = session.post(url, data, **kwargs).text try: logger.verbose("Server replied: '%s'", _res) except AttributeError: pass except exceptions.Timeout as e: logger.error("Request failed due to timeout") _res = '{"result":"error","errcode":"TIMEOUT"}' if self._tracker: tb = traceback.format_list(traceback.extract_stack()) self._tracker.error(tb, str(e)) except Exception as e: logger.error("Request failed due to %s", e) _res = 'failure' if self._tracker: tb = traceback.format_list(traceback.extract_stack()) self._tracker.error(tb, str(e)) return _res def _get_or_create_session(self, thread): session = self._sessions.get(thread, None) if not session: session = Session() if self.cfg.host == REGULAR_URI: session.mount(self.cfg.host, SslPinningAdapter()) self._sessions[thread] = session return session def sharing_enable(self, uuid, share_ttl): """ 'sharing_enable' request of share registration API. Registers file or folder sharing on API server @param uuid [str] @param share_ttl Time sharing be valid (in seconds) [int] @return (share_link [str], share_hash [str]) @raise Client_APIError """ data = { 'uuid': uuid, 'share_ttl': str(share_ttl), 'share_password': None } response = self.create_sharing_request(action='sharing_enable', data=data) if response \ and 'result' in response \ and 'success' in response['result'] \ and 'data' in response \ and 'share_link' in response['data'] \ and 'share_hash' in response['data']: share_link = str(response['data']['share_link']) share_hash = str(response['data']['share_hash']) return share_link, share_hash, '' elif response \ and 'result' in response \ and 'error' in response['result']: error_info = response.get('info', '') logger.error("Sharing failed, server response: '%s'", response) return None, None, error_info else: logger.error("Sharing failed, server response: '%s'", response) raise Client_APIError("Sharing failed", response) def sharing_disable(self, uuid): """ 'sharing_disable' request of share registration API. Registers file folder sharing cancelling on API server @param uuid [str] @raise Client_APIError """ data = { 'uuid': uuid, } response = self.create_sharing_request(action='sharing_disable', data=data) if not (response and 'result' in response and 'success' in response['result']): raise Client_APIError("Sharing failed", response) def file_event_create(self, event_uuid, file_name, file_size, folder_uuid, diff_file_size, file_hash): ''' 'file_event_create' request of file event registration API. Registers file creation on API server @return Server reply in the form {'result': status, 'info': server_message, data: useful_data} ''' data = { "event_uuid": event_uuid, "file_name": ensure_unicode(file_name), "file_size": file_size, "folder_uuid": folder_uuid if folder_uuid else "", "diff_file_size": diff_file_size, "hash": file_hash, } return self.create_event_request(action='file_event_create', data=data) def file_event_update(self, event_uuid, file_uuid, file_size, last_event_id, diff_file_size, rev_diff_file_size, file_hash): ''' 'file_event_update' request of file event registration API. Registers file update on API server @param file_uuid UUID of file assigned on event creation [string] @param last_event_id Maximum ID of file event on the file being processed known to the node @return Server reply in the form {'result': status, 'info': server_message, data: useful_data} ''' data = { "event_uuid": event_uuid, "file_uuid": file_uuid, "file_size": file_size, "last_event_id": str(last_event_id), "diff_file_size": diff_file_size, "rev_diff_file_size": rev_diff_file_size, "hash": file_hash, } return self.create_event_request(action='file_event_update', data=data) def file_event_delete(self, event_uuid, file_uuid, last_event_id): ''' 'file_event_delete' request of file event registration API. Registers file deletion on API server @param file_uuid UUID of file assigned on event creation [string] @param last_event_id Maximum ID of file event on the file being processed known to the node @return Server reply in the form {'result': status, 'info': server_message, data: useful_data} ''' data = { "event_uuid": event_uuid, "file_uuid": file_uuid, "last_event_id": str(last_event_id) } return self.create_event_request(action='file_event_delete', data=data) def file_event_move(self, event_uuid, file_uuid, last_event_id, new_file_name, new_folder_uuid): ''' 'file_event_move' request of file event registration API. Registers file moving on API server @param file_uuid UUID of file assigned on event creation [string] @param last_event_id Maximum ID of file event on the file being processed known to the node @param new_file_name New name of file [unicode] @param new_folder_uuid uuid of a folder where file will be placed @return Server reply in the form {'result': status, 'info': server_message, data: useful_data} ''' data = { "event_uuid": event_uuid, "file_uuid": file_uuid, "new_folder_uuid": new_folder_uuid if new_folder_uuid else "", "new_file_name": new_file_name, "last_event_id": str(last_event_id) } return self.create_event_request(action='file_event_move', data=data) def folder_event_create(self, event_uuid, folder_name, parent_folder_uuid): if parent_folder_uuid is None: parent_folder_uuid = "" data = { "event_uuid": event_uuid, "folder_name": folder_name, "parent_folder_uuid": parent_folder_uuid, } return self.create_event_request(action='folder_event_create', data=data) def folder_event_move(self, event_uuid, folder_uuid, last_event_id, new_folder_name, new_parent_folder_uuid): if new_parent_folder_uuid is None: new_parent_folder_uuid = "" data = { "event_uuid": event_uuid, "folder_uuid": folder_uuid, "new_folder_name": new_folder_name, "new_parent_folder_uuid": new_parent_folder_uuid, "last_event_id": str(last_event_id), } return self.create_event_request(action='folder_event_move', data=data) def folder_event_delete(self, event_uuid, folder_uuid, last_event_id): data = { "event_uuid": event_uuid, "folder_uuid": folder_uuid, "last_event_id": str(last_event_id), } return self.create_event_request(action='folder_event_delete', data=data) def file_event_get_filelist(self, last_event_id): ''' 'file_list' request of file event registration API. Obtains list of files created/updated after event with given ID @param last_event_id Maximum ID of file event known to the node @return Server reply in the form {'result': status, 'info': server_message, data: useful_data} ''' data = {"last_event_id": str(last_event_id)} return self.create_event_request(action='file_list', data=data) def file_event_get_events(self, last_event_id): ''' 'file_events' request of file event registration API. Obtains events registered on server after event with given ID @param last_event_id Maximum ID of file event known to the node @return Server reply in the form {'result': status, 'info': server_message, data: useful_data} ''' data = {"last_event_id": str(last_event_id)} return self.create_event_request(action='file_events', data=data) def start_http_download(self, upload_id): """ 'download' request for starting of uploaded file download via HTTP protocol. After node auth checking the server should return redirect to actual file URL @param upload_id ID of uploaded file [int] """ data = {"upload_id": str(upload_id)} return self.create_event_request(action='download', data=data) def remote_action_done(self, remote_action_uuid): return self.create_request(action='remote_action_done', data=dict(action_uuid=remote_action_uuid)) def patch_ready(self, patch_uuid, patch_size): return self.create_event_request(action='patch_ready', data=dict(diff_uuid=patch_uuid, diff_size=patch_size)) def get_token_login_link(self): return self.create_request(action='get_token_login_link') def node_management_action(self, action, node_id, action_type=""): if action_type: data = dict(target_node_id=node_id, action_type=action_type) else: data = dict(node_id=node_id) return self.create_request(action=action, data=data) def get_notifications(self, limit, from_id): data = {'from': from_id, 'limit': limit} return self.create_request(action="getNotifications", data=data) def accept_invitation(self, colleague_id): data = dict(colleague_id=colleague_id) return self.create_sharing_request(action="collaboration_join", data=data) def set_uris(self): self.server_addr = API_URI.format(self.cfg.host) self.events_server_addr = API_EVENTS_URI.format(self.cfg.host) self.sharing_server_addr = API_SHARING_URI.format(self.cfg.host) self.upload_server_addr = API_UPLOAD_URI.format(self.cfg.host) # Collaboration settings requests def collaboration_info(self, uuid): data = dict(uuid=uuid) return self.create_sharing_request(action="collaboration_info", data=data) def colleague_delete(self, uuid, colleague_id): data = dict(uuid=uuid, colleague_id=colleague_id) return self.create_sharing_request(action="colleague_delete", data=data) def colleague_edit(self, uuid, colleague_id, access_type): data = dict(uuid=uuid, colleague_id=colleague_id, access_type=access_type) return self.create_sharing_request(action="colleague_edit", data=data) def colleague_add(self, uuid, colleague_email, access_type): data = dict(uuid=uuid, colleague_email=colleague_email, access_type=access_type) return self.create_sharing_request(action="colleague_add", data=data) def collaboration_cancel(self, uuid): data = dict(uuid=uuid) return self.create_sharing_request(action="collaboration_cancel", data=data) def collaboration_leave(self, uuid): data = dict(uuid=uuid) return self.create_sharing_request(action="collaboration_leave", data=data) # Support requests def send_support_message(self, subject, body, log_file_name): data = dict(subject=subject, body=body) if log_file_name: data['log_file_name'] = log_file_name return self.create_request(action="support", data=data) def upload_file(self, path, mime_type, callback=lambda m: None): filename = op.basename(path) with open(path, 'rb') as f: fields = {'UploadLogsForm[uploadedFile]': (filename, f, mime_type)} data = self.get_upload_request_data(fields, callback) return self.create_upload_request(data)
class MainWindow(QMainWindow): def __init__(self): super().__init__() self.__activeCount: int = 0 # number of operations in progress centralWidget = MainWidget() self.setCentralWidget(centralWidget) self.notifier = None # Set by main script # Initialise a thread pool self.threadPool = QThreadPool.globalInstance() logging.info('Multithreading with maximum {} threads'.format( self.threadPool.maxThreadCount())) self.setWindowTitle('dataMole') self.setUpMenus() self.__spinnerMutex = QMutex() centralWidget.frameInfoPanel.operationRequest.connect( self.executeOperation) centralWidget.workbenchView.selectedRowChanged[str, str].connect( self.changedSelectedFrame) def moveEvent(self, event: QtGui.QMoveEvent) -> None: self.notifier.mNotifier.updatePosition() super().moveEvent(event) def resizeEvent(self, event: QtGui.QResizeEvent) -> None: self.notifier.mNotifier.updatePosition() super().resizeEvent(event) @Slot() def openComparePanel(self) -> None: w = self.centralWidget().workbenchModel dv = DataframeSideBySideView(self) dv.setWindowFlags(Qt.Window) dv.setAttribute(Qt.WA_DeleteOnClose) dv.setWindowTitle('Side by side view') dv.dataWidgetL.setWorkbench(w) dv.dataWidgetR.setWorkbench(w) if w.rowCount(): dv.dataWidgetL.setDataframe(dv.dataWidgetL.inputCB.currentText()) dv.dataWidgetR.setDataframe(dv.dataWidgetR.inputCB.currentText()) dv.show() # @Slot() # def openDiffPanel(self) -> None: # TODO: remove this? # dv = DiffDataframeWidget(self) # dv.setWindowFlags(Qt.Window) # dv.setAttribute(Qt.WA_DeleteOnClose) # dv.setWindowTitle('Diff view') # dv.setWorkbench(self.centralWidget().workbenchModel) # dv.show() @Slot(str, str) def changedSelectedFrame(self, newName: str, _: str) -> None: # Slot called when workbench selection change self.aWriteCsv.setOperationArgs(w=self.centralWidget().workbenchModel, frameName=newName) self.aWritePickle.setOperationArgs( w=self.centralWidget().workbenchModel, frameName=newName) @Slot(int, str) def operationStateChanged(self, uid: int, state: str) -> None: if state == 'success': logging.info('Operation uid={:d} succeeded'.format(uid)) self.statusBar().showMessage('Operation succeeded', 10000) elif state == 'error': logging.error('Operation uid={:d} stopped with errors'.format(uid)) self.statusBar().showMessage('Operation stopped with errors', 10000) elif state == 'start': self.__spinnerMutex.lock() self.__activeCount += 1 self.__spinnerMutex.unlock() self.statusBar().startSpinner() logging.info('Operation uid={:d} started'.format(uid)) self.statusBar().showMessage('Executing...', 10000) elif state == 'finish': logging.info('Operation uid={:d} finished'.format(uid)) self.__spinnerMutex.lock() self.__activeCount -= 1 if self.__activeCount == 0: self.statusBar().stopSpinner() self.__spinnerMutex.unlock() # print('Emit', uid, state, 'count={}'.format(self.__activeCount)) @Slot(type) def executeOperation(self, opType: type) -> None: action = OperationAction(opType, self, opType.name(), self.rect().center(), self.centralWidget().workbenchModel) # Set selected frame in the input combo box of the action selection = self.centralWidget().workbenchView.selectedIndexes() if selection: selectedFrame: str = selection[0].data(Qt.DisplayRole) action.setSelectedFrame(selectedFrame) # Delete action when finished action.stateChanged.connect(self.operationStateChanged) action.stateChanged.connect(self.deleteAction) # Start operation action.trigger() @Slot(int, str) def deleteAction(self, uid: int, state: str): if state == 'finish': action: QAction = self.sender() action.deleteLater() logging.info( 'Action for operation uid={:d} scheduled for deletion'.format( uid)) def setUpMenus(self) -> None: menuBar = QMenuBar() fileMenu = menuBar.addMenu('File') importMenu = fileMenu.addMenu('Import') exportMenu = fileMenu.addMenu('Export') flowMenu = menuBar.addMenu('Flow') viewMenu = menuBar.addMenu('View') helpMenu = menuBar.addMenu('Help') aAppendEmpty = QAction('Add frame', fileMenu) aAppendEmpty.setStatusTip('Create an empty dataframe in the workbench') aQuit = QAction('Quit', fileMenu) aLoadCsv = OperationAction(CsvLoader, fileMenu, 'From csv', self.rect().center(), self.centralWidget().workbenchModel) self.aWriteCsv = OperationAction(CsvWriter, fileMenu, 'To csv', self.rect().center(), w=self.centralWidget().workbenchModel) aLoadPickle = OperationAction(PickleLoader, fileMenu, 'From pickle', self.rect().center(), self.centralWidget().workbenchModel) self.aWritePickle = OperationAction( PickleWriter, fileMenu, 'To pickle', self.mapToGlobal(self.rect().center()), w=self.centralWidget().workbenchModel) aCompareFrames = QAction('Compare dataframes', viewMenu) aLogDir = QAction('Open log directory', helpMenu) aClearLogs = QAction('Delete old logs', helpMenu) fileMenu.addActions([aAppendEmpty, aQuit]) exportMenu.addActions([self.aWriteCsv, self.aWritePickle]) importMenu.addActions([aLoadCsv, aLoadPickle]) self._aStartFlow = QAction('Execute', flowMenu) self._aResetFlow = QAction('Reset', flowMenu) aSaveFlow = QAction('Save', flowMenu) aLoadFlow = QAction('Load', flowMenu) flowMenu.addActions( [self._aStartFlow, self._aResetFlow, aSaveFlow, aLoadFlow]) viewMenu.addAction(aCompareFrames) helpMenu.addActions([aLogDir, aClearLogs]) self.setMenuBar(menuBar) # Tips aLoadCsv.setStatusTip('Load a csv file in the workbench') aLoadPickle.setStatusTip('Load a Pickle file in the workbench') self.aWriteCsv.setStatusTip('Write a dataframe to a csv file') self.aWritePickle.setStatusTip( 'Serializes a dataframe into a pickle file') aCompareFrames.setStatusTip('Open two dataframes side by side') self._aStartFlow.setStatusTip('Start flow-graph execution') self._aResetFlow.setStatusTip('Reset the node status in flow-graph') aLogDir.setStatusTip('Open the folder containing all logs') aClearLogs.setStatusTip('Delete older logs and keep the last 5') # Connect aAppendEmpty.triggered.connect( self.centralWidget().workbenchModel.appendEmptyRow) aQuit.triggered.connect(self.close) self._aStartFlow.triggered.connect( self.centralWidget().controller.executeFlow) self._aResetFlow.triggered.connect( self.centralWidget().controller.resetFlowStatus) aCompareFrames.triggered.connect(self.openComparePanel) aLogDir.triggered.connect(self.openLogDirectory) aClearLogs.triggered.connect(self.clearLogDir) aSaveFlow.triggered.connect(self.saveFlow) aLoadFlow.triggered.connect(self.readFlow) aLoadCsv.stateChanged.connect(self.operationStateChanged) aLoadPickle.stateChanged.connect(self.operationStateChanged) self.aWriteCsv.stateChanged.connect(self.operationStateChanged) self.aWritePickle.stateChanged.connect(self.operationStateChanged) @Slot() def openLogDirectory(self) -> None: QDesktopServices.openUrl( QUrl(os.path.join(os.getcwd(), flogging.LOG_FOLDER))) @Slot() def clearLogDir(self) -> None: flogging.deleteOldLogs() gui.statusBar.showMessage('Logs cleared') @Slot() def saveFlow(self): """ Dump a pipeline in a pickle file """ path, ext = QFileDialog.getSaveFileName(self, 'Save flow graph') if path: path = getFileNameWithExtension(path, ext) serialization = self.centralWidget().graph.serialize() # Add info about scene position to node data nodeSerialization: Dict[int, Dict] = serialization['nodes'] for nodeId, data in nodeSerialization.items(): data['pos'] = self.centralWidget( ).graphScene.nodesDict[nodeId].scenePos().toTuple() with open(path, 'wb') as file: pickle.dump(serialization, file) gui.statusBar.showMessage( 'Pipeline was successfully exported in {:s}'.format(path), 15) @Slot() def readFlow(self): """ Load a pipeline from a pickle file """ path, ext = QFileDialog.getOpenFileName( self, 'Open flow graph', filter='Pickle (*.pickle);;All files (*)') if path: try: with open(path, 'rb') as file: serialization: Dict = pickle.load(file) graph = flow.dag.OperationDag.deserialize(serialization) except pickle.PickleError as e: gui.notifier.addMessage('Pickle error', str(e), QMessageBox.Critical) except exc.DagException as e: gui.notifier.addMessage('Error while creating pipeline', str(e), QMessageBox.Critical) else: # Add the workbench g = graph.getNxGraph() for nodeId in g.nodes: g.nodes[nodeId][ 'op'].operation._workbench = self.centralWidget( ).workbenchModel self.centralWidget().createNewFlow(graph) self.centralWidget().controller.showGraphInScene() # Set position of every node nodeDict = serialization['nodes'] for nodeId, node in self.centralWidget( ).graphScene.nodesDict.items(): pos: Tuple[float, float] = nodeDict[nodeId]['pos'] node.setPos(*pos) node.refresh() # Update connected edges # Reconnect actions to the new controller self._aStartFlow.triggered.connect( self.centralWidget().controller.executeFlow) self._aResetFlow.triggered.connect( self.centralWidget().controller.resetFlowStatus) gui.statusBar.showMessage('Pipeline was successfully imported', 15)
class FrameModel(QAbstractTableModel): """ Table model for a single dataframe """ # This role returns frame header data as a tuple (Name, Type) DataRole = Qt.UserRole statisticsComputed = Signal(tuple) statisticsError = Signal(tuple) def __init__(self, parent: QWidget = None, frame: Union[Frame, Shape] = Frame()): super().__init__(parent) if isinstance(frame, Frame): self.__frame: Frame = frame self.__shape: Shape = self.__frame.shape elif isinstance(frame, Shape): # it's a Shape self.__frame: Frame = Frame() self.__shape: Shape = frame else: self.__frame: Frame = Frame() self.__shape: Shape = Shape() # Dictionary { attributeIndex: value } self._statistics: Dict[int, Dict[str, object]] = dict() self._histogram: Dict[int, Dict[Any, int]] = dict() # Dataframe name self.name: str = '' # Set of alive workers by identifier (attribute number, type, operation) self._runningWorkers: Set[Tuple] = set() self._dataAccessMutex = QMutex() @property def frame(self) -> Frame: return self.__frame @property def shape(self) -> Shape: return self.__shape def setFrame(self, frame: Frame) -> None: self.beginResetModel() self.__frame = frame self.__shape: Shape = self.__frame.shape self._statistics = dict() self._histogram = dict() self.endResetModel() def rowCount(self, parent: QModelIndex = QModelIndex()) -> int: if parent.isValid(): return 0 return self.frame.nRows # True number of rows def columnCount(self, parent: QModelIndex = QModelIndex()) -> int: if parent.isValid(): return 0 return self.shape.nColumns def data(self, index: QModelIndex, role: int = ...) -> Any: if index.isValid(): if role == Qt.DisplayRole: return str(self.__frame.getRawFrame().iloc[index.row(), index.column()]) return None def headerData(self, section: int, orientation: Qt.Orientation, role: int = Qt.DisplayRole) -> Any: if orientation == Qt.Horizontal: if role == Qt.DisplayRole: return self.__shape.colNames[ section] + '\n' + self.__shape.colTypes[section].name elif role == FrameModel.DataRole: return self.__shape.colNames[section], self.__shape.colTypes[ section] # Vertical header shows row number elif orientation == Qt.Vertical and role == Qt.DisplayRole: return section return None def setHeaderData(self, section: int, orientation: Qt.Orientation, value: Any, role: int = ...) \ -> bool: """ Change column name """ if orientation == Qt.Horizontal and role == Qt.EditRole and section < self.columnCount( ): names = self.__frame.colnames names[section] = value self.__frame = self.__frame.rename({ self.headerData(section, orientation, FrameModel.DataRole)[0]: value }) self.__shape.colNames[section] = value self.headerDataChanged.emit(orientation, section, section) return True return False @property def statistics(self) -> Dict[int, Dict[str, object]]: return self._statistics @property def histogram(self) -> Dict[int, Dict[Any, int]]: return self._histogram def existsRunningTask(self, identifier) -> bool: # Check if a task is already running for this attribute exists: bool = False if identifier in self._runningWorkers: exists = True return exists def computeStatistics(self, attribute: int) -> None: """ Compute statistics for a given attribute """ flogging.appLogger.debug( 'computeStatistics() called, attribute {:d}'.format(attribute)) attType = self.__shape.colTypes[attribute] identifier = (attribute, attType, 'stat') if self.__frame.nRows == 0: return self.onWorkerError((attribute, attType, 'stat'), tuple()) # Check if a task is already running for this attribute if self.existsRunningTask(identifier): return # Create a new task stats = AttributeStatistics() stats.setOptions(attribute=attribute) statWorker = Worker(stats, args=(self.__frame, ), identifier=identifier) rc = statWorker.signals.result.connect(self.onWorkerSuccess, Qt.DirectConnection) ec = statWorker.signals.error.connect(self.onWorkerError, Qt.DirectConnection) fc = statWorker.signals.finished.connect(self.onWorkerFinished, Qt.DirectConnection) flogging.appLogger.debug( 'Connected stat worker: {:b}, {:b}, {:b}'.format(rc, ec, fc)) # Remember which computations are already in progress self._runningWorkers.add(identifier) QThreadPool.globalInstance().start(statWorker) def computeHistogram(self, attribute: int, histBins: int) -> None: flogging.appLogger.debug( 'computeHistogram() called, attribute {:d}'.format(attribute)) attType = self.__shape.colTypes[attribute] identifier = (attribute, attType, 'hist') if self.__frame.nRows == 0: return self.onWorkerError((attribute, attType, 'hist'), tuple()) # Check if a task is already running for this attribute if self.existsRunningTask(identifier): return # Create a new task hist = Hist() hist.setOptions(attribute=attribute, attType=attType, bins=histBins) histWorker = Worker(hist, args=(self.__frame, ), identifier=identifier) rc = histWorker.signals.result.connect(self.onWorkerSuccess, Qt.DirectConnection) ec = histWorker.signals.error.connect(self.onWorkerError, Qt.DirectConnection) fc = histWorker.signals.finished.connect(self.onWorkerFinished, Qt.DirectConnection) flogging.appLogger.debug( 'Connected hist worker: {:b}, {:b}, {:b}'.format(rc, ec, fc)) # Remember computations in progress self._runningWorkers.add(identifier) QThreadPool.globalInstance().start(histWorker) @Slot(object, object) def onWorkerSuccess(self, identifier: Tuple[int, Type, str], result: Dict[Any, Any]) -> None: attribute, attType, mode = identifier if mode == 'stat': self._dataAccessMutex.lock() self._statistics[attribute] = result self._dataAccessMutex.unlock() flogging.appLogger.debug('Statistics computation succeeded') elif mode == 'hist': self._dataAccessMutex.lock() self._histogram[attribute] = result self._dataAccessMutex.unlock() flogging.appLogger.debug('Histogram computation succeeded') self.statisticsComputed.emit(identifier) @Slot(object, tuple) def onWorkerError(self, identifier: Tuple[int, Type, str], error: Tuple[type, Exception, str]) -> None: if error: flogging.appLogger.error( 'Statistics computation (mode "{}") failed with {}: {}\n{}'. format(identifier[2], *error)) self.statisticsError.emit(identifier) @Slot(object) def onWorkerFinished(self, identifier: Tuple[int, Type, str]) -> None: flogging.appLogger.debug('Worker (mode "{}") finished'.format( identifier[2])) self._runningWorkers.remove(identifier)
class RenderThread(QThread): ColormapSize = 512 renderedImage = Signal(QImage, float) def __init__(self, parent=None): super(RenderThread, self).__init__(parent) self.mutex = QMutex() self.condition = QWaitCondition() self.centerX = 0.0 self.centerY = 0.0 self.scaleFactor = 0.0 self.resultSize = QSize() self.colormap = [] self.restart = False self.abort = False for i in range(RenderThread.ColormapSize): self.colormap.append( self.rgbFromWaveLength(380.0 + (i * 400.0 / RenderThread.ColormapSize))) def stop(self): self.mutex.lock() self.abort = True self.condition.wakeOne() self.mutex.unlock() self.wait(2000) def render(self, centerX, centerY, scaleFactor, resultSize): locker = QMutexLocker(self.mutex) self.centerX = centerX self.centerY = centerY self.scaleFactor = scaleFactor self.resultSize = resultSize if not self.isRunning(): self.start(QThread.LowPriority) else: self.restart = True self.condition.wakeOne() def run(self): while True: self.mutex.lock() resultSize = self.resultSize scaleFactor = self.scaleFactor centerX = self.centerX centerY = self.centerY self.mutex.unlock() halfWidth = resultSize.width() // 2 halfHeight = resultSize.height() // 2 image = QImage(resultSize, QImage.Format_RGB32) NumPasses = 8 curpass = 0 while curpass < NumPasses: MaxIterations = (1 << (2 * curpass + 6)) + 32 Limit = 4 allBlack = True for y in range(-halfHeight, halfHeight): if self.restart: break if self.abort: return ay = 1j * (centerY + (y * scaleFactor)) for x in range(-halfWidth, halfWidth): c0 = centerX + (x * scaleFactor) + ay c = c0 numIterations = 0 while numIterations < MaxIterations: numIterations += 1 c = c * c + c0 if abs(c) >= Limit: break numIterations += 1 c = c * c + c0 if abs(c) >= Limit: break numIterations += 1 c = c * c + c0 if abs(c) >= Limit: break numIterations += 1 c = c * c + c0 if abs(c) >= Limit: break if numIterations < MaxIterations: image.setPixel( x + halfWidth, y + halfHeight, self.colormap[numIterations % RenderThread.ColormapSize]) allBlack = False else: image.setPixel(x + halfWidth, y + halfHeight, qRgb(0, 0, 0)) if allBlack and curpass == 0: curpass = 4 else: if not self.restart: self.renderedImage.emit(image, scaleFactor) curpass += 1 self.mutex.lock() if not self.restart: self.condition.wait(self.mutex) self.restart = False self.mutex.unlock() def rgbFromWaveLength(self, wave): r = 0.0 g = 0.0 b = 0.0 if wave >= 380.0 and wave <= 440.0: r = -1.0 * (wave - 440.0) / (440.0 - 380.0) b = 1.0 elif wave >= 440.0 and wave <= 490.0: g = (wave - 440.0) / (490.0 - 440.0) b = 1.0 elif wave >= 490.0 and wave <= 510.0: g = 1.0 b = -1.0 * (wave - 510.0) / (510.0 - 490.0) elif wave >= 510.0 and wave <= 580.0: r = (wave - 510.0) / (580.0 - 510.0) g = 1.0 elif wave >= 580.0 and wave <= 645.0: r = 1.0 g = -1.0 * (wave - 645.0) / (645.0 - 580.0) elif wave >= 645.0 and wave <= 780.0: r = 1.0 s = 1.0 if wave > 700.0: s = 0.3 + 0.7 * (780.0 - wave) / (780.0 - 700.0) elif wave < 420.0: s = 0.3 + 0.7 * (wave - 380.0) / (420.0 - 380.0) r = pow(r * s, 0.8) g = pow(g * s, 0.8) b = pow(b * s, 0.8) return qRgb(r * 255, g * 255, b * 255)
class SerialPortThread(QThread): on_data = Signal(str) on_error = Signal(str) on_stop = Signal() def __init__(self): super(SerialPortThread, self).__init__() self.device_path: str = '' self.commands_mutex = QMutex() self.commands: List[bytes] = [] def run(self): print(self.device_path) try: s = serial.Serial(self.device_path, 115200, timeout=0.1) while s.isOpen(): d = s.readline() try: if d is not bytes(): print(d) self.on_data.emit(d.decode('ascii')) self.__send_command(s) except: print('Error') except (SerialException) as e: self.on_error.emit(f'Serial error {e}') except (UnicodeDecodeError) as e: self.on_error.emit(f'Unicode error {e}, {traceback.format_exc()}') self.on_stop.emit() def __send_command(self, serial_port): self.commands_mutex.lock() if len(self.commands) > 0: command = self.commands[0] rest_commands = self.commands[1:] print(f'send command: {command}') print(f'rest commands: {rest_commands}') self.commands = rest_commands print(f'1') serial_port.write(command) print(f'2') self.commands_mutex.unlock() @Slot() def abort(self): pass @Slot() def select_port(self, device_path: str): self.device_path = device_path @Slot() def send_command(self, command: bytes): self.commands_mutex.lock() print(f'Send command: {command}') self.commands += [command] self.commands_mutex.unlock()
class FortuneThread(QThread): newFortune = Signal(str) error = Signal(int, str) def __init__(self, parent=None): super(FortuneThread, self).__init__(parent) self.quit = False self.hostName = '' self.cond = QWaitCondition() self.mutex = QMutex() self.port = 0 def __del__(self): self.mutex.lock() self.quit = True self.cond.wakeOne() self.mutex.unlock() self.wait() def requestNewFortune(self, hostname, port): locker = QMutexLocker(self.mutex) self.hostName = hostname self.port = port if not self.isRunning(): self.start() else: self.cond.wakeOne() def run(self): self.mutex.lock() serverName = self.hostName serverPort = self.port self.mutex.unlock() while not self.quit: Timeout = 5 * 1000 socket = QTcpSocket() socket.connectToHost(serverName, serverPort) if not socket.waitForConnected(Timeout): self.error.emit(socket.error(), socket.errorString()) return while socket.bytesAvailable() < 2: if not socket.waitForReadyRead(Timeout): self.error.emit(socket.error(), socket.errorString()) return instr = QDataStream(socket) instr.setVersion(QDataStream.Qt_4_0) blockSize = instr.readUInt16() while socket.bytesAvailable() < blockSize: if not socket.waitForReadyRead(Timeout): self.error.emit(socket.error(), socket.errorString()) return self.mutex.lock() fortune = instr.readQString() self.newFortune.emit(fortune) self.cond.wait(self.mutex) serverName = self.hostName serverPort = self.port self.mutex.unlock()
class Motor: """ Class for a motor which has a position, a name and a status """ def __init__(self, mainWindow=None, name="", goalPosition=0, status=False): """ Initialization :param mainWindow: The main window of the ui :param name: The name of the motor :param pos: The position of the motor :param status: the status of the motor """ ## The main window of the ui self.__name = name ## The goal position of the motor self.__goalPosition = goalPosition ## The current position of the motor self.__currentPosition = 0 ## The status of the motor self.__status = status ## The main window of the ui self.__window = mainWindow self.mu = QMutex() def setGoalPosition(self, pos): """ Setter of the goal positon :param pos: the position :return: No return """ self.mu.lock() self.__goalPosition = pos self.mu.unlock() print("%s: %d" % (self.__name, pos)) self.__window.sendMessage('a') def getGoalPosition(self): """ Accessor of the goal position :return: The goal position of the motor """ self.mu.lock() pos = self.__goalPosition self.mu.unlock() return pos def setCurrentPosition(self, pos): """ Setter of the current position :return: No return """ self.mu.lock() self.__currentPosition = pos self.mu.unlock() def getCurrentPosition(self): """ Accessor of the current position :return: The current position of the motor """ self.mu.lock() pos = self.__currentPosition self.mu.unlock() return pos def setName(self, name): """ Setter of the name of the motor :param name: The name of the motor :return: No return """ self.__name = name def getName(self): """ Accessor of the name :return: The name of the motor """ return self.__name def setStatus(self, status): """ Setter of the status of the motor :param status: The status :return: No return """ self.__status = status def isEnabled(self): """ Accessor of the status :return: The status """ return self.__status
class RenderThread(QThread): ColormapSize = 512 renderedImage = Signal(QImage, float) def __init__(self, parent=None): super(RenderThread, self).__init__(parent) self.mutex = QMutex() self.condition = QWaitCondition() self.centerX = 0.0 self.centerY = 0.0 self.scaleFactor = 0.0 self.resultSize = QSize() self.colormap = [] self.restart = False self.abort = False for i in range(RenderThread.ColormapSize): self.colormap.append(self.rgbFromWaveLength(380.0 + (i * 400.0 / RenderThread.ColormapSize))) def stop(self): self.mutex.lock() self.abort = True self.condition.wakeOne() self.mutex.unlock() self.wait(2000) def render(self, centerX, centerY, scaleFactor, resultSize): locker = QMutexLocker(self.mutex) self.centerX = centerX self.centerY = centerY self.scaleFactor = scaleFactor self.resultSize = resultSize if not self.isRunning(): self.start(QThread.LowPriority) else: self.restart = True self.condition.wakeOne() def run(self): while True: self.mutex.lock() resultSize = self.resultSize scaleFactor = self.scaleFactor centerX = self.centerX centerY = self.centerY self.mutex.unlock() halfWidth = resultSize.width() // 2 halfHeight = resultSize.height() // 2 image = QImage(resultSize, QImage.Format_RGB32) NumPasses = 8 curpass = 0 while curpass < NumPasses: MaxIterations = (1 << (2 * curpass + 6)) + 32 Limit = 4 allBlack = True for y in range(-halfHeight, halfHeight): if self.restart: break if self.abort: return ay = 1j * (centerY + (y * scaleFactor)) for x in range(-halfWidth, halfWidth): c0 = centerX + (x * scaleFactor) + ay c = c0 numIterations = 0 while numIterations < MaxIterations: numIterations += 1 c = c*c + c0 if abs(c) >= Limit: break numIterations += 1 c = c*c + c0 if abs(c) >= Limit: break numIterations += 1 c = c*c + c0 if abs(c) >= Limit: break numIterations += 1 c = c*c + c0 if abs(c) >= Limit: break if numIterations < MaxIterations: image.setPixel(x + halfWidth, y + halfHeight, self.colormap[numIterations % RenderThread.ColormapSize]) allBlack = False else: image.setPixel(x + halfWidth, y + halfHeight, qRgb(0, 0, 0)) if allBlack and curpass == 0: curpass = 4 else: if not self.restart: self.renderedImage.emit(image, scaleFactor) curpass += 1 self.mutex.lock() if not self.restart: self.condition.wait(self.mutex) self.restart = False self.mutex.unlock() def rgbFromWaveLength(self, wave): r = 0.0 g = 0.0 b = 0.0 if wave >= 380.0 and wave <= 440.0: r = -1.0 * (wave - 440.0) / (440.0 - 380.0) b = 1.0 elif wave >= 440.0 and wave <= 490.0: g = (wave - 440.0) / (490.0 - 440.0) b = 1.0 elif wave >= 490.0 and wave <= 510.0: g = 1.0 b = -1.0 * (wave - 510.0) / (510.0 - 490.0) elif wave >= 510.0 and wave <= 580.0: r = (wave - 510.0) / (580.0 - 510.0) g = 1.0 elif wave >= 580.0 and wave <= 645.0: r = 1.0 g = -1.0 * (wave - 645.0) / (645.0 - 580.0) elif wave >= 645.0 and wave <= 780.0: r = 1.0 s = 1.0 if wave > 700.0: s = 0.3 + 0.7 * (780.0 - wave) / (780.0 - 700.0) elif wave < 420.0: s = 0.3 + 0.7 * (wave - 380.0) / (420.0 - 380.0) r = pow(r * s, 0.8) g = pow(g * s, 0.8) b = pow(b * s, 0.8) return qRgb(r*255, g*255, b*255)
class Operator(QObject): currentConfig = None processHandles = {} command = Signal(object) # command def __init__(self): super(Operator, self).__init__() logging.debug('Operator::__init__() called') self.threadpool = QThreadPool.globalInstance() self.procHandleMutex = QMutex() self._startAll = OperatorStartAll(self) self.identGenMutex = QMutex() self.n_ident = 0 def start(self, config): logging.info('<#>DAEMON STARTED<#>') if not config: # return here when there is no config file return self.currentConfig = config # check for autostart elements startElements = [ x for x in self.currentConfig if not x['Socket'] and x['Config']['GeneralConfig']['Autostart'] ] for startElement in startElements: logging.info("Autostart " + startElement['ObjectName']) self.createProcHandle(startElement) def updateConfig(self, config): logging.debug('Operator::updateConfig() called') self.currentConfig = config def getIdent(self): # generate a new process handle identifier self.identGenMutex.lock() self.n_ident += 1 # check if current ident is already in use while list( filter(lambda item: item[0] == self.n_ident, self.processHandles.items())): self.n_ident += 1 if not self.n_ident & 0x7fff: self.n_ident = 0 self.identGenMutex.unlock() return self.n_ident def startExec(self, id): logging.debug('Operator::startExec() called - id: 0x{:08x}'.format(id)) ## create processor and forward config and start filename # self.currentConfig = config # https://stackoverflow.com/questions/34609935/passing-a-function-with-two-arguments-to-filter-in-python # return first element which matches the ID startElement = [x for x in self.currentConfig if x['Id'] == id][0] self.createProcHandle(startElement) def createProcHandle(self, element, inputData=None): #logging.debug('Operator::startExec() called - id: 0x{:08x}'.format(id)) self.procHandleMutex.lock() processHandles = self.processHandles.copy() self.procHandleMutex.unlock() procHandle = OperatorCreateProcHandle(element, inputData, processHandles, self) self.threadpool.start(procHandle) def addHandle(self, identifier, handle): self.procHandleMutex.lock() self.processHandles[identifier] = handle self.procHandleMutex.unlock() def stopExec(self, id): logging.debug('Operator::stopExec() called - id: 0x{:08x}'.format(id)) self.procHandleMutex.lock() processHandles = self.processHandles.copy() self.procHandleMutex.unlock() stopOperator = OperatorStopExec(processHandles, id, self) self.threadpool.start(stopOperator) def stopAll(self): logging.debug('Operator::stopAll() called') logging.info('User command: Stop All') self.procHandleMutex.lock() for threadIdentifier, processHandle in self.processHandles.items(): processHandle.stop() self.procHandleMutex.unlock() def startAll(self, config): logging.info('User command: Start All') self.currentConfig = config self.threadpool.start(self._startAll) def killAll(self): logging.debug('Operator::killAll() called') logging.info('User command: Kill All Processes') # Separate dict must be created because call to removerOperatorThreads # modifies self.processHandles self.procHandleMutex.lock() processes = dict( filter(lambda item: item[1].pid, self.processHandles.items())) self.procHandleMutex.unlock() for threadIdentifier, processHandle in processes.items(): os.kill(processHandle.pid, signal.SIGTERM) # removeOperatorThread is called in run() function of ProcessHandler def getElementStates(self): logging.debug('Operator::getElementStates() called') self.procHandleMutex.lock() processHandles = self.processHandles.copy() self.procHandleMutex.unlock() stateOperator = OperatorReturnElementState(processHandles, self) self.threadpool.start(stateOperator) def updateStatus(self, element, status): #start highlight # area # id # target = "Element" # cmd = UpdateElementStatus #logging.debug('Operator::updateStatus() called - {} - id: 0x{:08x}'.format(status, element['Id'])) address = { 'target': 'Element', 'area': element['AreaNo'], 'id': element['Id'] } cmd = { 'cmd': 'UpdateElementStatus', 'address': address, 'data': status } self.command.emit(cmd) def highlightConnection(self, parentId, childId, wrkArea): logging.debug('Operator::updateStatus() called') address = {'target': 'WorkingArea', 'area': wrkArea} data = {'parentId': parentId, 'childId': childId} cmd = {'cmd': 'HighlightConnection', 'address': address, 'data': data} self.command.emit(cmd) def emitCommand(self, command): self.command.emit(command) def operationDone(self, id, area, record, identifier): logging.debug( 'Operator::operationDone() result received - id: 0x{:08x}, ident: {:04d}' .format(id, identifier)) if isinstance(record, GuiCMD): address = {'target': 'Element', 'id': id, 'area': area} cmd = { 'cmd': 'ElementText', 'address': address, 'data': record.text } self.command.emit(cmd) return if isinstance(record, GuiException): address = {'target': 'Element', 'id': id, 'area': area} cmd = {'cmd': 'ElementException', 'address': address} # log critical exceptions from multiprocessed elements logging.error(repr(record.e)) self.command.emit(cmd) return operationDoneRunnable = OperatorElementOpDone(self.currentConfig, id, record, identifier, self) self.threadpool.start(operationDoneRunnable) def removeOperatorThread(self, id, identifier): logging.debug( 'Operator::removeOperatorThread() called - id: 0x{:08x}, ident: {:04d}' .format(id, identifier)) self.procHandleMutex.lock() procHandle = self.processHandles[identifier] if procHandle.element["HighlightState"]: self.updateStatus(procHandle.element, False) del self.processHandles[identifier] self.procHandleMutex.unlock()
class MainWindow(QMainWindow): """ Main window class """ def __init__(self, app): """ MainWindow initialization """ super(MainWindow, self).__init__() ## app self.app = app ## ui object self.ui = QUiLoader().load("mainwindow.ui") self.ui.show() self.ui.setWindowTitle("RoboAide") self.setMinimumHeight(100) self.setMinimumWidth(250) # self.setMaximumHeight(200) # self.setMaximumWidth(800) self.setIcon() self.msgMu = QMutex() self.numberOfMotors = 6 self.s, self.messageSize = makeStruct() # Connect button signals self.ui.calibrateVerticalAxisButton.clicked.connect( self.calibrateVerticalAxis) self.ports_list = scanAvailablePorts() self.populatePortsList() # --------------- ## Dictionnary of all motor objects self.dictMot = dict() for i in range(1, self.numberOfMotors + 1): mot = Motor(self, "motor" + str(i)) self.dictMot[mot.getName()] = mot # --------------- ## List of drawers self.drawersList = [] for i in range(3): self.drawersList.append(Drawer(self, "drawer" + str(i + 1))) self.updateSliderPositions() ## ListOfSequencesHandler object self.listOfSequencesHandler = ListOfSequencesHandler( self, self.dictMot) # load the last save loadSequences(self.listOfSequencesHandler, self.dictMot) # Connect the slider signals self.ui.slider_mot1.sliderMoved.connect(lambda: self.dictMot[ "motor1"].setGoalPosition(self.ui.slider_mot1.value())) self.ui.slider_mot2.sliderMoved.connect(lambda: self.dictMot[ "motor2"].setGoalPosition(self.ui.slider_mot2.value())) self.ui.slider_mot3.sliderMoved.connect(lambda: self.dictMot[ "motor3"].setGoalPosition(self.ui.slider_mot3.value())) self.ui.slider_mot4.sliderMoved.connect(lambda: self.dictMot[ "motor4"].setGoalPosition(self.ui.slider_mot4.value())) self.ui.slider_mot5.sliderMoved.connect(lambda: self.dictMot[ "motor5"].setGoalPosition(self.ui.slider_mot5.value())) self.ui.slider_mot6.sliderMoved.connect(lambda: self.dictMot[ "motor6"].setGoalPosition(self.ui.slider_mot6.value())) # Connect button signals self.ui.calibrateVerticalAxisButton.clicked.connect( self.calibrateVerticalAxis) # Connect drawer buttons signals self.ui.drawer1Open.clicked.connect(self.drawersList[0].open) self.ui.drawer2Open.clicked.connect(self.drawersList[1].open) self.ui.drawer3Open.clicked.connect(self.drawersList[2].open) self.ui.drawer1Close.clicked.connect(self.drawersList[0].close) self.ui.drawer2Close.clicked.connect(self.drawersList[1].close) self.ui.drawer3Close.clicked.connect(self.drawersList[2].close) # Connect the tab changed with updating the sliders self.ui.tabWidget.currentChanged.connect(self.updateSliderPositions) # Serial communication ## Outgoing message deque self.msgDeque = deque(maxlen=3) ## Stop indicator for the motors self.shouldStop = False ## Message reception QThread object self.msgReception = MessageReception(self) ## Message transmission QThread object self.msgTransmission = MessageTransmission(self) self.app.aboutToQuit.connect(self.msgReception.stop) self.app.aboutToQuit.connect(self.msgTransmission.stop) self.comm = None self.serialConnected = None try: with open('SavePort.json') as save: savedPort = json.load(save) for index in range(1, len(self.ports_list)): if self.ports_list[index].device == savedPort: self.ui.portselection.setCurrentIndex(index) self.connect_port(savedPort) else: print("The last port is not available") except FileNotFoundError: print("SavePort file not found") self.ui.portselection.currentIndexChanged.connect(self.connect_port) def connect_port(self, lastPort=None): """ Connect the selected port of the controller :param: lastPort: name of the port that was last used :return: None """ if isinstance(lastPort, str): commPort = lastPort else: commPort = self.ui.portselection.currentText() self.comm, self.serialConnected = initSerialConnection(commPort) if self.comm is not None: self.app.aboutToQuit.connect(self.comm.close) self.msgReception.start() self.msgTransmission.start() self.sendMessage('s') def setIcon(self): """ Set main window icon :return: None """ appIcon = QIcon(icon) self.ui.setWindowIcon(appIcon) def updateSliderPositions(self, index=0): """ Update motor slider positions :return: None """ if index == 0: print("Updating motor slider positions") self.ui.slider_mot1.setValue( self.dictMot["motor1"].getCurrentPosition()) self.ui.slider_mot2.setValue( self.dictMot["motor2"].getCurrentPosition()) self.ui.slider_mot3.setValue( self.dictMot["motor3"].getCurrentPosition()) self.ui.slider_mot4.setValue( self.dictMot["motor4"].getCurrentPosition()) self.ui.slider_mot5.setValue( self.dictMot["motor5"].getCurrentPosition()) self.ui.slider_mot6.setValue( self.dictMot["motor6"].getCurrentPosition()) print("Finished initializing slider positions") def populatePortsList(self): """ Populate the available serial ports in the drop down menu :return: None """ print("Scanning and populating list of available serial ports") for index in range(len(self.ports_list)): result = isinstance(self.ports_list[index], str) if not result: self.ui.portselection.addItem(self.ports_list[index].device) else: self.ui.portselection.addItem(self.ports_list[index]) def sendMessage(self, mode): """ Package message and send on communication port :param mode: mode in which the message should be interpreted by the controller :return: None """ if self.serialConnected: values = (mode.encode(), self.dictMot["motor1"].getGoalPosition(), self.dictMot["motor2"].getGoalPosition(), self.dictMot["motor3"].getGoalPosition(), self.dictMot["motor4"].getGoalPosition(), self.dictMot["motor5"].getGoalPosition(), self.dictMot["motor6"].getGoalPosition(), self.shouldStop, self.drawersList[0].getState(), self.drawersList[1].getState(), self.drawersList[2].getState(), b'\0') print("Outgoing: ", end='') print(values) packed_data = self.s.pack(*values) self.msgMu.lock() self.msgDeque.append(packed_data) self.msgMu.unlock() else: print("Error sending message, serial not connected") def calibrateVerticalAxis(self): """ Trigger vertical axis calibration :return: None """ print("Calibrating vertical axis") self.sendMessage('c') def stopMotors(self): """ Signal all motors to stop and empty message deque """ self.shouldStop = True self.msgDeque.clear() self.sendMessage('a')
class MainWindow(QMainWindow): def __init__(self): super(MainWindow, self).__init__() self.ui = Ui_MainWindow() self.ui.setupUi(self) self.signals = MainWindowSignals() self.save_data_dir = None self.current_measurement = 0 self.max_measurements = 0 self.collecting = False self.time_axis = None self.mutex = QMutex() self.comp_thread = QThread() self.exp_thread = QThread() self._connect_components() self._set_initial_widget_states() self._store_line_objects() self._set_plot_mouse_mode() def _set_initial_widget_states(self): with start_action(action_type="_set_initial_widget_states"): self.update_max_measurements(self.ui.measurements.value()) self.ui.stop_btn.setDisabled(True) self.ui.reset_avg_btn.setDisabled(True) self.ui.save_loc.setDisabled(True) self.ui.save_loc_browse_btn.setDisabled(True) def _connect_components(self): """Connect widgets and events to make the UI respond to user interaction. """ # Start/Stop Buttons self.ui.start_btn.clicked.connect(self.start_collecting) self.ui.stop_btn.clicked.connect(self.stop_collecting) # Measurement Counter self.ui.measurements.valueChanged.connect(self.update_max_measurements) # Save data controls self.ui.save_data_checkbox.stateChanged.connect( self.save_loc_set_state) self.ui.save_loc_browse_btn.clicked.connect(self.get_save_location) # Start/Stop point self.ui.stop_pt_checkbox.stateChanged.connect(self.stop_pt_set_state) # Dark current self.ui.dark_curr_checkbox.stateChanged.connect( self.dark_curr_set_state) def closeEvent(self, event): """Clean up worker threads if the window is closed while collecting data. Notes ----- This overrides the default closeEvent method of QMainWindow. """ with start_action(action_type="close"): if self.collecting: with start_action(action_type="quit_threads"): self.comp_thread.quit() self.exp_thread.quit() with start_action(action_type="wait_comp_thread"): self.comp_thread.wait() with start_action(action_type="wait_exp_thread"): self.exp_thread.wait() event.accept() def _store_line_objects(self): """Store references to the lines so the data can be updated later. """ starting_data = (np.arange(100), np.zeros(100)) self.live_par_line = self.ui.live_par_graph.plot(*starting_data) self.live_perp_line = self.ui.live_perp_graph.plot(*starting_data) self.live_ref_line = self.ui.live_ref_graph.plot(*starting_data) self.live_da_par_line = self.ui.live_da_par_graph.plot(*starting_data) self.live_da_perp_line = self.ui.live_da_perp_graph.plot( *starting_data) self.live_da_cd_line = self.ui.live_da_cd_graph.plot(*starting_data) self.avg_da_par_line = self.ui.avg_da_par_graph.plot(*starting_data) self.avg_da_perp_line = self.ui.avg_da_perp_graph.plot(*starting_data) self.avg_da_cd_line = self.ui.avg_da_cd_graph.plot(*starting_data) def _set_plot_mouse_mode(self): self.ui.live_par_graph.getPlotItem().getViewBox().setMouseMode( ViewBox.RectMode) self.ui.live_perp_graph.getPlotItem().getViewBox().setMouseMode( ViewBox.RectMode) self.ui.live_ref_graph.getPlotItem().getViewBox().setMouseMode( ViewBox.RectMode) self.ui.live_da_par_graph.getPlotItem().getViewBox().setMouseMode( ViewBox.RectMode) self.ui.live_da_perp_graph.getPlotItem().getViewBox().setMouseMode( ViewBox.RectMode) self.ui.live_da_cd_graph.getPlotItem().getViewBox().setMouseMode( ViewBox.RectMode) self.ui.avg_da_par_graph.getPlotItem().getViewBox().setMouseMode( ViewBox.RectMode) self.ui.avg_da_perp_graph.getPlotItem().getViewBox().setMouseMode( ViewBox.RectMode) self.ui.avg_da_cd_graph.getPlotItem().getViewBox().setMouseMode( ViewBox.RectMode) @Slot(np.ndarray) def set_time_axis(self, values): with start_action(action_type="set_time_axis"): self.time_axis = values * 1e6 @Slot(int) def update_max_measurements(self, x): with start_action(action_type="update_max_measurements", new_max=x): self.max_measurements = x self.ui.measurement_counter_label.setText( f"{self.current_measurement}/{self.max_measurements}") @Slot(int) def update_current_measurement(self, x): with start_action(action_type="update_current_measurement", new_meas=x): self.current_measurement = x self.ui.measurement_counter_label.setText( f"{self.current_measurement}/{self.max_measurements}") @Slot() def start_collecting(self): """Begins collecting data when the "Start" button is pressed. """ with start_action(action_type="start_collecting"): settings, should_quit = self._collect_settings() if should_quit: Message.log(should_quit=should_quit) return with start_action(action_type="create_workers"): self.comp_worker = ComputationWorker(self.mutex, settings) self.exp_worker = ExperimentWorker(self.mutex, settings) self._connect_worker_signals() self.comp_worker.moveToThread(self.comp_thread) self.exp_worker.moveToThread(self.exp_thread) with start_action(action_type="start_threads"): self.comp_thread.start() self.exp_thread.start() self.signals.measure.emit() Message.log(signal="measure") self._disable_acq_controls() def _collect_settings(self): """Collect all the settings from the UI. """ with start_action(action_type="collect_settings"): settings = UiSettings() settings, should_quit = self._collect_meas_settings(settings) if should_quit: return settings, should_quit settings, should_quit = self._collect_instr_settings(settings) if should_quit: return settings, should_quit settings, should_quit = self._collect_save_settings(settings) if should_quit: return settings, should_quit settings, should_quit = self._collect_start_stop_settings(settings) if should_quit: return settings, should_quit settings, should_quit = self._collect_dark_curr_settings(settings) return settings, should_quit def _collect_dark_curr_settings(self, settings): with start_action(action_type="dark_current_settings"): quit = False use_dark_curr = self.ui.dark_curr_checkbox.isChecked() Message.log(checked=use_dark_curr) if not use_dark_curr: Message.log(quit=quit) return settings, quit try: dark_curr_par = float(self.ui.dark_curr_par.text()) dark_curr_perp = float(self.ui.dark_curr_perp.text()) dark_curr_ref = float(self.ui.dark_curr_ref.text()) settings.dark_curr_par = dark_curr_par settings.dark_curr_perp = dark_curr_perp settings.dark_curr_ref = dark_curr_ref except ValueError: quit = True Message.log(quit=quit) return settings, quit def _collect_meas_settings(self, settings): """Collect the number of measurements from the UI. """ with start_action(action_type="measurement_settings"): quit = False settings.num_measurements = self.ui.measurements.value() Message.log(quit=quit) return settings, quit def _collect_instr_settings(self, settings): """Collect the settings from the UI related to the instrument. """ with start_action(action_type="instrument_settings"): quit = False instr_name = self.ui.instr_name.text() Message.log(instrument_name=instr_name) if instr_name == "": Message.log(quit=quit) quit = True settings.instr_name = instr_name Message.log(quit=quit) return settings, quit def _collect_save_settings(self, settings): """Collect the settings from the UI related to saving data. """ with start_action(action_type="save_data_settings"): quit = False should_save = self.ui.save_data_checkbox.isChecked() Message.log(checked=should_save) if should_save and not self._saving_should_proceed(): Message.log(quit=quit) quit = True settings.save = should_save settings.save_loc = self.ui.save_loc.text() Message.log(dir=settings.save_loc) Message.log(quit=quit) return settings, quit def _collect_start_stop_settings(self, settings): """Collect the settings from the UI related to the Start/Stop points. """ with start_action(action_type="start_stop_settings"): quit = False start_pt = self.ui.start_pt.value() settings.start_pt = start_pt Message.log(start=start_pt) if not self.ui.stop_pt_checkbox.isChecked(): stop_pt = self.ui.stop_pt.value() settings.stop_pt = stop_pt if start_pt >= stop_pt: self._tell_start_greater_than_stop() quit = True Message.log(stop=settings.stop_pt) Message.log(quit=quit) return settings, quit def _connect_worker_signals(self): """Connect signals for communication between workers and the main window. """ # Produced by the experiment worker self.exp_worker.signals.preamble.connect( self.comp_worker.store_preamble) self.exp_worker.signals.new_data.connect( self.comp_worker.compute_signals) self.exp_worker.signals.done.connect(self.cleanup_when_done) # Produced by the computation worker self.comp_worker.signals.time_axis.connect(self.set_time_axis) self.comp_worker.signals.new_data.connect(self.update_plots) self.comp_worker.signals.meas_num.connect( self.update_current_measurement) # Produced by the main window self.signals.measure.connect(self.exp_worker.measure) self.ui.reset_avg_btn.clicked.connect(self.comp_worker.reset_averages) def _disable_acq_controls(self): """Disable certain controls while collecting data. """ # Disabled self.ui.start_btn.setDisabled(True) self.ui.instr_name.setDisabled(True) self.ui.measurements.setDisabled(True) self.ui.save_data_checkbox.setDisabled(True) self.ui.save_loc.setDisabled(True) self.ui.save_loc_browse_btn.setDisabled(True) self.ui.start_pt.setDisabled(True) self.ui.stop_pt.setDisabled(True) self.ui.stop_pt_checkbox.setDisabled(True) # Enabled self.ui.stop_btn.setEnabled(True) self.ui.reset_avg_btn.setEnabled(True) def _enable_acq_controls(self): """Enable certain controls after data collection is complete. """ # Enabled self.ui.start_btn.setEnabled(True) self.ui.instr_name.setEnabled(True) self.ui.measurements.setEnabled(True) self.ui.save_data_checkbox.setEnabled(True) if self.ui.save_data_checkbox.isChecked(): self.ui.save_loc.setEnabled(True) self.ui.save_loc_browse_btn.setEnabled(True) self.ui.start_pt.setEnabled(True) self.ui.stop_pt_checkbox.setEnabled(True) if not self.ui.stop_pt_checkbox.isChecked(): self.ui.stop_pt.setEnabled(True) # Disabled self.ui.stop_btn.setDisabled(True) self.ui.reset_avg_btn.setDisabled(True) @Slot() def stop_collecting(self): """Stops collecting data when the "Stop" button is pressed. """ with start_action(action_type="stop_collecting"): with start_action(action_type="mutex"): self.mutex.lock() common.SHOULD_STOP = True self.mutex.unlock() with start_action(action_type="quit_threads"): self.comp_thread.quit() self.exp_thread.quit() with start_action(action_type="wait_comp_thread"): self.comp_thread.wait() with start_action(action_type="wait_exp_thread"): self.exp_thread.wait() self._enable_acq_controls() self.current_measurement = 0 @Slot() def cleanup_when_done(self): """Clean up workers and threads after data collection is complete. """ with start_action(action_type="done_collecting"): with start_action(action_type="quit_threads"): self.comp_thread.quit() self.exp_thread.quit() with start_action(action_type="wait_comp_thread"): self.comp_thread.wait() with start_action(action_type="wait_exp_thread"): self.exp_thread.wait() with start_action(action_type="mutex"): self.mutex.lock() common.SHOULD_STOP = False self.mutex.unlock() self._enable_acq_controls() self.current_measurement = 0 with start_action(action_type="dialog"): QMessageBox.information(self, "Done", "The experiment has finished.", QMessageBox.StandardButton.Ok) @Slot(PlotData) def update_plots(self, data): """Update the plots in the Live/Average tabs when new data is available. Parameters ---------- data : PlotData Three live data channels and the signals computed from them. """ with start_action(action_type="update_plots"): self.live_par_line.setData(self.time_axis, data.par) self.live_perp_line.setData(self.time_axis, data.perp) self.live_ref_line.setData(self.time_axis, data.ref) if data.da_par is not None: with start_action(action_type="update_da_plots"): self.live_da_par_line.setData(self.time_axis, data.da_par) self.live_da_perp_line.setData(self.time_axis, data.da_perp) self.live_da_cd_line.setData(self.time_axis, data.da_cd) self.avg_da_par_line.setData(self.time_axis, data.avg_da_par) self.avg_da_perp_line.setData(self.time_axis, data.avg_da_perp) self.avg_da_cd_line.setData(self.time_axis, data.avg_da_cd) @Slot(int) def save_loc_set_state(self, state): """Enable or disable the save location controls in response to the checkbox. Parameters ---------- state : int An integer representing the state of the checkbox. Notes ----- 0 - unchecked 2 - checked """ with start_action(action_type="save_loc_state", state=state): if state == 0: self.ui.save_loc.setDisabled(True) self.ui.save_loc_browse_btn.setDisabled(True) Message.log(save="disabled") elif state == 2: self.ui.save_loc.setEnabled(True) self.ui.save_loc_browse_btn.setEnabled(True) Message.log(save="enabled") @Slot() def get_save_location(self): """Get an existing directory in which to store the collected data. """ with start_action(action_type="get_save_location"): self.save_data_dir = QFileDialog.getExistingDirectory() self.ui.save_loc.setText(self.save_data_dir) Message.log(dir=self.save_data_dir) def _save_loc_still_valid(self): """Ensure that the path to the directory still exists before saving data to it. """ save_dir = Path(self.save_data_dir) return save_dir.exists() def _tell_save_loc_is_invalid(self): """Tell the user that the current save location isn't valid or doesn't exist. """ with start_action(action_type="dialog"): QMessageBox.critical( self, "Invalid Save Location", "The current save data location is invalid or doesn't exist. Please choose a new location.", QMessageBox.StandardButton.Ok, ) def _save_would_overwrite(self): """Returns True if the save directory contains *anything*. """ for item in Path(self.save_data_dir).iterdir(): return True return False def _should_overwrite(self): """Asks the user whether data in the save directory should be overwritten. """ with start_action(action_type="dialog") as action: reply = QMessageBox.warning( self, "Overwrite?", "The current directory contents will be erased. Continue?", QMessageBox.StandardButton.Ok | QMessageBox.StandardButton.Cancel, ) should_overwrite = reply == QMessageBox.StandardButton.Ok action.add_success_fields(overwrite=should_overwrite) return should_overwrite def _saving_should_proceed(self): """Determine whether valid settings have been entered for saving data. """ with start_action(action_type="saving_should_proceed"): try: loc_is_valid = self._save_loc_still_valid() except TypeError: loc_is_valid = False if not loc_is_valid: self._tell_save_loc_is_invalid() return False would_overwrite = self._save_would_overwrite() if would_overwrite and (not self._should_overwrite()): return False return True @Slot(int) def stop_pt_set_state(self, state): """Enable or disable the "Stop Point" controls in response to the checkbox. Parameters ---------- state : int An integer representing the state of the checkbox. Notes ----- 0 - unchecked 2 - checked """ with start_action(action_type="stop_pt_state", state=state): if state == 2: self.ui.stop_pt.setDisabled(True) Message.log(stop_pt="disabled") elif state == 0: self.ui.stop_pt.setEnabled(True) Message.log(stop_pt="enabled") def _tell_start_greater_than_stop(self): """Tell the user that the current save location isn't valid or doesn't exist. """ QMessageBox.critical( self, "Invalid Start/Stop Points", "The Start point must be less than the Stop point.", QMessageBox.StandardButton.Ok, ) @Slot(int) def dark_curr_set_state(self, state): """Enable or disable the dark current controls in response to the checkbox. Parameters ---------- state : int An integer representing the state of the checkbox. Notes ----- 0 - unchecked 2 - checked """ with start_action(action_type="dark_curr_state", state=state): if state == 0: self.ui.dark_curr_par.setDisabled(True) self.ui.dark_curr_perp.setDisabled(True) self.ui.dark_curr_ref.setDisabled(True) Message.log(dark_curr="disabled") elif state == 2: self.ui.dark_curr_par.setEnabled(True) self.ui.dark_curr_perp.setEnabled(True) self.ui.dark_curr_ref.setEnabled(True) Message.log(dark_curr="enabled")
class msv2: def __init__(self): self.ser = serial.Serial() self.escape = 0 self.connected = 0 self.state = WAITING_DLE self.crc_data = [] self.data = [] self.mutex = QMutex() def explore(self): list = serial.tools.list_ports.comports(include_links=False) return [x.device for x in list] def connect(self, port): self.port = port self.ser.baudrate = BAUDRATE self.ser.port = port self.ser.timeout = 0.2 try: self.ser.open() self.connected = 1 print("connected") return 1 except: return 0 def reconnect(self): try: self.ser.port = self.port self.ser.baudrate = BAUDRATE self.ser.timeout = 0.2 self.ser.open() self.connected = 1 print("connected") return 1 except: return 0 def disconnect(self): try: self.ser.close() self.connected = 0 print("disconnected") return 1 except: return 0 def is_connected(self): return self.connected def encode(self, opcode, data): bin_data = [] crc_data = [] bin_data.append(DLE) bin_data.append(STX) bin_data.append(opcode) bin_data.append(int(len(data)/2)) crc_data.append(opcode) crc_data.append(int(len(data)/2)) i = 0 for byte in data: bin_data.append(byte) crc_data.append(byte) if byte == DLE: bin_data.append(byte) crc_data.append(0) crc_data.append(0) crc = crc16(crc_data) bin_data.append(crc & 0x00ff) bin_data.append((crc >> 8) & 0x00ff) return bin_data def decode(self, d): #print("decode state:", self.state) d = ord(d) if (self.escape == 1 and d == STX): self.state = WAITING_OPCODE self.escape = 0 return MSV2_PROGRESS if (self.state == WAITING_DLE and d == DLE): self.crc_data = [] self.data = [] self.state = WAITING_STX return MSV2_PROGRESS if (d == DLE and self.escape == 0): self.escape = 1 return MSV2_PROGRESS if (d == DLE and self.escape == 1): self.escape = 0 if (self.state == WAITING_STX and d == STX): self.state = WAITING_OPCODE return MSV2_PROGRESS if (self.state == WAITING_OPCODE): self.opcode = d self.state = WAITING_LEN self.crc_data.append(d) return MSV2_PROGRESS if (self.state == WAITING_LEN): self.data_len = d self.length = 2*d self.crc_data.append(d) self.counter = 0 self.state = WAITING_DATA return MSV2_PROGRESS if (self.state == WAITING_DATA): self.data.append(d) self.crc_data.append(d) self.counter += 1 if (self.counter==self.length): self.state = WAITING_CRC1 return MSV2_PROGRESS if (self.state == WAITING_CRC1): self.crc = d self.state = WAITING_CRC2 return MSV2_PROGRESS if (self.state == WAITING_CRC2): self.crc += d<<8 self.state = WAITING_DLE self.crc_data.append(0) self.crc_data.append(0) if(self.crc == crc16(self.crc_data)): return MSV2_SUCCESS else: return MSV2_WRONG_CRC self.state=WAITING_DLE return MSV2_PROGRESS def send(self, opcode, data): if self.connected: self.mutex.lock() msg = self.encode(opcode, data) error = 0 try: self.ser.write(msg) except: print("WRITE ERROR") self.mutex.unlock() self.reconnect() return -1 #print('[{}]'.format(', '.join(hex(x) for x in msg))) try: while 1: byte = self.ser.read(1) #print("dta: {}".format(hex(ord(byte)))) if not byte: print("no resp error") self.mutex.unlock() return 0 res = self.decode(byte) #print("res:", res) if not res == MSV2_PROGRESS: break #print('[{}]'.format(', '.join(hex(x) for x in self.data))) if self.data == [0xce, 0xec] or self.data == [0xbe, 0xeb]: self.mutex.unlock() print("CRC_ERROR") return 0 else: self.mutex.unlock() print("nominal_resp") return self.data except: print("READ ERROR") self.mutex.unlock() self.reconnect() return -1 else: print("CONN_ERROR") return -1
class NodeHandler(QObject): newNodeDiscovered = Signal(dict) nodeConnected = Signal(dict) nodeDisconnected = Signal(dict) nodeDisappeared = Signal(dict) nodeAliveMessage = Signal(dict) cleanupDone = Signal() threadReady = Signal() def __init__(self, mainwindow): super(NodeHandler, self).__init__() self.visibleNodes = {} self.connectedNodes = {} self.nodeListMutex = QMutex() self.mainwindow = mainwindow @Slot() def onRun(self): # multithreading hack to prevent threading sigsev conditions # all the stuff should be executed in this threads event loop # if we do stuff here, the thread context could be different self.threadReady.connect(self.onThreadReady) self.threadReady.emit() @Slot() def onThreadReady(self): self.udpSocket = QUdpSocket(self) #self.udpSocket.bind(13370, QUdpSocket.ShareAddress) self.udpSocket.bind(13370, QUdpSocket.ReuseAddressHint) self.udpSocket.readyRead.connect(self.onSocketReadyRead) # check every second self.disconnectTimer = QTimer(self) self.disconnectTimer.moveToThread(self.thread()) self.disconnectTimer.timeout.connect(self.onDisconnectTimerFire) self.disconnectTimer.start(100) @Slot() def onSocketReadyRead(self): msg = self.udpSocket.readDatagram(self.udpSocket.pendingDatagramSize()) if msg[0][0:2] == "CB": msg_split = msg[0].split('|') device_id = msg_split[1].data().decode('ascii') device_version = msg_split[2].data().decode('ascii') now = datetime.datetime.now() ip = QHostAddress(msg[1].toIPv4Address()).toString() device = {"id": device_id, "version": device_version, "ip": ip, "last_seen": now} self.nodeListMutex.lock() if (device_id not in self.connectedNodes.keys()) and \ (device_id not in self.visibleNodes.keys()): self.visibleNodes[device_id] = device self.newNodeDiscovered.emit(device) print(f"discovered {device_id}") # update timestamps for known visible/connected devices if device_id in self.visibleNodes.keys(): self.visibleNodes[device_id]["last_seen"] = now self.nodeAliveMessage.emit(self.visibleNodes[device_id]) if device_id in self.connectedNodes.keys(): self.connectedNodes[device_id]["last_seen"] = now self.nodeAliveMessage.emit(self.connectedNodes[device_id]) self.nodeListMutex.unlock() @Slot(str) def onSocketCanDiscovered(self, vcan): now = datetime.datetime.now() device = {"id": vcan, "version": "socket_can", "ip": None, "last_seen": now} self.nodeListMutex.lock() if (vcan not in self.connectedNodes.keys()) and \ (vcan not in self.visibleNodes.keys()): self.visibleNodes[vcan] = device self.newNodeDiscovered.emit(device) if vcan in self.visibleNodes.keys(): self.visibleNodes[vcan]["last_seen"] = now self.nodeAliveMessage.emit(self.visibleNodes[vcan]) if vcan in self.connectedNodes.keys(): self.connectedNodes[vcan]["last_seen"] = now self.nodeAliveMessage.emit(self.connectedNodes[vcan]) self.nodeListMutex.unlock() @Slot() def onDisconnectTimerFire(self): now = datetime.datetime.now() self.nodeListMutex.lock() ids_to_delete = [] for id, node in self.visibleNodes.items(): # check time difference if (now - node["last_seen"]) > datetime.timedelta(seconds=6): ids_to_delete.append(id) self.nodeDisappeared.emit(node) for id in ids_to_delete: del self.visibleNodes[id] ids_to_delete = [] for id, node in self.connectedNodes.items(): # check time difference if (now - node["last_seen"]) > datetime.timedelta(seconds=10): ids_to_delete.append(id) self.nodeDisconnected.emit(node) for id in ids_to_delete: # check for running connection process if self.connectedNodes[id]["connection"].isConnected: self.connectedNodes[id]["connection"].resetConnection() del self.connectedNodes[id] self.nodeListMutex.unlock() @Slot(dict) def onConnectToNode(self, node): print(f"Node handler creating connection to {node['id']}.") if ("connection" not in node.keys() or node["connection"] is None) and node["ip"] is not None: # start a connection to a canbadger con = NodeConnection(node) node["connection"] = con con.connectionSucceeded.connect(self.onConnectionSucceeded) con.connectionFailed.connect(self.onConnectionFailed) con.newSettingsData.connect(self.mainwindow.onSettingsReceived) self.mainwindow.exiting.connect(con.resetConnection) con.onRun() elif ("connection" not in node.keys() or node["connection"] is None) and node["ip"] is None: # start a socketCan connection con = SocketCanConnection(node) node["connection"] = con con.connectionSucceeded.connect(self.onConnectionSucceeded) con.connectionFailed.connect(self.onConnectionFailed) self.mainwindow.exiting.connect(con.cleanup) con.onRun() @Slot() def onConnectionSucceeded(self): node_connection = self.sender() node = node_connection.node self.nodeListMutex.lock() if node["id"] in self.visibleNodes.keys(): del self.visibleNodes[node["id"]] self.connectedNodes[node["id"]] = node self.nodeListMutex.unlock() self.nodeConnected.emit(node) node_connection.nodeDisconnected.connect(self.onDisconnectNode) @Slot() def onConnectionFailed(self): # signal red buttonFeedback(False, self.mainwindow.interfaceSelection) @Slot(dict) def onDisconnectNode(self, node): # reset connection and terminate thread if "connection" in node.keys(): node["connection"].resetConnection() if "thread" in node.keys(): node["thread"].exit() pass # delete the node from dicts self.nodeListMutex.lock() if node["id"] in self.connectedNodes: del self.connectedNodes[node["id"]] if node["id"] in self.visibleNodes: del self.visibleNodes[node["id"]] self.nodeListMutex.unlock() self.nodeDisconnected.emit(node) @Slot(str, str) def onIDChange(self, old, new): self.nodeListMutex.lock() if old in self.connectedNodes: self.connectedNodes[new] = self.connectedNodes[old] del self.connectedNodes[old] if old in self.visibleNodes: self.visibleNodes[new] = self.visibleNodes[old] del self.visibleNodes[old] self.nodeListMutex.unlock() @Slot() def onExiting(self): self.disconnectTimer.stop() self.udpSocket.close() self.cleanupDone.emit()