def __on_all_test_nodes_clicked(self, sender): """ Called when all nodes in the test touch view were touched. :type sender: TouchTestView """ if not self.__touch_test_view: return self.__touch_test_view.active = False if sender is not None: StudyLog.get_instance().write_event_log( 'The touch test view was successfully passed.') player.setTimeout(1, lambda: self.__draw_main_view())
def __on_drag_end(self): """ Called when a drag has ended. """ StudyLog.get_instance().write_device_canvas_event( self.__device, "DRAG END") self.__last_drag_offset = (0, 0) self.__hold_active = False if self.__double_tap_hold_active: StudyLog.get_instance().write_device_canvas_event( self.__device, "DOUBLE TAP HOLD END") self.dispatch(self.__DOUBLE_TAP_HOLD_END, sender=self) self.__double_tap_hold_active = False
def __on_swipe_down(self): """ Called when a swipe to the left was recognized. """ if self.__hold_active or self.__double_tap_hold_active: return event_time = player.getFrameTime() - self.__event_time if event_time > self.__device_pointer_config.swipe_max_time: return StudyLog.get_instance().write_device_canvas_event( self.__device, "SWIPE DOWN") self.dispatch(self.__SWIPE, sender=self, direction=SwipeRecognizer.DOWN)
def __on_swipe_down(self, sender): """ Called when a swipe down occurred. Deletes aid lines or hides the detail view of map points. :param sender: The device that caused this event. :type sender: Device """ grid_element_div = self.__multi_view_node.get_grid_element_from_pos( (sender.pos_screen_x, sender.pos_screen_y)) # Look for a grid element under the cursor. if not grid_element_div: return for vis_view in grid_element_div.child_nodes: rel_pos = grid_element_div.get_rel_pos( (sender.pos_screen_x, sender.pos_screen_y)) if isinstance(vis_view, MapVisView): element = vis_view.getElementByPos(rel_pos) # Show the detail view of the map point. if isinstance(element, avg.ImageNode): map_point_view = self.__get_parent_from_node_with_type( element, MapPointView, MapVisView) if map_point_view: map_point_view.show_detail_view(False) StudyLog.get_instance().write_event_log( 'A DoD was closed (by swipe).') else: map_point_detail_view = self.__get_parent_from_node_with_type( element, MapPointDetailView, MapVisView) if map_point_detail_view: map_point_detail_view.map_point_view.show_detail_view( False) StudyLog.get_instance().write_event_log( 'A DoD was closed (by swipe).') # Aid line deletion. elif isinstance(vis_view, ChartBase): for aid_line_type in [ AidLineType.Deposit, AidLineType.DepositVertical, AidLineType.DepositHorizontal, AidLineType.AxisDragX, AidLineType.AxisDragY ]: vis_view.invoke_aid_line_controller_event( event_name=DepositAidLine.DELETE_AID_LINE, aid_line_controller_type=aid_line_type, pos=rel_pos)
def __on_cursor_motion(self, event): """ Called when a cursor motion was recognized on this view. :type event: CursorEvent """ StudyLog.get_instance().write_device_canvas_touch( self.__device, "CURSOR MOTION", event) self.dispatch(self.__CURSOR_MOTION, sender=self, event=event) if not self.__event: return if self.__event.cursorid != event.cursorid: return self.__event = event if event.contact.distancefromstart > self.__event_movement_dist: self.__event_movement_dist = event.contact.distancefromstart
def _on_add_aid_line(self, pos): """ Creates an aid line at the given pos. :param pos: The pos to create a aid line on. :type pos: tuple[float, float] """ if not (self._aid_line_area[0] <= pos[0] <= self._aid_line_area[2] and self._aid_line_area[1] <= pos[1] <= self._aid_line_area[3]): return h_aid_line = self._draw_horizontal_aid_line(pos=pos) v_aid_line = self._draw_vertical_aid_line(pos=pos) if h_aid_line: self._horizontal_aid_lines.append(h_aid_line) if v_aid_line: self._vertical_aid_lines.append(v_aid_line) # Draw the intersections and the labels. if self._intersection_config.show_intersections: aid_line_values = { a.pos: Orientation.Horizontal for a in self._horizontal_aid_lines } aid_line_values.update({ a.pos: Orientation.Vertical for a in self._vertical_aid_lines }) intersections = self._get_intersections( aid_line_orientations=aid_line_values.values(), aid_line_positions=aid_line_values.keys()) self._remove_intersections() self._draw_intersections(intersections=intersections) if self._aid_line_config.show_label: if h_aid_line: self._draw_label(aid_line_div=h_aid_line, orientation=Orientation.Horizontal) if v_aid_line: self._draw_label(aid_line_div=v_aid_line, orientation=Orientation.Vertical) StudyLog.get_instance().write_event_log( 'An desposite aid line was created.')
def __on_drag(self, offset): """ Called when a drag has started. :param offset: The offset to the start pos. :type offset: tuple[float, float] """ if self.__hold_active: return pos_change = offset[0] - self.__last_drag_offset[0], offset[ 1] - self.__last_drag_offset[1] self.__last_drag_offset = offset event = self.__DOUBLE_TAP_HOLD_DRAG if self.__double_tap_hold_active else self.__DRAG if self.__double_tap_hold_active: StudyLog.get_instance().write_device_canvas_event( self.__device, "DOUBLE TAP HOLD DRAG") self.dispatch(event, sender=self, pos_change=pos_change)
def inject_touch_event(event_type, pos, event_id): """ Injects a touch event in the system. :param event_type: The type of event to inject. :type event_type: int :param pos: The position the event should take place. :type pos: tuple[float, float] :param event_id: The id for the event. :type event_id: int """ # print "Inject event: id={!s}, type={!s}, pos={!s}".format(event_id, event_type, pos) global __helper if config_app.study_mode: StudyLog.get_instance().write_touch(pos_x=pos[0], pos_y=pos[1], event_type=event_type, to_injection=True) __helper.fakeTouchEvent(event_id, event_type, avg.Event.TOUCH, pos)
def on_frame(self): """ Needed to be called every frame. """ if not self.__event: return if self.__double_tap_hold_active or self.__hold_active: return now = player.getFrameTime() down_time = now - self.__event_time if down_time <= self.__device_pointer_config.hold_delay: return if self.__event_movement_dist > self.__device_pointer_config.hold_max_dist * player.getPixelsPerMM( ): return else: self.__hold_active = True StudyLog.get_instance().write_device_canvas_event( self.__device, "HOLD") self.dispatch(self.__HOLD, sender=self, event=self.__event)
def _on_axis_drag_started(self, sender): """ Called when a drag on an axis has started. :type sender: ChartAxis """ axis = sender # Only allow one aid line to be created from one axis at a time. if axis in self.__newly_added_aid_lines: return # Check which axis this is: # Set the size of the line node. if self.__orientation is Orientation.Horizontal: aid_line = self._draw_horizontal_aid_line(pos=axis.pos) StudyLog.get_instance().write_event_log( 'An horizontal axis drag aid line in chart {} was created.'. format(self._chart.view_id)) else: aid_line = self._draw_vertical_aid_line(pos=axis.pos) StudyLog.get_instance().write_event_log( 'An vertical axis drag aid line in chart {} was created.'. format(self._chart.view_id)) self.__newly_added_aid_lines[axis] = aid_line self.__newly_added_aid_line_pos[aid_line] = aid_line.pos # Check which axis this is. self.__aid_lines[aid_line] = axis # Draw the intersections and the labels. if self._intersection_config.show_intersections: self._remove_intersections() self._draw_intersections(intersections=self._get_intersections( aid_line_orientations=[self.__orientation] * len(self.__aid_lines), aid_line_positions=[a.pos for a in self.__aid_lines.keys()])) if self._aid_line_config.show_label: self._draw_label(aid_line_div=aid_line, orientation=self.__orientation)
def __on_cursor_down(self, event): """ Called when a cursor down was recognized on this view. :type event: CursorEvent """ StudyLog.get_instance().write_device_canvas_touch( self.__device, "CURSOR DOWN", event) self.dispatch(self.__CURSOR_DOWN, sender=self, event=event) if self.__event: return self.__event = event self.__event_id = event.cursorid if player.getFrameTime( ) - self.__last_tap_time < self.__device_pointer_config.double_tap_hold_max_time: self.__double_tap_hold_active = True StudyLog.get_instance().write_device_canvas_event( self.__device, "DOUBLE TAP HOLD") self.dispatch(self.__DOUBLE_TAP_HOLD, sender=self, event=event) else: self.__event_time = player.getFrameTime() self.__event_movement_dist = 0
def _on_delete_aid_line(self, pos): """ Deletes an aid line at the pos, if an aid line lies there. :param pos: The pos to create a aid line on. :type pos: tuple[float, float] """ if not (self._aid_line_area[0] <= pos[0] <= self._aid_line_area[2] and self._aid_line_area[1] <= pos[1] <= self._aid_line_area[3]): return aid_lines_to_remove = [] for i in range(len(self._horizontal_aid_lines)): horizontal_aid_line = self._horizontal_aid_lines[i] line_node = horizontal_aid_line.getChild(0) aid_line_size = (line_node.pos1[0], horizontal_aid_line.pos[1] - line_node.strokewidth / 2, line_node.pos2[0], horizontal_aid_line.pos[1] + line_node.strokewidth / 2) if aid_line_size[0] <= pos[0] <= aid_line_size[ 2] and aid_line_size[1] <= pos[1] <= aid_line_size[3]: if i not in aid_lines_to_remove: aid_lines_to_remove.append(i) for i in range(len(self._vertical_aid_lines)): vertical_aid_line = self._vertical_aid_lines[i] line_node = vertical_aid_line.getChild(0) aid_line_size = (vertical_aid_line.pos[0] - line_node.strokewidth / 2, line_node.pos1[1], vertical_aid_line.pos[0] + line_node.strokewidth / 2, line_node.pos2[1]) if aid_line_size[0] <= pos[0] <= aid_line_size[ 2] and aid_line_size[1] <= pos[1] <= aid_line_size[3]: if i not in aid_lines_to_remove: aid_lines_to_remove.append(i) removed = 0 for i in aid_lines_to_remove: i -= removed yet_removed = 0 if 0 <= i < len(self._horizontal_aid_lines): yet_removed = 1 self._horizontal_aid_lines.pop(i).unlink(True) if i < len(self._vertical_aid_lines): yet_removed = 1 self._vertical_aid_lines.pop(i).unlink(True) removed -= yet_removed if self._intersection_config.show_intersections: self._remove_intersections() aid_line_values = { a.pos: Orientation.Horizontal for a in self._horizontal_aid_lines } aid_line_values.update({ a.pos: Orientation.Vertical for a in self._vertical_aid_lines }) self._draw_intersections( self._get_intersections( aid_line_orientations=aid_line_values.values(), aid_line_positions=aid_line_values.keys())) StudyLog.get_instance().write_event_log( 'An desposite aid line was deleted.')
def __on_close_button_clicked(self): """ Called when a tap on the close button has happened. """ self.__map_point_view.show_detail_view(False) StudyLog.get_instance().write_event_log('A DoD was closed (by close button).')
def __draw_main_view(self, fake=False): """ Draws the main vis view. :param fake: Should the fake or the original data set be loaded? :type fake: bool """ if self.__touch_test_view: self.__touch_test_view.unlink(True) self.__touch_test_view = None self.__wall_divico = WallDivico( parent=self, size=self.size, map_image_info=config_app.maps_images[config_app.map_image], data_file=config_app.sql_result_dump_filename[ config_app.data_mode_string], grid_groups=self.__grid_groups, grid_labels=self.__grid_labels) self.__task_text_view_canvas = player.createCanvas( id="task_text_view", autorender=True, multisamplesamples=8, size=(self.size[0], self.size[1] / 12)) text = [ "{} ({})".format(self._study_handler.current_block_name, self._study_handler.current_phase) ] if self._study_handler else [] self.__task_text_view = TextView2( text_elements=text, alignment="center", font_size=42, element_size=900, parent=self.__task_text_view_canvas.getRootNode(), size=(self.size[0], self.size[1] / 12), padding_top=20, padding_bottom=20, element_offset=50, # elementoutlinecolor="f00" ) self.__task_text_view_image_top = avg.ImageNode( parent=self, size=(self.size[0], self.size[1] / 12), href="canvas:task_text_view") self.__task_text_view_image_bottom = avg.ImageNode( parent=self, pos=(0, self.size[1] * 11 / 12), size=(self.size[0], self.size[1] / 12), href="canvas:task_text_view") self.__phase_block_label = avg.WordsNode( parent=self, pos=(3, self.size[1] - 20), fontsize=15, color="555", alignment="left", variant="bold", rawtextmode=True, text="#{} - {}: {}".format(config_app.SESSION, self._study_handler.current_phase, self._study_handler.current_block_name) if self._study_handler else "") self.__interaction_activation_label = avg.WordsNode(parent=self, pos=(3, 3), fontsize=15, color="555", alignment="left", variant="bold", rawtextmode=True, text="") self.__create_touch_vis_view() # initialize StudyLogging if config_app.study_mode: StudyLog.get_instance().write_event_log( "initialized view and divico control") self.subscribe( avg.Node.CURSOR_DOWN, lambda event: self.__log_touch( event, event_type="CURSOR_DOWN")) self.subscribe( avg.Node.CURSOR_MOTION, lambda event: self.__log_touch(event, event_type="CURSOR_MOTION")) self.subscribe( avg.Node.CURSOR_UP, lambda event: self.__log_touch(event, event_type="CURSOR_UP"))
def __log_touch(event, event_type="-1"): # print "event node: ", self.getElementByPos(event.pos) StudyLog.get_instance().write_touch(event.x, event.y, event.userid, event_type)
def __on_key_down(self, event): """ Called when a key input was recognized. """ shift = event.modifiers & avg.KEYMOD_LSHIFT or event.modifiers & avg.KEYMOD_RSHIFT strg = event.modifiers & avg.KEYMOD_LCTRL or event.modifiers & avg.KEYMOD_RCTRL # For the normal interaction with the application. if event.keyname == 'A' and not strg and not shift: key_message = [ v['msg'] for v in self.__key_messages if v['keys'] == ['A'] ] if len(key_message) != 0: StudyLog.get_instance().write_event_log(key_message[0]) self.sensitive = True self.__interaction_activation_label.text = "" # For the normal interaction with the application. if event.keyname == 'D' and not strg and not shift: key_message = [ v['msg'] for v in self.__key_messages if v['keys'] == ['D'] ] if len(key_message) != 0: StudyLog.get_instance().write_event_log(key_message[0]) self.sensitive = False self.__interaction_activation_label.text = "Touch interactions deactivated" if event.keyname == 'V' and not strg and not shift: key_message = [ v['msg'] for v in self.__key_messages if v['keys'] == ['V'] ] if len(key_message) != 0: StudyLog.get_instance().write_event_log(key_message[0]) if self.__touch_visualization_overlay: self.__touch_visualization_overlay.active = not self.__touch_visualization_overlay.active if self.__touch_visualization_overlay.active: self.reorderChild(self.__touch_visualization_overlay, self.getNumChildren() - 1) else: self.__create_touch_vis_view() if event.keyname == 'T' and not strg and not shift: if self.__touch_test_view and self.__touch_test_view.active: key_message = [ v['msg'] for v in self.__key_messages if v['keys'] == ['T'] ] if len(key_message) != 0: StudyLog.get_instance().write_event_log(key_message[0]) self.__on_all_test_nodes_clicked(None) if event.keyname == 'R' and not strg and not shift: key_message = [ v['msg'] for v in self.__key_messages if v['keys'] == ['R'] ] if len(key_message) != 0: StudyLog.get_instance().write_event_log(key_message[0]) self.__wall_divico.divico_control.reset() # For the interaction with the application if study mode is true. if self._study_handler: if event.keyname == 'H' and not strg and not shift: key_message = [ v['msg'] for v in self.__key_messages if v['keys'] == ['H'] ] if len(key_message) != 0: StudyLog.get_instance().write_event_log(key_message[0]) if event.keyname == 'Escape' and not strg and not shift: key_message = [ v['msg'] for v in self.__key_messages if v['keys'] == ['Escape'] ] if len(key_message) != 0: StudyLog.get_instance().write_event_log(key_message[0]) # Change the phase if shift and event.keyname in ['1', '2', '3', '4'] and not strg: self._study_handler.go_to_phase( self._study_handler.phases[int(event.keyname) - 1]) self.__task_text_view.text_alignment = "center" self.__task_text_view.font_colors = [] text = self._study_handler.current_block_name text = text if "Training" not in self._study_handler.current_phase else text + " ({})".format( self._study_handler.current_phase) self.__task_text_view.text_elements = [text] self.__task_text_view.draw() self.__phase_block_label.text = "#{} - {}: {}".format( config_app.SESSION, self._study_handler.current_phase, self._study_handler.current_block_name) key_message = [ v['msg'] for v in self.__key_messages if event.keyname in v['keys'] and 'shift' in v['keys'] ] if len(key_message) != 0: StudyLog.get_instance().write_event_log( key_message[0] + self.__phase_block_label.text) # Switch through the blocks of a phase. if event.keyname in ['Up', 'Down'] and not shift and not strg: self._study_handler.go_to_neighbor_block(-1 if event.keyname == 'Up' else 1) self.__task_text_view.text_alignment = "center" self.__task_text_view.font_colors = [] self.__task_text_view.text_elements = [ self._study_handler.current_block_name ] self.__task_text_view.draw() self.__phase_block_label.text = "#{} - {}: {}".format( config_app.SESSION, self._study_handler.current_phase, self._study_handler.current_block_name) key_message = [ v['msg'] for v in self.__key_messages if v['keys'] == [event.keyname] ] if len(key_message) != 0: StudyLog.get_instance().write_event_log( key_message[0] + self._study_handler.current_block_name) # Check/uncheck a task in a block. if event.keyname in [str(i) for i in range(1, 10) ] and not shift and not strg: if not (len(self.__task_text_view.text_elements) == 1 and self._study_handler.current_block_name in self.__task_text_view.text_elements[0]): index = int(event.keyname) - 1 if index < len(self.__task_text_view.text_elements): current_color = self.__task_text_view.font_colors[ index] new_color = colors.GREY_DARKEN_3 if current_color != colors.GREY_DARKEN_3 else self.__task_text_view.default_font_color self.__task_text_view.font_colors[index] = new_color self.__task_text_view.draw() if index < len(self._study_handler.current_tasks): key_message = [ v['msg'] for v in self.__key_messages if event.keyname in v['keys'] and 'strg' not in v['keys'] and 'shift' not in v['keys'] ] if len(key_message) != 0: StudyLog.get_instance().write_event_log( "{}{} - {}".format( key_message[0], event.keyname, self._study_handler.current_tasks[index]. encode('utf-8'))) # Go inside a block and show its task. if event.keyname == 'Return' and not shift and not strg: self.__task_text_view.text_elements = self._study_handler.current_tasks if "Guided" in self._study_handler.current_phase: self.__task_text_view.text_alignment = "left" elif "Free" in self._study_handler.current_phase: self.__task_text_view.text_alignment = "center" elif "Training" in self._study_handler.current_phase: self.__task_text_view.text_alignment = "left" user = "******" if self._study_handler.phases.index( self._study_handler.current_phase) == 0 else "N2" tasks = self._study_handler.current_tasks if user == "N1" else list( reversed(self._study_handler.current_tasks)) colors = [ (colors.AMBER if t.startswith(user) else colors.CYAN) for t in tasks ] tasks = [t[3:] for t in tasks] self.__task_text_view.font_colors = colors + [ colors.WHITE ] + colors self.__task_text_view.text_elements = tasks + [""] + tasks self.__task_text_view.draw() key_message = [ v['msg'] for v in self.__key_messages if v['keys'] == ['Return'] ] if len(key_message) != 0: StudyLog.get_instance().write_event_log( key_message[0] + self._study_handler.current_block_name)
def __on_tap(self): """ Called when a tap event has occurred. """ self.__last_tap_time = player.getFrameTime() StudyLog.get_instance().write_device_canvas_event(self.__device, "TAP")
def onFrame(self): if self.__wall_divico: self.__wall_divico.divico_control.on_frame() if config_app.study_mode: StudyLog.get_instance().on_frame()
def __update_position(self): if self.__mapping is PositionMapping.Orthogonal: self.__pos_screen = (float(self.__pos_space.pos_x) * self.__pixel_per_cm, float(self.__pos_space.pos_y) * self.__pixel_per_cm) # temp. calculation for surface in wall labor needed! # self.__pos_screen = (self.__pos_space.pos_x,self.__pos_space.pos_y) elif self.__mapping is PositionMapping.Interpolation: # pitch influences y value; yaw influences x value # dx = tan(self.pitch) * self.pos_space_z * -1 # *-1 because x direction is negative # pitch left wall corner: 0.8; right wall corner: -0.8 pitch = self.__one_euro_filter_pitch(self.pitch, time.time()) dx = self.map_value(pitch, config_app.pitch_min_max) # dy = tan(self.roll) * self.pos_space_z # top of wall: roll = -0.7; bottom of wall: 0.7 roll = self.__one_euro_filter_roll(self.roll, time.time()) dy = self.map_value(roll, config_app.roll_min_max) self.__pos_screen = ( float(self.__pos_space.pos_x) * self.__pixel_per_cm + dx, float(self.__pos_space.pos_y) * self.__pixel_per_cm + dy) elif self.__mapping in [ PositionMapping.Perspective, PositionMapping.Perspective_Weighted ]: # Get all necessary values. position = glm.vec3( self.pos_space_x, # Invert the space y coordinate because it was inverted in the optitrack_receiver before. config_app.display_height_cm - self.pos_space_y, self.pos_space_z) if self.__smoothing is SmoothingTyp.Quaternion: vec_orientation = glm.quat( self.__one_euro_filter_quat_1(self.__orientation_quat[0], time.time()), self.__one_euro_filter_quat_2(self.__orientation_quat[1], time.time()), self.__one_euro_filter_quat_3(self.__orientation_quat[2], time.time()), self.__one_euro_filter_quat_4(self.__orientation_quat[3], time.time())) else: vec_orientation = glm.quat(self.__orientation_quat[0], self.__orientation_quat[1], self.__orientation_quat[2], self.__orientation_quat[3]) if self.__mapping is PositionMapping.Perspective_Weighted: vec_orientation_unit = glm.quat(-1, 0, 0, 0) weight = (position.z - self.weighted_front_dist_cm) / ( self.weighted_back_dist_cm - self.weighted_front_dist_cm) weight = max(min(weight, 1), 0) vec_orientation = glm.quat.slerp(vec_orientation_unit, vec_orientation, weight) # -1 for the z coordinate to prevent that the device "stands" behind the wall. vec_rotation = vec_orientation * glm.vec3(0, 0, -position.z) # Calculate a factor for the rotation. if vec_rotation.z != 0: # -1 for the z coordinate to prevent that the device "stands" behind the wall. factor = -position.z / vec_rotation.z else: factor = -1 # Calculate the new pointing position. if factor <= 0: pos_pointed = position * -1 else: pos_pointed = position + (vec_rotation * factor) # Convert the cm to pixels. pos_pointed *= self.__pixel_per_cm # Invert the y position to match it with the wall coordinate system pos_pointed.y = config_app.app_resolution[1] - pos_pointed.y if self.__smoothing is SmoothingTyp.Pixel: # Interpolate the pixels. pos_pointed_inter = ((pos_pointed.x / config_app.app_resolution[0]), (pos_pointed.y / (config_app.app_resolution[1]))) pos_pointed_inter = (max(min(pos_pointed_inter[0], 1), 0), max(min(pos_pointed_inter[1], 1), 0)) # Use the filter on the interpolation. pos_pointed_inter = (self.__one_euro_filter_screen_x_inter( pos_pointed_inter[0], time.time()), self.__one_euro_filter_screen_y_inter( pos_pointed_inter[1], time.time())) # Calculate it back. pos_pointed = glm.vec3( config_app.app_resolution[0] * pos_pointed_inter[0], config_app.app_resolution[1] * pos_pointed_inter[1], 0) # Clamp the pointer position to the resolution of the application. # b1 = glm.vec3(0, config_app.app_resolution[1] / 12, 0) b1 = glm.vec3(0, 0, 0) b2 = glm.vec3(config_app.app_resolution[0], config_app.app_resolution[1] * 11.0 / 12.0, 0) if (pos_pointed.x < b1.x or pos_pointed.x > b2.x or pos_pointed.y < b1.y or pos_pointed.y > b2.y): pos_pointed.x = max(pos_pointed.x, b1.x) pos_pointed.x = min(pos_pointed.x, b2.x) pos_pointed.y = max(pos_pointed.y, b1.y) pos_pointed.y = min(pos_pointed.y, b2.y) self.__pos_screen = pos_pointed.x, pos_pointed.y for callback in self.__callbacks_device_moved: callback(sender=self, pos_space=self.__pos_space, pos_screen=self.__pos_screen) if config_app.study_mode: StudyLog.get_instance().write_device(self)