class SettingsScreen(Popup): sundial = ObjectProperty() # passed in if needed? location_field = ObjectProperty() close_button = ObjectProperty() feedback = ObjectProperty() config_latlon = ConfigParserProperty('', 'global', datahelpers.LOCATION_LATLON, 'app', val_type=str) config_friendlyname = ConfigParserProperty('', 'global', datahelpers.LOCATION_FRIENDLY, 'app', val_type=str) def __init__(self, **kwargs): super().__init__(**kwargs) self.close_button.disabled = self.config_latlon == '' self.location_field.text = self.config_friendlyname def on_config_latlon(self, instance, value): """Assumes this will only ever be set on a valid key""" self.close_button.disabled = False
class ConfigApp(App): number = ConfigParserProperty(0, 'general', 'number', 'app', val_type=float) text = ConfigParserProperty('', 'general', 'text', 'app', val_type=str) def build_config(self, config): config.setdefaults('general', {'number': 0, 'text': 'test'}) def build(self): return Builder.load_string(KV)
class DefaultNoteFilter(EventDispatcher): skip_open_strings = ConfigParserProperty(0, 'midi', 'ignore_open_strings', 'app', val_type=int) min_velocity = ConfigParserProperty(20, 'midi', 'min_note_velocity', 'app', val_type=int) note_queue = collections.deque(maxlen=3) # min_velocity = 20 open_string_min_velocity = 50 max_fret_distance = 6 max_seq_gap_millis = 250 # skip_open_strings = True def __init__(self, tuning, *args, **kwargs): super().__init__(*args, **kwargs) self.tuning = tuning def filter_note(self, message): # print("got message {} and skipping os {}".format(message, self.skip_open_strings)) now = current_time_millis() message.time = now if message.type == 'note_on': if (self.tuning.is_impossible_note(message.channel, message.note)): return if (self.tuning.is_open_string(message.channel, message.note) and (self.skip_open_strings > 0 or message.velocity < self.open_string_min_velocity)): return elif message.velocity < self.min_velocity: return else: if len(self.note_queue) > 0: last_msg = self.note_queue[-1] # print("the time diff is {} and dist {}".format( # now - last_msg.time, # self.tuning.get_distance(last_msg.channel, last_msg.note, message.channel, message.note) # )) if now - last_msg.time < self.max_seq_gap_millis and \ self.tuning.get_distance(last_msg.channel, last_msg.note, message.channel, message.note) > self.max_fret_distance: # print("skipping this interval cuz its wide...", self.tuning.get_distance(last_msg.channel, last_msg.note, message.channel, message.note), now - last_msg.time) return # if self.tuning.is_open_string(message.channel, message.note): # print("got open string {}".format(message)) self.note_queue.append(message) return message def get_note_queue(self): return list(self.note_queue)
class MainScreen(Screen): config_latlon = ConfigParserProperty('', 'global', datahelpers.LOCATION_LATLON, 'app', val_type=str) def __init__(self, **kwargs): super(MainScreen, self).__init__(**kwargs) # hacky config file validation for bad or missing coords # we have to store or it would draw under the dials settings_modal = None try: pt = Point(self.config_latlon) except ValueError: pt = Point() settings_modal = SettingsScreen() settings_modal.config_friendlyname = '' if self.config_latlon != '': settings_modal.feedback.text =\ "Invalid location detected in config, please select a new location" self.config_latlon = '' self.dial_widget = DialWidget(latlon_point=pt) self.now_marker = NowMarker() self.season_dial = SeasonDial() self.add_widget(self.dial_widget) self.add_widget(self.now_marker) self.add_widget(self.season_dial) # workaround for kivy's LIFO draw order if settings_modal is not None: Clock.schedule_once(lambda dt: settings_modal.open(), 0.5) def on_size(self, a, b): # Maintains a constant aspect ratio of 0.5625 (16:9) width, height = Window.size if not Window.fullscreen and (height / width) != 0.5625: height = width * 0.5625 width /= Metrics.density height /= Metrics.density Window.size = (width, height) def time_control_button(self): time_control_popup = TimeWizard(self.dial_widget, self) time_control_popup.open() def settings_button(self): SettingsScreen().open()
class GuessLocationButton(Button): """ Holds IP guessing logic for now. Putting all of it in screen caused weakref errors. """ settingsscreen = ObjectProperty() latlon = ConfigParserProperty('', 'global', datahelpers.LOCATION_LATLON, 'app', val_type=str) friendlyname = ConfigParserProperty('', 'global', datahelpers.LOCATION_FRIENDLY, 'app', val_type=str) def on_press(self): response = datahelpers.guess_location_by_ip() location_field = self.settingsscreen.location_field feedback = self.settingsscreen.feedback if response is None: feedback.text = "Error when connecting to geoip server" return self.disabled = True self.latlon = response[datahelpers.LOCATION_LATLON] if datahelpers.LOCATION_FRIENDLY in response: location_field.text = response[datahelpers.LOCATION_FRIENDLY] self.friendlyname = response[datahelpers.LOCATION_FRIENDLY] else: location_field.text = self.latlon self.friendlyname = self.latlon feedback.text = "Successfully got location!" Clock.schedule_once(lambda dt: setattr(self, 'disabled', False), timeout=45 / 60) Clock.schedule_once(lambda dt: setattr(feedback, 'text', ""), timeout=5)
class UpdateLocationButton(Button): """ Button for updating location. """ settingsscreen = ObjectProperty() latlon = ConfigParserProperty('', 'global', datahelpers.LOCATION_LATLON, 'app', val_type=str) friendlyname = ConfigParserProperty('', 'global', datahelpers.LOCATION_FRIENDLY, 'app', val_type=str) latlong_regex = re.compile(r'^(\-?\d+(\.\d+)?),\s*(\-?\d+(\.\d+)?)$') def on_press(self): input_location = self.settingsscreen.location_field.text feedback = self.settingsscreen.feedback # Use a regex to check if they entered a lat and long manually latlong_re = self.latlong_regex.match(input_location) if latlong_re: lat, _, long, _ = latlong_re.groups() self.latlon = f'{lat},{long}' self.friendlyname = f'{lat},{long}' feedback.text = 'Successfully set Latitude and Longitude' return geolocator = Nominatim(user_agent="interactive_python_session_client") resp = geolocator.geocode(input_location) # Possible that location isn't found if not resp: feedback.text = 'Failed to find location, try again!' return self.friendlyname = resp.address self.latlon = f'{resp.latitude},{resp.longitude}' feedback.text = f'Successfully set location to {resp.address}'
class SettingDispatcher(EventDispatcher): auto_save = ConfigParserProperty(0, 'application', 'auto_save', 'kivystudio', val_type=int) auto_emulate = ConfigParserProperty(1, 'application', 'auto_emulate', 'kivystudio', val_type=int) dpi_scale = ConfigParserProperty(1, 'graphics', 'dpi_scale', 'kivystudio', val_type=float)
class TermsPopup(PolicyPopup): """ Display terms and conditions for using the app. """ is_first_run = ConfigParserProperty('1', 'General', 'is_first_run', 'app', val_type=int) def __init__(self, **kwargs): app = App.get_running_app() self.__reject_btn = MDRectangleFlatButton(text=_("DECLINE"), on_release=app.manager.quit) self.__accept_btn = MDRaisedButton(text=_("ACCEPT"), on_release=self.dismiss) # Show Terms and Privacy Policy together on first run. if self.is_first_run: text = self._get_text(get_terms()) + "\n\n" + self._get_text( get_policy()) else: text = self._get_text(get_terms()) content = RecycleLabel(text=text, halign='justify') content.bind( on_ref_press=lambda instance, value: self.open_link(value)) default_kwargs = dict( content_cls=content, buttons=[ self.__reject_btn, self.__accept_btn, ], ) default_kwargs.update(kwargs) super(TermsPopup, self).__init__(**default_kwargs) def on_dismiss(self): if self.is_first_run: self.is_first_run = 0 def on_pre_open(self): # When the terms are dismissed the first time, it means they were accepted. if not self.is_first_run: self.content_cls.text = self._get_text(get_terms()) self.ids.button_box.remove_widget(self.__reject_btn) self.__accept_btn.text = _("CLOSE") self.content_cls.ids.rv.scroll_y = 1
class TextureOutput(EventDispatcher): stretch_type = ConfigParserProperty(None, 'view', 'stretch', 'app', val_type=StretchType) do_stretch = BooleanProperty(False) _stretch = { StretchType.LINEAR: LinearStretch(), StretchType.SQRT: SqrtStretch(), # StretchType.POWER: PowerStretch(), StretchType.ASIN: AsinhStretch() } def __init__(self): super(TextureOutput, self).__init__() self.register_event_type('on_update') self.texture = Texture.create(size=(320, 240)) def write(self, buf): if self.do_stretch: buf = self.stretch(buf) self._blit(buf) @mainthread def _blit(self, buf): """ write buffer to texture and fire event :param buf: :return: """ self.texture.blit_buffer(buf, colorfmt='rgb', bufferfmt='ubyte') self.dispatch('on_update', buf) def on_update(self, buf): pass def stretch(self, buf): """ perform the currently selected stretch method on buffer :param buf: :return: """ im = np.frombuffer(buf, dtype=np.uint8) im = self._stretch[self.stretch_type](im / 255.0) * 255.0 return im.astype(np.uint8).tobytes()
class PrintScreen(Screen): interval = ConfigParserProperty(5, 'server', 'job_interval', 'app', val_type=int) job_loaded = BooleanProperty(False) job_completion = NumericProperty(0.0, allownone=True) print_time = NumericProperty(0.0, allownone=True) print_time_left = NumericProperty(0.0, allownone=True) file_name = StringProperty('Nothing') def __init__(self, **kw): super(PrintScreen, self).__init__(**kw) self.octoprint = None def init(self, octoprint): self.octoprint = octoprint def on_interval(self, _, value): Clock.unschedule(self.update) Clock.schedule_interval(self.update, value) def on_enter(self, *args): if self.octoprint is not None: self.update() Clock.schedule_interval(self.update, self.interval) def on_leave(self, *args): Clock.unschedule(self.update) def update(self, *_): job = self.octoprint.get_job() connection = self.octoprint.get_connection() if job: state = PrinterState.parse(job=job, connection=connection) self.job_loaded = state.job_loaded self.job_completion = state.completion self.print_time = state.print_time self.print_time_left = state.print_time_left self.file_name = state.file_name print('job update: ', state)
class ImgFileName(BoxLayout): CANON_REGEXP = re.compile(r'IMG_([0-9]+).JPG') NIKON_REGEXP = re.compile(r'DSC_([0-9]+).JPG') last_image = ConfigParserProperty('', 'tracker', 'last_image', 'app') def last_image_inc(self, step=1): m = self.CANON_REGEXP.match(self.last_image) if m: n = int(m.group(1)) n += step self.last_image = 'IMG_%d.JPG' % n m = self.NIKON_REGEXP.match(self.last_image) if m: n = int(m.group(1)) n += step self.last_image = 'DSC_%04d.JPG' % n def load_image(self): pass
class LoadDialog(FloatLayout): """A video file chooser dialog. The user can select a video file which will be used for predicting the groundtruth exercise data. Attributes ----------- save_from_video : `kivy.properties.ObjectProperty` Initiates the `predict` action. cancel : `kivy.properties.ObjectProperty` Closes the displayed dialog. video_input_path : `kivy.properties.ConfigParserProperty` The default starting directory of the dialog. """ save_from_video = ObjectProperty(None) cancel = ObjectProperty(None) video_input_path = ConfigParserProperty( "/home/ziposc/Videos", "ExercisorEditor", "video_input_path", "Exercisor" )
class ExercisorScreen(Screen): """The main screen of the Exercisor Widget""" config = ConfigParser("Exercisor") exercises_path = ConfigParserProperty( "./widgets/exercisor/exercise_data/", "Exercisor", "exercises_path", "Exercisor" ) obj_mesh_path = ConfigParserProperty( "./widgets/exercisor/play/monkey.obj", "Exercisor", "obj_mesh_path", "Exercisor" ) def __init__(self, **kwargs): # Load the kv files path = os.path.dirname(os.path.abspath(__file__)) Builder.load_file(os.path.join(path, "controls.kv")) Builder.load_file(os.path.join(path, "play", "player.kv")) Builder.load_file(os.path.join(path, "editor", "editor.kv")) super().__init__(**kwargs) def on_kv_post(self, base_widget): # Create the SMPL and HMR threads self.mlthreads = {} self.mlthreads["hmr"] = HMRThread(model_cfg, self.save_exercise) self.mlthreads["smpl"] = SMPLThread( model_cfg.smpl_model_path, model_cfg.joint_type ) # Create the renderer widgets for the SMPL and HMR treads and for the error vectors kwargs = { "smpl_faces_path": model_cfg.smpl_faces_path, "keypoints_spec": model_cfg.keypoints_spec, "obj_mesh_path": self.obj_mesh_path, } renderers = [Renderer(**kwargs), Renderer(**kwargs), Renderer(**kwargs)] for rend in renderers: self.ids.renderer_layout.add_widget(rend) # Create the color adjustment dialog and bind the color change function self.color_adjust_dialog = ColorAdjustDialog() self.color_adjust_dialog.bind( diffuse_light_color=partial(self.on_color_change, "diffuse") ) self.color_adjust_dialog.bind( object_color=partial(self.on_color_change, "object") ) self.color_adjust_dialog.object_color = (0.972, 0.866, 0.898) self.color_adjust_dialog.diffuse_light_color = (0.9, 0.9, 0.9) # Create the controlling classes of the thread's and renderers' state self.actions = { "playback": PlaybackAction( self.mlthreads["smpl"], renderers[0], model_cfg.keypoints_spec ), "predict": PredictAction(self.mlthreads["hmr"], renderers[0]), "play": PlayAction(self.mlthreads, renderers), } self.exercise_controller = ExerciseController(self.exercises_path) self.change_control("normal") # Displays the control buttons def update_config(self): # Documentation for settings # self.config.read('config_path') pass def subscribe(self): """Export the functions for the wit.ai integration""" wit_wrap = WitWrapper(self) exported_functions = wit_wrap.export_functions() return exported_functions def change_control(self, state): """Change the buttons at the bottom center of the window""" self.ids.ctrl_btn.state = state if state == "down": edit_actions = {key: self.actions[key] for key in ("playback", "predict")} self.controls = EditorControls(edit_actions, self.ids.info_label) else: play_actions = {key: self.actions[key] for key in ("play", "predict")} self.controls = PlayControls(play_actions, self.ids.info_label) def _stop_actions(self): for action in self.actions.values(): action.stop() def toggle_color_adjust(self, state): """Toggle the ColorAdjustment dialog""" if state == "down": self.ids.renderer_layout.add_widget(self.color_adjust_dialog) else: self.ids.renderer_layout.remove_widget(self.color_adjust_dialog) def on_color_change(self, color_type, _, new_color): """Change the color of the rendered meshes. Parameters ---------- color_type : {'object', 'diffuse'} Specify the type of the color to change, either the color of the object or the diffuse lighting color new_color : tuple of floats in range [0, 1] The new rgb values for the specified `color_type` """ for renderer in self.ids.renderer_layout.walk(restrict=True): if type(renderer) == Renderer: if color_type == "object": renderer.canvas["object_color"] = new_color elif color_type == "diffuse": renderer.canvas["diffuse_light"] = new_color def save_exercise(self, exercise_name, thetas): """Save the thetas of the exercise and update the list of available exercises Parameters ---------- exercise_name : `str` The name of the file to will be saved thetas : `numpy.ndarray`, (N x 82) The 82 theta parameters of the smpl model for each frame """ with open(f"{self.exercises_path}/{exercise_name}.npy", "w+b") as f: np.save(f, thetas) self.exercise_controller.exercises[exercise_name] = thetas self.exercise_controller.notify() self.ids.info_label.text = ( f"Exercise {exercise_name} has been saved to {self.exercises_path}" ) self._stop_actions() @property def controls(self): """The BoxLayout widget that holds the buttons at the bottom center of the screen. When set, update the :attr: controls of each of the :class: AbstractAction. """ return self._controls @controls.setter def controls(self, new_controls): # Remove the old controls from the observers list try: self.exercise_controller.detach(self._controls) except AttributeError: pass self._controls = new_controls # Add the controls to the parent layout self.ids.controls_layout.clear_widgets() self.ids.controls_layout.add_widget(self._controls) # Reset and update the actions self._stop_actions() for action in self.actions.values(): action.controls = self._controls # Subscribe for exercises' change self.exercise_controller.attach(self._controls) self.exercise_controller.notify() self.ids.info_label.text = "Waiting to choose an exercise.."
class AppWindow(BoxLayout): height_ratio = ConfigParserProperty(0.0, 'window', 'height_ratio', 'app', val_type=float) initial_width = ConfigParserProperty(0, 'window', 'initial_width', 'app', val_type=int) initial_screen_loc_x = ConfigParserProperty(0, 'window', 'initial_screen_loc_x', 'app', val_type=int) initial_screen_loc_y = ConfigParserProperty(0, 'window', 'initial_screen_loc_y', 'app', val_type=int) maximized = ConfigParserProperty(0, 'window', 'maximized', 'app', val_type=int) midi_port = ConfigParserProperty(0, 'midi', 'midi_port', 'app') midi_output_port = ConfigParserProperty(0, 'midi', 'midi_output_port', 'app') def __init__(self, midi_player, **kwargs): super(AppWindow, self).__init__(**kwargs) self.midi_player = midi_player self.orientation = 'vertical' self.tuning = P4Tuning( int( ConfigParser.get_configparser('app').get( 'fretboard', 'num_frets'))) self.note_filter = DefaultNoteFilter(self.tuning) if self.midi_port: self.midi_config = Midi(self.midi_player, self.note_filter, self.midi_port, self.midi_message_received, self.midi_output_port) self.scale_config = Scales() self.chords_config = Chords() self.patterns_config = Patterns() with self.canvas: Window.size = (self.initial_width, self.initial_width * self.height_ratio) Window.left = self.initial_screen_loc_x Window.top = self.initial_screen_loc_y Window.clearcolor = (1, 1, 1, 1) if self.maximized: Window.maximize() self.rect = Rectangle(pos=self.pos, size=self.size, group='fb') pattern_mapper = P4TuningPatternMatcher(self.tuning, self.chords_config, self.scale_config, self.patterns_config) self.fretboard = Fretboard(tuning=self.tuning, pattern_mapper=pattern_mapper, pos_hint={ 'x': 0, 'y': 0 }, size_hint=(1, 0.3)) self.player_panel = PlayerPanel(fretboard=self.fretboard, midi_config=self.midi_config, size_hint=(1, 1)) self.note_trainer_panel = NoteTrainerPanel( fretboard=self.fretboard, midi_config=self.midi_config, tuning=self.tuning, size_hint=(1, 1)) self.menu_panel = MenuPanel(fretboard=self.fretboard, player_panel=self.player_panel, note_trainer_panel=self.note_trainer_panel, size_hint=(1, 0.7)) self.add_widget(self.menu_panel) self.add_widget(self.fretboard) with self.canvas.before: pass with self.canvas.after: pass self.bind(size=self.size_changed) Window.bind(on_maximize=lambda x: App.get_running_app().config.set( 'window', 'maximized', 1)) Window.bind(on_restore=lambda x: App.get_running_app().config.set( 'window', 'maximized', 0)) # self.init_midi() def size_changed(self, *args): self.rect.pos = self.pos self.rect.size = self.size print("pos is {}, size is {}".format(self.pos, self.size)) App.get_running_app().config.set('window', 'initial_screen_loc_x', self.pos[0]) App.get_running_app().config.set('window', 'initial_screen_loc_y', self.pos[1]) App.get_running_app().config.set('window', 'initial_width', self.size[0]) App.get_running_app().config.set('window', 'height_ratio', self.size[1] / self.size[0]) def init_midi(self): self.midi_config.open_input() self.midi_config.open_output() def reload_midi(self): self.midi_config.set_default_input_port(self.midi_port, open_port=True) self.midi_config.set_default_output_port(self.midi_output_port, open_port=True) def shutdown_midi(self): if self.midi_player: self.midi_player.stop() if self.midi_config: self.midi_config.shutdown() def reload_scales(self): self.scale_config.load_scales() # import time # last_time = time.time()*1000.0 def midi_message_received(self, midi_note, channel, on, time=None): if on: # print('midi!!! ({}, {}, {}, {})'.format(midi_note, channel, on, time - self.last_time)) self.fretboard.midi_note_on(midi_note, channel, time) # if self.midi_config: # last_notes = self.midi_config.note_filter.get_note_queue() # if last_notes: # pass # self.fretboard.show_pattern() else: self.fretboard.midi_note_off(midi_note, channel)
class BLEApp(App): scan_results = DictProperty({}) visus = DictProperty({}) error_log = StringProperty('') sensor_list = ListProperty(['rx', 'ry', 'rz', 'ax', 'ay', 'az']) auto_activate = ConfigParserProperty(False, 'general', 'auto_activate', 'app', val_type=configbool) auto_display = ConfigParserProperty(False, 'general', 'auto_display', 'app', val_type=configbool) device_filter = ConfigParserProperty('', 'general', 'device_filter', 'app', val_type=str) nexus4_fix = ConfigParserProperty(False, 'android', 'nexus4_fix', 'app', val_type=configbool) osx_queue_fix = ConfigParserProperty(False, 'osx', 'osx_queue_fix', 'app', val_type=configbool) display_timeout = ConfigParserProperty(10, 'general', 'display_timeout', 'app', val_type=int) def build(self): # uncomment these lines to use profiling # if __name__ != '__main__': # self.root = Builder.load_file('ble.kv') self.scanner = None self.init_ble() self.set_scanning(True) self.osc_socket = socket(AF_INET, SOCK_DGRAM) if rtmidi2: self.midi_out = rtmidi2.MidiOut().open_virtual_port(':0') Clock.schedule_interval(self.clean_results, 1) if '--simulate' in sys.argv: if NO_SIMULATE: raise NO_SIMULATE Clock.schedule_once(self.simulate_twiz, 0) return super(BLEApp, self).build() def on_pause(self, *args): return True def build_config(self, config): config.setdefaults('general', { 'auto_activate': False, 'auto_display': False }) config.setdefaults('android', { 'nexus4_fix': False, }) config.setdefaults('osx', { 'osx_queue_fix': False, }) def build_settings(self, settings): settings.add_json_panel('Twiz-manager', self.config, 'twiz_manager.json') def on_stop(self, *args): print "writing config" self.config.write() print "config written" def open_content_dropdown(self, text_input): options = { 'euler angles (0-0xffff)': 'rx,ry,rz', 'euler angles (0-1.0)': 'rx_d,ry_d,rz_d', 'accelerations (0-0xffff)': 'ax,ay,az', 'accelerations (0-1.0)': 'ax_d,ay_d,az_d', 'accelerations + euler (0-0xffff)': 'ax,ay,az,rx,ry,rz', 'accelerations + euler (0-1.0)': 'ax_d,ay_d,az_d,rx_d,ry_d,rz_d', } #d = DropDown(width=text_input.width) #for o in options: # b = Button(text=o, size_hint_y=None) # b.bind(texture_size=b.setter('size')) # b.bind(on_press=lambda x: text_input.setter('text')(options[o])) # d.add_widget(b) #d.open(text_input) p = Popup(title='message content', size_hint=(.9, .9)) def callback(option): text_input.text = options.get(option, option) p.dismiss() content = GridLayout(spacing=10, cols=1) for o in options: b = Button(text=o) b.bind(on_press=lambda x: callback(x.text)) content.add_widget(b) instructions = Label( text='custom content:\n two types of sensors are ' 'proposed, rotation (euler angles) and acceleration, each ' 'in 3 axis: rx, ry and rz represent rotation values, ax, ' 'ay and az represent acceleration values, any value can ' 'take a "_d" suffix, to be casted to a value between 0 ' 'and 1 instead of the default (from 0 to 0xffff', size_hint_y=None) instructions.bind(size=instructions.setter('text_size'), texture_size=instructions.setter('size')) content.add_widget(instructions) ti = TextInput(text=text_input.text, multiline=False, input_type='text', keyboard_suggestions=False) content.add_widget(ti) b = Button(text='set custom') b.bind(on_press=lambda x: callback(ti.text)) content.add_widget(b) p.add_widget(content) p.open() def clean_results(self, dt): # forget devices after N seconds without any update if not self.display_timeout: return t = time() - self.display_timeout for k, v in self.scan_results.items(): if v.last_update < t: self.scan_results.pop(k) self.root.ids.scan.ids.results.remove_widget(v) self.remove_visu(v) def on_osx_queue_fix(self, *args): self.scanner.queue = [] if self.osx_queue_fix else None def init_ble(self): if platform == 'android': self.scanner = AndroidScanner() self.scanner.callback = self.android_parse_event elif platform == 'macosx': self.scanner = Ble() self.scanner.create() self.scanner.callback = self.osx_parse_event self.scanner.queue = [] if self.osx_queue_fix else None else: try: self.scanner = LinuxBle(callback=self.update_device) except OSError: print "unable to get a ble device, try using the simulator" def simulate_twiz(self, dt): self.root.ids.scan.add_widget(TwizSimulator()) def filter_scan_result(self, result): return self.device_filter.strip().lower() in result.lower() def restart_scanning(self, dt): self.scanning_active = not self.scanning_active if self.scanning_active: stop_scanning(self.scanner) else: start_scanning(self.scanner) def set_scanning(self, value): if not self.scanner: return if platform == 'android': if value: start_scanning(self.scanner) if app.nexus4_fix: self.scanning_active = True Clock.schedule_interval(self.restart_scanning, .05) else: stop_scanning(self.scanner) Clock.unschedule(self.restart_scanning) elif platform == 'macosx': self.scanner.start_scan() else: if value: # hci_le_set_scan_parameters(sock) self.scanner.start() else: self.scanner.stop() def ensure_sections(self, device): section = device.name + '-osc' if not self.config.has_section(section): app.config.add_section(section) app.config.setdefaults(section, {}) section = device.name + '-midi' if not self.config.has_section(section): app.config.add_section(section) app.config.setdefaults(section, {k: '0,0,0,0,v' for k in app.sensor_list}) def add_visu(self, device): self.ensure_sections(device) w = ObjectView(device=device) self.visus[device] = w self.root.ids.visu.ids.content.add_widget(w) def remove_visu(self, device): w = self.visus.get(device) if w and w in self.root.ids.visu.ids.content.children: self.root.ids.visu.ids.content.remove_widget(w) del (self.visus[device]) gc.collect() @mainthread def update_device(self, data): name = data.get('name', '') pd = self.scan_results.get(name) if not pd: pd = TwizDevice() pd.update_data(data) results = self.root.ids.scan.ids.results if app.auto_display: pd.display = True if pd.name not in self.scan_results: self.scan_results[pd.name] = pd results.add_widget(pd) def decode_data(self, pkt): pkt = pack('<' + 'b' * len(pkt), *pkt.tolist()) local_name_len, = unpack("B", pkt[0]) dtype = 0 offset = 1 + local_name_len sensor_data = None while offset < len(pkt): dlen, dtype = unpack('BB', pkt[offset:offset + 2]) if dtype == 0xff: sensor_data = unpack('>' + 'h' * ((dlen - 3) // 2), pkt[offset + 4:offset + dlen + 1]) break offset += dlen + 1 return sensor_data def android_parse_event(self, name, address, irssi, data): if not name or not self.filter_scan_result(name): return device_data = { 'name': name, 'power': irssi, } try: sensor = self.decode_data(data) except: self.error_log += 'error decoding data from %s:%s\n' % ( name, unpack('<' + 'B' * len(data), pack('<' + 'b' * len(data), data))) if sensor: device_data['sensor'] = sensor self.update_device(device_data) def osx_parse_event(self, rssi, name, values): if len(values) > 12: values = values[2:] device_data = { 'name': name, 'power': rssi, 'sensor': unpack('>' + 'h' * (len(values) / 2), ''.join(values)), } self.update_device(device_data)
class SettingsCircleTask(Widget): """ Circle Task settings and properties. """ n_trials = ConfigParserProperty('30', 'CircleTask', 'n_trials', 'app', val_type=int, verify=lambda x: x > 0, errorvalue=20) n_blocks = ConfigParserProperty('3', 'CircleTask', 'n_blocks', 'app', val_type=int, verify=lambda x: x > 0, errorvalue=3) n_practice_trials = ConfigParserProperty('5', 'CircleTask', 'n_practice_trials', 'app', val_type=int, verify=lambda x: x >= 0, errorvalue=5) constrained_block = ConfigParserProperty('2', 'CircleTask', 'constrained_block', 'app', val_type=int) warm_up = ConfigParserProperty('1.0', 'CircleTask', 'warm_up_time', 'app', val_type=float, verify=lambda x: x > 0.0, errorvalue=1.0) trial_duration = ConfigParserProperty('2.0', 'CircleTask', 'trial_duration', 'app', val_type=float, verify=lambda x: x > 0.0, errorvalue=1.0) cool_down = ConfigParserProperty('0.5', 'CircleTask', 'cool_down_time', 'app', val_type=float, verify=lambda x: x > 0.0, errorvalue=0.5) email_recipient = ConfigParserProperty('', 'CircleTask', 'email_recipient', 'app', val_type=str) researcher = ConfigParserProperty('', 'CircleTask', 'researcher', 'app', val_type=str) def __init__(self, **kwargs): super(SettingsCircleTask, self).__init__(**kwargs) self.constraint = False self.constraint_type = 0 # 0 = no constraint, 1 = single constraint, 2 = both constrained self.practice_block = 0 def set_practice_block(self, block): """ Decide which practice block we're on. """ # We don't count practice blocks when no practice trials are set. if not self.n_practice_trials: return # Don't advance practice_block when the current block gets reset to 0. if 0 < block <= 3: self.practice_block += 1 # If we've done our 2 practice blocks, we're ready for the big leagues. if self.practice_block > 2 or block == 0: self.practice_block = 0 def set_constraint_setting(self, block): # Second practice block and adjusted constrained block. self.constraint = (self.practice_block == 2) \ or (block == (self.constrained_block + bool(self.n_practice_trials) * 2)) def on_new_block(self, new_block): # Choose a constraint type for this run and stick with it, so practice and test constraint match. if not self.constraint_type: self.constraint_type = np.random.choice((1, 2)) # Reset constraint type with new session. if new_block == 0: self.constraint_type = 0 self.set_practice_block(new_block) self.set_constraint_setting(new_block)
class PiCameraFileBrowser(BoxLayout): customrootpath = ConfigParserProperty('./DCIM', 'HQCam', 'picture_folder', 'app') def __init__(self, **kwargs): super(PiCameraFileBrowser, self).__init__(**kwargs) self.imageHelper = ImageHelper() def selected(self, filename): try: image_file = self.imageHelper.get_image(filename[0]) if image_file: # If a video was shown before if hasattr(self.videopreviewer, 'saved_attrs'): self.videopreviewer.state = "stop" self.videopreviewer.height, self.videopreviewer.size_hint_y, self.videopreviewer.opacity, self.videopreviewer.disabled = 0, None, 0, True self.filepreviewer.height, self.filepreviewer.size_hint_y, self.filepreviewer.opacity, self.filepreviewer.disabled = self.filepreviewer.saved_attrs self.filepreviewer.source = self.imageHelper.process_for_thumbnail( filename[0]) self.filename.text = self.imageHelper.extract_name(filename[0]) exifData = image_file._getexif() if exifData: self.filedate.text = image_file._getexif()[ 36867] # Date Picture was taken else: self.filedate.text = time.ctime( os.path.getmtime(filename[0])) self.filerez.text = "{} x {}".format(image_file.size[0], image_file.size[1]) except PIL.UnidentifiedImageError: try: Logger.warn( "PiCameraFileBrowser: selected: Unsupported File support for file {} - Trying Video" .format(filename[0])) # Hide Image File Previewer if not hasattr(self.filepreviewer, 'saved_attrs'): self.filepreviewer.saved_attrs = self.filepreviewer.height, self.filepreviewer.size_hint_y, self.filepreviewer.opacity, self.filepreviewer.disabled self.filepreviewer.height, self.filepreviewer.size_hint_y, self.filepreviewer.opacity, self.filepreviewer.disabled = 0, None, 0, True self.filepreviewer.source = "" # Show Video File Previewer self.videopreviewer.height, self.videopreviewer.size_hint_y, self.videopreviewer.opacity, self.videopreviewer.disabled = self.filepreviewer.saved_attrs if not hasattr(self.videopreviewer, 'saved_attrs'): self.videopreviewer.saved_attrs = self.filepreviewer.saved_attrs self.videopreviewer.source = filename[0] self.videopreviewer.state = "play" self.filename.text = self.imageHelper.extract_name(filename[0]) self.filerez.text = "" self.filedate.text = time.ctime(os.path.getmtime(filename[0])) except: exc_type, exc_obj, exc_tb = sys.exc_info() fname = os.path.split(exc_tb.tb_frame.f_code.co_filename)[1] Logger.error( "PiCameraFileBrowser: selected: {} / {} / {} / {}".format( exc_type, exc_obj, fname, exc_tb.tb_lineno)) def do_delete_image(self, *args): if len(self.filechooser.selection) == 1: path = self.filechooser.selection[0] try: if os.path.exists(path): os.remove(path) self.filechooser._update_files() if len(self.filechooser._items) > 0: self.filechooser._items[0].is_selected = True self.filechooser.selection = [ self.filechooser._items[0].path, ] except: Logger.error("PiCameraFileBrowser: do_delete_image: {}".format( sys.exc_info()[0]))
class UiManager(ScreenManager): """ This class handles all major screen changes, popups and callbacks related to that. """ settings = ObjectProperty() sidebar = ObjectProperty(None, allownone=True) # Different kinds of popups, objects stay in memory and only show with replaced content. # To avoid garbage collection of Popups and resulting ReferenceError because of translations, keep a reference. popup_language = ObjectProperty(None, allownone=True) popup_terms = ObjectProperty(None, allownone=True) popup_info = ObjectProperty(None, allownone=True) popup_warning = ObjectProperty(None, allownone=True) popup_error = ObjectProperty(None, allownone=True) popup_user_select = ObjectProperty(None, allownone=True) popup_user_edit = ObjectProperty(None, allownone=True) popup_user_remove = ObjectProperty(None, allownone=True) popup_about = ObjectProperty(None, allownone=True) popup_privacy = ObjectProperty(None, allownone=True) # is_first_run = ConfigParserProperty('1', 'General', 'is_first_run', 'app', val_type=int) orientation = ConfigParserProperty('portrait', 'General', 'orientation', 'app', val_type=str, force_dispatch=True) def __init__(self, **kwargs): super(UiManager, self).__init__(**kwargs) self.app = App.get_running_app() # Keep track of where we cae from. self.last_visited = 'Home' # Keep track of what study we're currently performing. self.task_consents = {'Circle Task': 'Consent CT'} self.task_instructions = {'Circle Task': 'Instructions CT'} # Events self.register_event_type('on_info') self.register_event_type('on_warning') self.register_event_type('on_error') self.register_event_type('on_invalid_session') self.register_event_type('on_upload_response') self.register_event_type('on_upload_successful') def on_kv_post(self, base_widget): Window.bind(on_keyboard=self.key_input) # Binds need to be executed 1 frame after on_kv_post, otherwise it tries to bind to not yet registered events. Clock.schedule_once(lambda dt: self.bind_sidebar_callbacks(), 1) if self.is_first_run: Clock.schedule_once(lambda dt: self.show_popup_language(), 1) # Doesn't open otherwise. def bind_sidebar_callbacks(self): """ Handle events in navigation drawer. """ # Handle sidebar item callbacks. root = self.parent.parent nav = root.ids.content_drawer nav.bind( on_home=lambda x: self.go_home(), on_users=lambda x: self.show_user_select(), on_settings=lambda x: self.open_settings(), on_website=lambda x: self.open_website(self.settings.server_uri), on_about=lambda x: self.show_about(), on_terms=lambda x: self.show_terms(), on_privacy_policy=lambda x: self.show_privacy_policy(), on_exit=lambda x: self.quit(), ) def bind_screen_callbacks(self, screen_name): """ Handle screen callbacks here. """ # Circle if screen_name == 'Consent CT': self.get_screen(screen_name).bind( on_consent=lambda instance: self.show_instructions()) if screen_name == 'Instructions CT': self.get_screen(screen_name).bind( on_proceed=lambda instance: self.start_task()) elif screen_name == 'Circle Task': self.get_screen(screen_name).bind( on_task_stopped=lambda instance, is_last_block: self. task_finished(is_last_block), on_warning=lambda instance, text: self.show_warning(text), ) # Webview elif screen_name == 'Webview': self.get_screen(screen_name).bind( on_quit_screen=lambda instance: self.go_home()) def show_instructions(self): """ Show screen with instructions for current task. """ # Switch to landscape without changing config. # Constantly switching orientation between instructions and task is really annoying and can lead to freezes. try: plyer.orientation.set_landscape() except (NotImplementedError, ModuleNotFoundError): pass # Advance to the instructions. self.transition.direction = 'up' self.transition.duration = 0.5 self.current = self.task_instructions[self.settings.current_task] # Collect demographic data. Clock.schedule_once(lambda dt: self.show_popup_demographics(), 1) def start_task(self): """ Change to screen with current task. """ self.transition.direction = 'up' self.transition.duration = 0.5 self.current = self.settings.current_task def task_finished(self, was_last_block=False): # Outro after last block. self.transition.direction = 'down' if was_last_block: # Set orientation back to config value. self.on_orientation(None, self.orientation) self.current = 'Outro' else: self.current = self.task_instructions[self.settings.current_task] def toggle_orientation(self, instance): """ Toggles between portrait and landscape screen orientation. """ # Update the icon. instance.icon = f'phone-rotate-{self.orientation}' # Switch orientation. if self.orientation == 'portrait': self.orientation = 'landscape' elif self.orientation == 'landscape': self.orientation = 'portrait' def on_orientation(self, instance, value): try: if value == 'landscape': plyer.orientation.set_landscape() elif value == 'portrait': plyer.orientation.set_portrait() except (NotImplementedError, ModuleNotFoundError): pass def open_settings(self): """ Leads to display_settings. """ self.app.open_settings() self.sidebar.set_state('close') def display_settings(self, settings): """ Show the settings screen. :param settings: Settings Widget to add to screen. """ if not self.has_screen('Settings'): s = BaseScreen(name='Settings', navbar_enabled=True) s.add_widget(settings) self.add_widget(s) self.transition.direction = 'right' self.transition.duration = 0.5 self.current = 'Settings' def close_settings(self): """ Change the screen to where we came from. """ self.transition.direction = 'left' self.transition.duration = 0.5 self.current = self.last_visited def open_website(self, url): webbrowser.open_new(url) def show_popup_language(self): """ Popup for language choice on first run of the app. """ if not self.popup_language: self.popup_language = LanguagePopup() self.popup_language.bind(on_language_set=self.on_language_set) self.popup_language.open() def on_language_set(self, *args): """ After language was set for first run. """ self.show_terms() # Once we accepted the terms, we don't want the language popup's on_current_language event to trigger the terms. self.remove_popup_language() self.get_screen('Home').set_text() def remove_popup_language(self): if self.popup_language: self.popup_language.unbind(on_language_set=self.on_language_set) #self.popup_language = None def show_terms(self): if not self.popup_terms: self.popup_terms = TermsPopup() self.popup_terms.open() def show_privacy_policy(self): if not self.popup_privacy: self.popup_privacy = PolicyPopup() self.popup_privacy.open() def show_user_select(self): if not self.popup_user_select: self.popup_user_select = UsersPopup() self.popup_user_select.bind( on_add_user=lambda instance: self.show_user_edit( add=True, user_alias=_("New User")), on_edit_user=lambda instance, user_id, alias: self. show_user_edit(user_id=user_id, user_alias=alias), on_remove_user=lambda instance, user_id, alias: self. show_user_remove(user_id, alias), ) self.popup_user_select.open() def show_user_edit(self, add=False, user_id=None, user_alias=''): if not self.popup_user_edit: self.popup_user_edit = UserEditPopup() self.popup_user_edit.bind(on_user_edited=self.settings.edit_user) self.popup_user_edit.open(add=add, user_id=user_id, user_alias=user_alias) def show_user_remove(self, user_id, user_alias): if not self.popup_user_remove: self.popup_user_remove = ConfirmPopup() # Now do some hacky stuff. ;) setattr(self.popup_user_remove, 'user_id', user_id) self.popup_user_remove.bind( on_confirm=lambda instance: self.settings.remove_user( self.popup_user_remove.user_id)) self.settings.bind(on_user_removed=lambda instance, user: self. popup_user_select.remove_item(user)) self.popup_user_remove.title = _("Do you want to remove {}?").format( user_alias) setattr(self.popup_user_remove, 'user_id', user_id) self.popup_user_remove.open() def show_about(self): # ToDo: own popup class with scrollview. details = get_app_details() self.show_info( title=_("About"), text= _("{appname}\n" # Alternatively self.app.get_application_name() "Version: {version}\n" "\n" "Author: {author}\n" "Contact: [ref=mailto:{contact}][color=0000ff]{contact}[/color][/ref]\n" "Source Code: [ref={source}][color=0000ff]{source}[/color][/ref]\n" "\n" "This app uses third-party solutions that may not be governed by the same license.\n" "Third-Party Licenses: [ref={thirdparty}][color=0000ff]{thirdparty}[/color][/ref]" ).format( appname=details['appname'], author=details['author'], contact=details['contact'], version=details['version'], source=details['source'], thirdparty=details['third-party'], ), ) def on_info(self, title=None, text=None): self.show_info(title=title, text=text) def show_info(self, title=None, text=None): if not self.popup_info: self.popup_info = SimplePopup() # Hack for making about dialog to fit into screen. self.popup_info.bind( on_dismiss=lambda instance: setattr(self, 'popup_info', None)) if title: self.popup_info.title = title if text: self.popup_info.text = text self.popup_info.open() def on_warning(self, text): self.show_warning(text) def show_warning(self, text=None): if not self.popup_warning: self.popup_warning = SimplePopup(title=_("Warning")) if text: self.popup_warning.text = text self.popup_warning.open() def on_error(self, text): self.show_error(text) def show_error(self, text=None): if not self.popup_error: self.popup_error = SimplePopup(title=_("Error")) if text: self.popup_error.text = text self.popup_error.open() def show_popup_demographics(self, *args): popup = DemographicsPopup() # ToDo: This was a quick hack, don't want data_mgr reference here. Should keep separate. popup.bind(on_confirm=lambda instance, *largs: self.app.data_mgr. add_user_data(*largs)) popup.open() def show_popup_exit(self): popup = ConfirmPopup(title=_("Do you want to quit?")) popup.bind(on_confirm=self.quit) popup.open() def on_current(self, instance, value): """ When switching screens reset counter on back button presses on home screen. """ if not self.has_screen(value): if value in screen_map: self.add_widget(screen_map[value](name=value)) self.bind_screen_callbacks(value) else: return screen = self.get_screen(value) # Handle navbar access. try: if screen.navbar_enabled: self.sidebar.set_disabled(False) else: self.sidebar.set_disabled(True) except AttributeError: pass super(UiManager, self).on_current(instance, value) def on_upload_response(self, status, error_msg=None): if status is True: self.show_info(title=_("Success!"), text=_("Upload successful!")) # Inform whatever widget needs to act now. self.dispatch('on_upload_successful') else: if not error_msg: error_msg = _("Upload failed.\nSomething went wrong.") error_msg += "\n" + _("Please make sure the app is up-to-date.") self.show_error(text=error_msg) def on_upload_successful(self): pass def key_input(self, window, key, scancode, codepoint, modifier): """ Handle escape key / back button presses. """ if platform == "android": back_keys = [27] else: back_keys = [ 27, 278 ] # backspace = 8, but would need to check if we're typing in an input mask. if key in back_keys: # escape, home keys. # Handle back button on popup dialogs: if self.app.root_window.children[0] == self.popup_terms: if self.is_first_run: self.quit() else: self.popup_terms.dismiss() elif self.app.root_window.children[0] == self.popup_user_select: self.popup_user_select.dismiss() elif self.app.root_window.children[0] == self.popup_user_edit: self.popup_user_edit.dismiss() elif isinstance(self.app.root_window.children[0], ( SimplePopup, ConfirmPopup, TextInputPopup, NumericInputPopup, )): self.app.root_window.children[0].dismiss() elif isinstance(self.app.root_window.children[0], DemographicsPopup): self.app.root_window.children[0].dismiss() self.go_home() elif isinstance(self.app.root_window.children[0], DifficultyRatingPopup): return True # Could call confirm, but it's not clear if that would be the user's intent. elif isinstance(self.app.root_window.children[0], BlockingPopup): return True # Do nothing. # ToDo: prevent closing follow-up popup. elif self.sidebar.state == 'open': self.sidebar.set_state('close') # Handle back button on screens. elif self.current == 'Settings': self.app.close_settings() # If we are in a task, stop that task. elif self.current in ['Circle Task']: self.get_screen(self.current).stop_task(interrupt=True) self.go_home() elif self.current == 'Webview': self.get_screen('Webview').key_back_handler() self.go_home() elif self.current == 'Outro': self.go_home() elif self.current == 'Home': # Using back button to quit the app after 2 presses lead to a freeze when coming back from another # screen and not touching the screen after first back-press. Odd, right?! So make user touch screen. #plyer.notification.notify(message=_("Press again to quit."), toast=True) self.show_popup_exit() else: self.go_home() return True # override the default behaviour else: # the key now does nothing return False def go_home(self, transition='down'): self.transition.direction = transition self.current = 'Home' self.sidebar.set_state('close') self.on_orientation(None, self.orientation) def show_consent(self, task): # Make sure the correct user is selected. self.show_user_select() self.transition.direction = 'up' self.transition.duration = 0.5 self.settings.current_task = task self.current = self.task_consents[task] def on_invalid_session(self, *args): """ Default event implementation. """ pass def quit(self, *args): self.app.stop()
class wfpiconsole(App): # Define App class dictionary properties Sched = DictProperty([]) # Define App class configParser properties BarometerMax = ConfigParserProperty('-', 'System', 'BarometerMax', 'app') BarometerMin = ConfigParserProperty('-', 'System', 'BarometerMin', 'app') IndoorTemp = ConfigParserProperty('-', 'Display', 'IndoorTemp', 'app') # Define display properties scaleFactor = NumericProperty(1) scaleSuffix = StringProperty('_lR.png') # BUILD 'WeatherFlowPiConsole' APP CLASS # -------------------------------------------------------------------------- def build(self): # Calculate initial ScaleFactor and bind self.setScaleFactor to Window # on_resize self.window = Window self.setScaleFactor(self.window, self.window.width, self.window.height) self.window.bind(on_resize=self.setScaleFactor) # Load Custom Panel KV file if present if Path('user/customPanels.py').is_file(): Builder.load_file('user/customPanels.kv') # Initialise ScreenManager self.screenManager = screenManager(transition=NoTransition()) self.screenManager.add_widget(CurrentConditions()) # Start Websocket service self.startWebsocketService() # Check for latest version self.system = system() Clock.schedule_once(self.system.check_version) # Set Settings syle class self.settings_cls = SettingsWithSidebar # Initialise realtime clock self.Sched.realtimeClock = Clock.schedule_interval(self.system.realtimeClock, 1.0) # Return ScreenManager return self.screenManager # SET DISPLAY SCALE FACTOR BASED ON SCREEN DIMENSIONS # -------------------------------------------------------------------------- def setScaleFactor(self, instance, x, y): self.scaleFactor = min(x / 800, y / 480) if self.scaleFactor > 1: self.scaleSuffix = '_hR.png' else: self.scaleSuffix = '_lR.png' # LOAD APP CONFIGURATION FILE # -------------------------------------------------------------------------- def build_config(self, config): config.optionxform = str config.read('wfpiconsole.ini') # BUILD 'WeatherFlowPiConsole' APP CLASS SETTINGS # -------------------------------------------------------------------------- def build_settings(self, settings): # Register setting types settings.register_type('ScrollOptions', userSettings.ScrollOptions) settings.register_type('FixedOptions', userSettings.FixedOptions) settings.register_type('ToggleTemperature', userSettings.ToggleTemperature) settings.register_type('ToggleHours', userSettings.ToggleHours) # Add required panels to setting screen. Remove Kivy settings panel settings.add_json_panel('Display', self.config, data=userSettings.JSON('Display')) settings.add_json_panel('Primary Panels', self.config, data=userSettings.JSON('Primary')) settings.add_json_panel('Secondary Panels', self.config, data=userSettings.JSON('Secondary')) settings.add_json_panel('Units', self.config, data=userSettings.JSON('Units')) settings.add_json_panel('Feels Like', self.config, data=userSettings.JSON('FeelsLike')) settings.add_json_panel('System', self.config, data=userSettings.JSON('System')) self.use_kivy_settings = False self.settings = settings # OVERLOAD 'display_settings' TO OPEN SETTINGS SCREEN WITH SCREEN MANAGER # -------------------------------------------------------------------------- def display_settings(self, settings): self.mainMenu.dismiss(animation=False) if not self.screenManager.has_screen('Settings'): self.settingsScreen = Screen(name='Settings') self.screenManager.add_widget(self.settingsScreen) self.settingsScreen.add_widget(self.settings) self.screenManager.current = 'Settings' return True # OVERLOAD 'close_settings' TO CLOSE SETTINGS SCREEN WITH SCREEN MANAGER # -------------------------------------------------------------------------- def close_settings(self, *args): if self.screenManager.current == 'Settings': mainMenu().open(animation=False) self.screenManager.current = self.screenManager.previous() self.settingsScreen.remove_widget(self.settings) return True # OVERLOAD 'on_config_change' TO MAKE NECESSARY CHANGES TO CONFIG VALUES # WHEN REQUIRED # -------------------------------------------------------------------------- def on_config_change(self, config, section, key, value): # Update current weather forecast when temperature or wind speed units # are changed if section == 'Units' and key in ['Temp', 'Wind']: self.forecast.parse_forecast() self.sager.get_forecast_text() # Update current weather forecast, sunrise/sunset and moonrise/moonset # times when time format changed if section == 'Display' and key in 'TimeFormat': self.forecast.parse_forecast() self.astro.format_labels('Sun') self.astro.format_labels('Moon') # Update "Feels Like" temperature cutoffs in wfpiconsole.ini and the # settings screen when temperature units are changed if section == 'Units' and key == 'Temp': for Field in self.config['FeelsLike']: if 'c' in value: Temp = str(round((float(self.config['FeelsLike'][Field]) - 32) * (5 / 9))) self.config.set('FeelsLike', Field, Temp) elif 'f' in value: Temp = str(round(float(self.config['FeelsLike'][Field]) * (9 / 5) + 32)) self.config.set('FeelsLike', Field, Temp) self.config.write() panels = self._app_settings.children[0].content.panels for Field in self.config['FeelsLike']: for panel in panels.values(): if panel.title == 'Feels Like': for item in panel.children: if isinstance(item, Factory.ToggleTemperature): if item.title.replace(' ', '') == Field: item.value = self.config['FeelsLike'][Field] # Update barometer limits when pressure units are changed if section == 'Units' and key == 'Pressure': Units = ['mb', 'hpa', 'inhg', 'mmhg'] Max = ['1050', '1050', '31.0', '788'] Min = ['950', '950', '28.0', '713'] self.config.set('System', 'BarometerMax', Max[Units.index(value)]) self.config.set('System', 'BarometerMin', Min[Units.index(value)]) # Update primary and secondary panels displayed on CurrentConditions # screen if section in ['PrimaryPanels', 'SecondaryPanels']: panel_list = ['panel_' + Num for Num in ['one', 'two', 'three', 'four', 'five', 'six']] for ii, (panel, type) in enumerate(self.config['PrimaryPanels'].items()): if panel == key: old_panel = self.CurrentConditions.ids[panel_list[ii]].children[0] self.CurrentConditions.ids[panel_list[ii]].clear_widgets() self.CurrentConditions.ids[panel_list[ii]].add_widget(eval(type + 'Panel')()) break if hasattr(self, old_panel.__class__.__name__): try: getattr(self, old_panel.__class__.__name__).remove(old_panel) except ValueError: Logger.warning('Unable to remove panel reference from wfpiconsole class') # Update button layout displayed on CurrentConditions screen if section == 'SecondaryPanels': self.CurrentConditions.button_list = [] button_list = ['button_' + Num for Num in ['one', 'two', 'three', 'four', 'five', 'six']] button_number = 0 for button in button_list: self.CurrentConditions.ids[button].clear_widgets() for ii, (panel, type) in enumerate(self.config['SecondaryPanels'].items()): if type and type != 'None': self.CurrentConditions.ids[button_list[button_number]].add_widget(eval(type + 'Button')()) self.CurrentConditions.button_list.append([button_list[button_number], panel_list[ii], type, 'Primary']) button_number += 1 # Change 'None' for secondary panel selection to blank in config # file if value == 'None': self.config.set(section, key, '') self.config.write() panels = self._app_settings.children[0].content.panels for panel in panels.values(): if panel.title == 'Secondary Panels': for item in panel.children: if isinstance(item, Factory.SettingOptions): if item.title.replace(' ', '') == key: item.value = '' break # Update Sager Forecast schedule if section == 'System' and key == 'SagerInterval': Clock.schedule_once(self.sager.schedule_forecast) # Update derived variables to reflect configuration changes self.obsParser.reformat_display() # START WEBSOCKET SERVICE # -------------------------------------------------------------------------- def startWebsocketService(self, *largs): self.websocket_thread = threading.Thread(target=run_path, args=['service/websocket.py'], kwargs={'run_name': '__main__'}, daemon=True, name='Websocket') self.websocket_thread.start() # STOP WEBSOCKET SERVICE # -------------------------------------------------------------------------- def stopWebsocketService(self): self.websocket_client._keep_running = False self.websocket_thread.join() del self.websocket_client # EXIT CONSOLE AND SHUTDOWN SYSTEM # -------------------------------------------------------------------------- def shutdown_system(self): global SHUTDOWN SHUTDOWN = 1 self.stop() # EXIT CONSOLE AND REBOOT SYSTEM # -------------------------------------------------------------------------- def reboot_system(self): global REBOOT REBOOT = 1 self.stop()
class HandledPiCameraView(BoxLayout): MODE_STILL = 1 MODE_TIMER = 2 MODE_VIDEO = 3 DEF_REZ = "640x480" SHOOT_MODE_VIEWER = 1 SHOOT_MODE_FILE = 2 SHOOT_MODE_VIDEO = 3 SHOOT_MODE_VIDEO_DONE = 4 SHOOT_MODE_PROCESSING = 0 capture1 = io.BytesIO() capture2 = io.BytesIO() still_resolution = ConfigParserProperty("640x480", 'HQCam', 'picture_res', 'app') video_resolution = ConfigParserProperty("1080p30", 'HQCam', 'video_res', 'app') def __init__(self, **kwargs): super(HandledPiCameraView, self).__init__(**kwargs) self._shoot_mode = HandledPiCameraView.SHOOT_MODE_VIEWER self.mode = HandledPiCameraView.MODE_STILL self.camera = PiCamera(resolution=self.still_resolution) time.sleep(2) self.record_thread = threading.Thread(target=self.record) # TODO Include clean shutdown self.record_thread.daemon = True self.record_thread.start() self.is_recording = False def set_mode_camera_still(self, *args): Logger.info("Setting camera for STILL mode") self.mode = HandledPiCameraView.MODE_STILL def set_mode_camera_timer(self, *args): Logger.info("Setting camera for TIMER mode") self.mode = HandledPiCameraView.MODE_TIMER def set_mode_camera_video(self, *args): Logger.info("Setting camera for VIDEO mode") self.mode = HandledPiCameraView.MODE_VIDEO def process(self, data): if not self._shoot_mode == HandledPiCameraView.SHOOT_MODE_VIEWER: return True if self.cameraimage is not None: try: self.cameraimage.memory_data = data except: Logger.warn("Skipping Image") # Logger.error(sys.exc_info()[0]) return False def record(self, *largs): analyse = None Logger.info("Processing from PiCamera STARTED") runnig = True try: while runnig: # Process Viewer if self._shoot_mode == HandledPiCameraView.SHOOT_MODE_VIEWER: Logger.info("Rendering Viewer") for stream in self.camera.record_sequence(itertools.cycle( (io.BytesIO(), io.BytesIO())), format='mjpeg'): if analyse is not None: if self.process(analyse): break analyse.seek(0) analyse.truncate() self.camera.wait_recording(1 / 30.) #.25) if self.cameraeffect: effect = self.cameraeffect.get_effect() if "black & white" == effect: self.camera.image_effect = "none" self.camera.color_effects = (128, 128) elif "sepia" == effect: self.camera.image_effect = "none" self.camera.color_effects = (100, 150) else: self.camera.color_effects = None self.camera.image_effect = effect analyse = stream Logger.info("DONE Rendering Viewer") # Process Still if self.mode == HandledPiCameraView.MODE_STILL: try: self.cameraimage.memory_data = io.BytesIO( open("pihqcam/resources/camera.png", "rb").read()) except: Logger.info("Skipping Still Camera Image") self._shoot_mode = HandledPiCameraView.SHOOT_MODE_PROCESSING self.camerashutter.trackball.click_blue_trackball() Logger.info("Requesting Shooting Picture from {}".format( threading.current_thread().name)) # Clock.schedule_once(partial(self.shoot), -1) self._thread_shoot = threading.Thread(target=self.shoot) self._thread_shoot.start() Logger.info("Requesting Shooting Done OK {}".format( threading.current_thread().name)) self._thread_shoot.join() self.camerashutter.trackball.click_green_trackball() time.sleep(1) self.camerashutter.trackball.clear_trackball() Logger.info("Done waiting capture {}".format( threading.current_thread().name)) elif self.mode == HandledPiCameraView.MODE_TIMER: try: self.cameraimage.memory_data = io.BytesIO( open("pihqcam/resources/camera-retro.png", "rb").read()) except: Logger.info("Skipping Camera Image") self._shoot_mode = HandledPiCameraView.SHOOT_MODE_PROCESSING Logger.info( "Requesting Timed Shooting Picture from {}".format( threading.current_thread().name)) self.camerashutter.trackball.timer_still() self.camerashutter.trackball.click_blue_trackball() # Clock.schedule_once(partial(self.shoot), -1) self._thread_shoot = threading.Thread(target=self.shoot) self._thread_shoot.start() Logger.info("Requesting Timed Shooting Done OK {}".format( threading.current_thread().name)) self._thread_shoot.join() self.camerashutter.trackball.click_green_trackball() time.sleep(1) self.camerashutter.trackball.clear_trackball() Logger.info("Done waiting timed capture {}".format( threading.current_thread().name)) elif self.mode == HandledPiCameraView.MODE_VIDEO: # Process Video End if self._shoot_mode == HandledPiCameraView.SHOOT_MODE_VIDEO_DONE: Logger.info( "In Mode VIDEO_MODE {} with recording at ".format( self._shoot_mode, self.is_recording)) if self.is_recording: Logger.info( "Stopping Video Shooting from {}".format( threading.current_thread().name)) self.camera.stop_recording() Logger.info("Stopped Video Shooting") self.camerashutter.trackball.deactivate_video() self.camerashutter.trackball.click_green_trackball( ) time.sleep(1) self.camerashutter.trackball.clear_trackball() self.is_recording = False Logger.info("Resetting Video Mode {}".format( threading.current_thread().name)) self.camera.resolution = HandledPiCameraView.DEF_REZ self.camera.image_effect = "none" self._shoot_mode = HandledPiCameraView.SHOOT_MODE_VIEWER self.myroot.ids["filechooser"]._update_files() Logger.info("Shooting Done OK {}".format( threading.current_thread().name)) else: Logger.error( "Should not be in SHOOT_MODE_VIDEO_DONE while not recording" ) # Process Video elif self._shoot_mode == HandledPiCameraView.MODE_VIDEO: # --------------------------- if not self.is_recording: # self._shoot_mode = HandledPiCameraView.SHOOT_MODE_PROCESSING Logger.info( "Requesting Video Shooting from {}".format( threading.current_thread().name)) try: self.cameraimage.memory_data = io.BytesIO( open("pihqcam/resources/video-camera.png", "rb").read()) except: Logger.info("Skipping Video Camera Image") self.is_recording = True self.camerashutter.trackball.activate_video() self._thread_video = threading.Thread( target=self.record_video) self._thread_video.start() Logger.info("Waiting video shooting thread") self._thread_video.join() Logger.info("Done waiting video shooting thread") self._thread_video = None else: time.sleep(0.5) # --------------------------- # Process Wait while self._shoot_mode == HandledPiCameraView.SHOOT_MODE_PROCESSING: time.sleep(1) Logger.info("Waiting {}".format( threading.current_thread().name)) except: exc_type, exc_obj, exc_tb = sys.exc_info() fname = os.path.split(exc_tb.tb_frame.f_code.co_filename)[1] Logger.error("HandledPiCameraView: record: {} / {} / {}".format( exc_type, fname, exc_tb.tb_lineno)) finally: Logger.info("Processing from PiCamera STOPPED") def record_video(self, *largs): try: timestr = time.strftime("%Y%m%d_%H%M%S") target = App.get_running_app().config.get('HQCam', 'picture_folder') if not target: Logger.warn("Unable to get path from config.") target = os.getcwd() + sep + FOLDER_PHOTOS else: Logger.info("Got path from config: {}".format(target)) name = target + sep + "VID_{}.h264".format(timestr) config = self.video_resolution.split("p") if config[0].find('x') == -1: vid_res = config[0] + "p" else: vid_res = config[0] Logger.info("Resolution is {}".format(vid_res)) self.camera.resolution = vid_res Logger.info("Framerate is {}".format(config[1])) self.camera.framerate = int(config[1]) # self.camera.resolution=self.video_resolution if self.cameraeffect: effect = self.cameraeffect.get_effect() Logger.info("Effect is {}".format(effect)) if "black & white" == effect: self.camera.image_effect = "none" self.camera.color_effects = (128, 128) elif "sepia" == effect: self.camera.image_effect = "none" self.camera.color_effects = (100, 150) else: self.camera.color_effects = None self.camera.image_effect = effect Logger.info("Recording to {} from {}".format( name, threading.current_thread().name)) self.camera.start_recording(name) except: Logger.error("HandledPiCameraView: record: {}".format( sys.exc_info()[0])) finally: pass def shoot(self, *largs): try: timestr = time.strftime("%Y%m%d_%H%M%S") target = App.get_running_app().config.get('HQCam', 'picture_folder') if not target: Logger.warn("Unable to get path from config.") target = os.getcwd() + sep + FOLDER_PHOTOS else: Logger.info("Got path from config: {}".format(target)) name = target + sep + "IMG_{}.jpg".format(timestr) Logger.info("MAX Resolution is {}".format(PiCamera.MAX_RESOLUTION)) Logger.info("Resolution is {}".format(self.still_resolution)) self.camera.resolution = self.still_resolution if self.cameraeffect: effect = self.cameraeffect.get_effect() if "black & white" == effect: self.camera.image_effect = "none" self.camera.color_effects = (128, 128) elif "sepia" == effect: self.camera.image_effect = "none" self.camera.color_effects = (100, 150) else: self.camera.color_effects = None self.camera.image_effect = effect Logger.info("Capturing to {} from {}".format( name, threading.current_thread().name)) self.camera.capture(name) except: Logger.error("HandledPiCameraView: shoot: {}".format( sys.exc_info()[0])) finally: Logger.info("Finalizing Shooting Still {}".format( threading.current_thread().name)) self.camera.resolution = HandledPiCameraView.DEF_REZ self.camera.image_effect = "none" self._shoot_mode = HandledPiCameraView.SHOOT_MODE_VIEWER self.myroot.ids["filechooser"]._update_files() Logger.info("Shooting Done OK {}".format( threading.current_thread().name)) def capture(self, *largs): Logger.info("New Shoot Mode > FILE") # Change Action Mode self._shoot_mode = HandledPiCameraView.SHOOT_MODE_FILE def capture_video(self, *largs): # Change Action Mode if self.is_recording: Logger.info("New Shoot Mode > VIDEO_DONE") self._shoot_mode = HandledPiCameraView.SHOOT_MODE_VIDEO_DONE else: Logger.info("New Shoot Mode > VIDEO") self._shoot_mode = HandledPiCameraView.SHOOT_MODE_VIDEO
class CamScreen(Screen): tex = ObjectProperty(None) recording = BooleanProperty(False) playing = BooleanProperty(False) measure_type = ConfigParserProperty(None, 'view', 'focus', 'app', val_type=MeasureType) measure_frequency = ConfigParserProperty(5, 'view', 'focus_rate', 'app', val_type=int) measure = False zoom_factor = NumericProperty(1) # todo more options (set in cfg?) iso = ConfigParserProperty(None, 'camera', 'iso', 'app', val_type=int, verify=lambda x: 100 <= x <= 800, errorvalue=800) framerate = ConfigParserProperty(None, 'camera', 'framerate', 'app', val_type=int, verify=lambda x: 1 <= x <= 90, errvalue=10) exposure_mode = ConfigParserProperty(None, 'camera', 'exposure_mode', 'app') sensor_mode = ConfigParserProperty(None, 'camera', 'sensor_mode', 'app') _measure_tick = 0 _fake_source = FakeSource() def __init__(self, **kw): super().__init__(**kw) self.texout = TextureOutput() self.tex = self.texout.texture def on_new_measure(self, fm): print(self.measure_type, fm) pass # noinspection PyAttributeOutsideInit def init(self, camera): self.register_event_type('on_new_measure') self.cam = camera self.texout.bind(on_update=self._texture_update) # todo set mode/framerate? def on_iso(self, _, value): print('iso', value) if not cfg.FAKE: self.cam.iso = value def on_framerate(self, _, value): print('framerate', value) if not cfg.FAKE: set_camera_mode(self.cam, self.exposure_mode, framerate=value) def on_sensor_mode(self, _, value): print('mode', value) if not cfg.FAKE: set_camera_mode(self.cam, self.exposure_mode, mode=value) def on_exposure_mode(self, _, value): print('exposure_mode', value) if not cfg.FAKE: self.cam.exposure_mode = value def play(self): print('play') self.playing = not self.playing if self.playing: if cfg.FAKE: print('fake play') @threaded(daemon=True) def run_fake_source(): print('start fake playing') for buf in self._fake_source: if not self.playing: break self.texout.write(buf) time.sleep(1 / float(cfg.FRAME_RATE)) print('done fake playing') run_fake_source() else: print_sensor(cam) self.cam.start_recording(self.texout, 'rgb', resize=(320, 240)) else: if cfg.FAKE: print('fake stop') else: self.cam.stop_recording() def capture(self): print('capture') if not cfg.FAKE: self.cam.capture('image-{}.jpg'.format(time.clock()), resize=(3280, 2464)) def record(self): print('record') self.recording = not self.recording if not cfg.FAKE: if self.recording: self.cam.start_recording('video-{}.h264'.format(time.clock()), splitter_port=2, resize=(1640, 1232)) else: self.cam.stop_recording(splitter_port=2) def zoom(self): # todo make zoom switch source directories in fake mode? if cfg.FAKE: self._fake_source.zoom() else: zf = self.zoom_factor + 1 self.zoom_factor = zf if zf <= 3 else 1 scale = 1.0 if self.zoom_factor == 1 else 1.0 / self.zoom_factor offset = 0.0 if self.zoom_factor == 1 else (1.0 - scale) / 2.0 print('new zoom:', self.zoom_factor, scale, offset) self.cam.zoom = (offset, offset, scale, scale) def _texture_update(self, _, buf): # self.canvas.ask_update() if self.measure: self._measure_tick -= 1 if self._measure_tick <= 0: self._measure_tick = self.measure_frequency if self.measure_type is MeasureType.HFR: fm = measure_hfr(buf) elif self.measure_type is MeasureType.LP: fm = measure_lp(buf) else: fm = 0 # fm = measure_fwhm(buf) self.dispatch('on_new_measure', fm)
class HandledPiCamPanels(TabbedPanel): LIVE_TAB = 2 BROWSER_TAB = 1 INFO_TAB = 0 duration = ConfigParserProperty('5', 'HQCam', 'timers_duration', 'app') def __init__(self, **kwargs): super(HandledPiCamPanels, self).__init__(**kwargs) # Define Trackball call self.trackball = TrackballHelper() self.trackball.setup() self.trackball.clear_trackball() self.trackball.start(self.on_read_trackball) self.myroot = None # Injected from KV TODO Change def on_read_trackball(self, trackball_value): tb_group = self.tab_list tb = next((t for t in tb_group if t.state == 'down'), None) if tb: position = tb_group.index(tb) new_pos = position max = len(tb_group) - 1 if TrackballHelper.NOTIFY_LEFT == trackball_value: new_pos = position + 1 if new_pos > max: new_pos = max elif TrackballHelper.NOTIFY_RIGHT == trackball_value: new_pos = position - 1 if new_pos < 0: new_pos = 0 if position != new_pos: tb_group[new_pos].state = "down" tb_group[position].state = "normal" tb_group[new_pos].dispatch('on_release') if new_pos == 1: # Browser filechooser = self.myroot.ids["filechooser"] if len(filechooser.selection) < 1: Logger.info("Updating selection") if len(filechooser._items) > 0: filechooser._items[0].is_selected = True filechooser.selection = [ filechooser._items[0].path, ] filepreviewer = self.myroot.ids["filepreviewer"] filepreviewer.source = ImageHelper( ).process_for_thumbnail(filechooser._items[0].path) Logger.info("Updating viewer source to {}".format( filepreviewer.source)) Logger.info("Done with Browser") elif TrackballHelper.NOTIFY_CLICK == trackball_value: if position == HandledPiCamPanels.LIVE_TAB: try: cameraview = self.myroot.ids["cameraview"] if HandledPiCameraView.MODE_STILL == cameraview.mode: Logger.info("Taking Picture") self.myroot.ids["cameraview"].capture() elif HandledPiCameraView.MODE_TIMER == cameraview.mode: Logger.info("Taking Picture with Timer") self.myroot.ids["cameraview"].capture() elif HandledPiCameraView.MODE_VIDEO == cameraview.mode: Logger.info("Taking Video") self.myroot.ids["cameraview"].capture_video() else: Logger.error("Invalid Mode {}".format( cameraview.mode)) except AttributeError as ae: Logger.error("===== AttributeError") Logger.error(ae) except: Logger.error( "HandledPiCamPanels: on_read_trackball: {}".format( sys.exc_info()[0])) else: if position == HandledPiCamPanels.BROWSER_TAB: # Browser filechooser = self.myroot.ids["filechooser"] if len(filechooser.selection) < 1: print(filechooser._items[0].path) if len(filechooser._items) > 0: filechooser._items[0].is_selected = True filechooser.selection = [ filechooser._items[0].path, ] return cur = filechooser.selection[0] sfiles = [entry.path for entry in filechooser._items] fcount = len(sfiles) if fcount < 1: return if cur in sfiles: idx = sfiles.index(cur) orig = idx if TrackballHelper.NOTIFY_UP == trackball_value: offset = -1 elif TrackballHelper.NOTIFY_DOWN == trackball_value: offset = 1 idx += offset if idx >= fcount: idx -= fcount new = sfiles[idx] filechooser._items[orig].is_selected = False filechooser._items[idx].is_selected = True filechooser.selection = [ new, ] # Access the kivy.uix.filechooser.FileChooserListLayout for fileChooserListLayout in filechooser.children: # Access the kivy.uix.boxlayout.BoxLayout for boxLayout in fileChooserListLayout.children: # Scroll to reach the ScrollView for child in boxLayout.children: if isinstance( child, ScrollView ): #kivy.uix.scrollview.ScrollView): child.scroll_to( filechooser._items[idx])
class SettingsContainer(Widget): """ Config settings that can be changes by user and properties for current state. """ # General properties. current_user = ConfigParserProperty('Default', 'General', 'current_user', 'app', val_type=str) is_sound_enabled = ConfigParserProperty('1', 'General', 'sound_enabled', 'app', val_type=int) is_vibrate_enabled = ConfigParserProperty('1', 'General', 'vibration_enabled', 'app', val_type=int) # Data Collection. is_local_storage_enabled = ConfigParserProperty( '0', 'DataCollection', 'is_local_storage_enabled', 'app', val_type=int) # Converts string to int. is_upload_enabled = ConfigParserProperty('1', 'DataCollection', 'is_upload_enabled', 'app', val_type=int) server_uri = ConfigParserProperty(get_app_details()['webserver'], 'DataCollection', 'webserver', 'app', val_type=str) is_email_enabled = ConfigParserProperty('0', 'DataCollection', 'is_email_enabled', 'app', val_type=int) # Properties that change over the course of all tasks and are not set by config. current_trial = NumericProperty(0) current_block = NumericProperty(0) # These only contain the values and are not bound to the config. # Can't use apply_property() to dynamically add ConfigParserProperty, because we can't set the key dynamically. # Can't save in one list with ids as mapping, because callbacks are only triggered at toplevel changes. user_ids = ListProperty([]) user_aliases = ListProperty([]) def __init__(self, **kwargs): super(SettingsContainer, self).__init__(**kwargs) # Study settings have to follow the rule of being named the lowercase study name with underscores. self.circle_task = SettingsCircleTask() self.current_task = None self.reset_current() self.register_event_type('on_user_removed') # Schedule user settings for nct frame. Section is not yet ready. Clock.schedule_once(lambda dt: self.populate_users(), 1) def populate_users(self): app = App.get_running_app() config = app.config for user_id in config['UserData']: self.user_ids.append(user_id) self.user_aliases.append(config.get('UserData', user_id)) def edit_user(self, instance, user_id=None, user_alias=''): """ Edit user information. This can be a new user. """ app = App.get_running_app() config = app.config try: idx = self.user_ids.index(user_id) self.user_aliases[idx] = user_alias except ValueError: self.user_aliases.append(user_alias) self.user_ids.append(user_id) # Add to config. config.set('UserData', user_id, user_alias) config.write() def remove_user(self, user_id): """ Remove a user from settings and config by its ID. """ app = App.get_running_app() config = app.config config.remove_option(section='UserData', option=user_id) config.write() idx = self.user_ids.index(user_id) self.user_aliases.pop(idx) self.user_ids.remove(user_id) self.dispatch('on_user_removed', user_id) def on_user_removed(self, *args): """ Default dummy implementation of event callback. """ pass def reset_current(self): self.current_block = 0 self.current_trial = 0 def next_block(self): self.current_trial = 0 self.current_block += 1 def on_current_trial(self, instance, value): """ Bound to change in current trial. """ pass def on_current_block(self, instance, value): """ Bound to change in current block property. """ if self.current_task == 'Circle Task': self.circle_task.on_new_block(value)
class wfpiconsole(App): # Define App class observation dictionary properties Obs = DictProperty ([('rapidSpd','--'), ('rapidDir','----'), ('rapidShift','-'), ('WindSpd','-----'), ('WindGust','--'), ('WindDir','---'), ('AvgWind','--'), ('MaxGust','--'), ('RainRate','---'), ('TodayRain','--'), ('YesterdayRain','--'), ('MonthRain','--'), ('YearRain','--'), ('Radiation','----'), ('UVIndex','----'), ('peakSun','-----'), ('outTemp','--'), ('outTempMin','---'), ('outTempMax','---'), ('inTemp','--'), ('inTempMin','---'), ('inTempMax','---'), ('Humidity','--'), ('DewPoint','--'), ('Pres','---'), ('MaxPres','---'), ('MinPres','---'), ('PresTrend','----'), ('FeelsLike','----'), ('StrikeDeltaT','-----'), ('StrikeDist','--'), ('StrikeFreq','----'), ('Strikes3hr','-'), ('StrikesToday','-'), ('StrikesMonth','-'), ('StrikesYear','-') ]) Astro = DictProperty ([('Sunrise',['-','-',0]), ('Sunset',['-','-',0]), ('Dawn',['-','-',0]), ('Dusk',['-','-',0]), ('sunEvent','----'), ('sunIcon',['-',0,0]), ('Moonrise',['-','-']), ('Moonset',['-','-']), ('NewMoon','--'), ('FullMoon','--'), ('Phase','---'), ('Reformat','-'), ]) MetData = DictProperty ([('Weather','Building'), ('Temp','--'), ('Precip','--'), ('WindSpd','--'), ('WindDir','--'), ('Valid','--') ]) Sager = DictProperty ([('Forecast','--'), ('Issued','--')]) System = DictProperty ([('Time','-'), ('Date','-')]) Version = DictProperty ([('Latest','-')]) # Define App class configParser properties BarometerMax = ConfigParserProperty('-','System', 'BarometerMax','wfpiconsole') BarometerMin = ConfigParserProperty('-','System', 'BarometerMin','wfpiconsole') IndoorTemp = ConfigParserProperty('-','Display','IndoorTemp', 'wfpiconsole') # BUILD 'WeatherFlowPiConsole' APP CLASS # -------------------------------------------------------------------------- def build(self): # Load user configuration from wfpiconsole.ini and define Settings panel # type self.config = ConfigParser(allow_no_value=True,name='wfpiconsole') self.config.optionxform = str self.config.read('wfpiconsole.ini') self.settings_cls = SettingsWithSidebar # Force window size if required based on hardware type if self.config['System']['Hardware'] == 'Pi4': Window.size = (800,480) Window.borderless = 1 Window.top = 0 elif self.config['System']['Hardware'] == 'Other': Window.size = (800,480) # Initialise real time clock Clock.schedule_interval(partial(system.realtimeClock,self.System,self.config),1.0) # Initialise Sunrise and Sunset time, Moonrise and Moonset time, and # MetOffice or DarkSky weather forecast astro.SunriseSunset(self.Astro,self.config) astro.MoonriseMoonset(self.Astro,self.config) forecast.Download(self.MetData,self.config) # Generate Sager Weathercaster forecast Thread(target=sagerForecast.Generate, args=(self.Sager,self.config), name="Sager", daemon=True).start() # Initialise websocket connection self.WebsocketConnect() # Check for latest version Clock.schedule_once(partial(system.checkVersion,self.Version,self.config,updateNotif)) # Initialise Station class, and set device status to be checked every # second self.Station = Station() Clock.schedule_interval(self.Station.getDeviceStatus,1.0) # Schedule function calls Clock.schedule_interval(self.UpdateMethods,1.0) Clock.schedule_interval(partial(astro.sunTransit,self.Astro,self.config),1.0) Clock.schedule_interval(partial(astro.moonPhase ,self.Astro,self.config),1.0) # BUILD 'WeatherFlowPiConsole' APP CLASS SETTINGS # -------------------------------------------------------------------------- def build_settings(self,settingsScreen): # Register setting types settingsScreen.register_type('ScrollOptions', SettingScrollOptions) settingsScreen.register_type('FixedOptions', SettingFixedOptions) settingsScreen.register_type('ToggleTemperature', SettingToggleTemperature) # Add required panels to setting screen. Remove Kivy settings panel settingsScreen.add_json_panel('Display', self.config, data = settings.JSON('Display')) settingsScreen.add_json_panel('Primary Panels', self.config, data = settings.JSON('Primary')) settingsScreen.add_json_panel('Secondary Panels', self.config, data = settings.JSON('Secondary')) settingsScreen.add_json_panel('Units', self.config, data = settings.JSON('Units')) settingsScreen.add_json_panel('Feels Like', self.config, data = settings.JSON('FeelsLike')) self.use_kivy_settings = False # OVERLOAD 'on_config_change' TO MAKE NECESSARY CHANGES TO CONFIG VALUES # WHEN REQUIRED # -------------------------------------------------------------------------- def on_config_change(self,config,section,key,value): # Update current weather forecast and Sager Weathercaster forecast when # temperature or wind speed units are changed if section == 'Units' and key in ['Temp','Wind']: if self.config['Station']['Country'] == 'GB': forecast.ExtractMetOffice(self.MetData,self.config) else: forecast.ExtractDarkSky(self.MetData,self.config) if key == 'Wind' and 'Dial' in self.Sager: self.Sager['Dial']['Units'] = value self.Sager['Forecast'] = sagerForecast.getForecast(self.Sager['Dial']) # Update "Feels Like" temperature cutoffs in wfpiconsole.ini and the # settings screen when temperature units are changed if section == 'Units' and key == 'Temp': for Field in self.config['FeelsLike']: if 'c' in value: Temp = str(round((float(self.config['FeelsLike'][Field])-32) * 5/9)) self.config.set('FeelsLike',Field,Temp) elif 'f' in value: Temp = str(round(float(self.config['FeelsLike'][Field])*9/5 + 32)) self.config.set('FeelsLike',Field,Temp) self.config.write() panels = self._app_settings.children[0].content.panels for Field in self.config['FeelsLike']: for panel in panels.values(): if panel.title == 'Feels Like': for item in panel.children: if isinstance(item,Factory.SettingToggleTemperature): if item.title.replace(' ','') == Field: item.value = self.config['FeelsLike'][Field] # Update barometer limits when pressure units are changed if section == 'Units' and key == 'Pressure': Units = ['mb','hpa','inhg','mmhg'] Max = ['1050','1050','31.0','788'] Min = ['950','950','28.0','713'] self.config.set('System','BarometerMax',Max[Units.index(value)]) self.config.set('System','BarometerMin',Min[Units.index(value)]) # Update primary and secondary panels displayed on CurrentConditions # screen if section in ['PrimaryPanels','SecondaryPanels']: for Panel,Type in App.get_running_app().config['PrimaryPanels'].items(): if Panel == key: self.CurrentConditions.ids[Panel].clear_widgets() self.CurrentConditions.ids[Panel].add_widget(eval(Type + 'Panel')()) break # Update button layout displayed on CurrentConditions screen if section == 'SecondaryPanels': ii = 0 self.CurrentConditions.buttonList = [] Button = ['Button' + Num for Num in ['One','Two','Three','Four','Five','Six']] for Panel, Type in App.get_running_app().config['SecondaryPanels'].items(): self.CurrentConditions.ids[Button[ii]].clear_widgets() if Type and Type != 'None': self.CurrentConditions.ids[Button[ii]].add_widget(eval(Type + 'Button')()) self.CurrentConditions.buttonList.append([Button[ii],Panel,Type,'Primary']) ii += 1 # Change 'None' for secondary panel selection to blank in config # file if value == 'None': self.config.set(section,key,'') self.config.write() panels = self._app_settings.children[0].content.panels for panel in panels.values(): if panel.title == 'Secondary Panels': for item in panel.children: if isinstance(item,Factory.SettingOptions): if item.title.replace(' ','') == key: item.value = '' break # CONNECT TO THE SECURE WEATHERFLOW WEBSOCKET SERVER # -------------------------------------------------------------------------- def WebsocketConnect(self): Server = 'wss://ws.weatherflow.com/swd/data?api_key=' + self.config['Keys']['WeatherFlow'] self._factory = WeatherFlowClientFactory(Server,self) reactor.connectTCP('ws.weatherflow.com',80,self._factory,) # SEND MESSAGE TO THE WEATHERFLOW WEBSOCKET SERVER # -------------------------------------------------------------------------- def WebsocketSendMessage(self,Message): Message = Message.encode('utf8') proto = self._factory._proto if Message and proto: proto.sendMessage(Message) # DECODE THE WEATHERFLOW WEBSOCKET MESSAGE # -------------------------------------------------------------------------- def WebsocketDecodeMessage(self,Msg): # Extract type of received message Type = Msg['type'] # Start listening for device observations and events upon connection of # websocket based on device IDs specified in user configuration file if Type == 'connection_opened': if self.config['Station']['TempestID']: self.WebsocketSendMessage('{"type":"listen_start",' + ' "device_id":' + self.config['Station']['TempestID'] + ',' + ' "id":"Sky"}') self.WebsocketSendMessage('{"type":"listen_rapid_start",' + ' "device_id":' + self.config['Station']['TempestID'] + ',' + ' "id":"rapidWind"}') elif self.config['Station']['SkyID']: self.WebsocketSendMessage('{"type":"listen_start",' + ' "device_id":' + self.config['Station']['SkyID'] + ',' + ' "id":"Sky"}') self.WebsocketSendMessage('{"type":"listen_rapid_start",' + ' "device_id":' + self.config['Station']['SkyID'] + ',' + ' "id":"rapidWind"}') if self.config['Station']['OutAirID']: self.WebsocketSendMessage('{"type":"listen_start",' + ' "device_id":' + self.config['Station']['OutAirID'] + ',' + ' "id":"OutdoorAir"}') if self.config['Station']['InAirID']: self.WebsocketSendMessage('{"type":"listen_start",' + ' "device_id":' + self.config['Station']['InAirID'] + ',' + ' "id":"IndoorAir"}') # Extract observations from obs_st websocket message elif Type == 'obs_st': Thread(target=websocket.Tempest, args=(Msg,self), name="Tempest", daemon=True).start() # Extract observations from obs_sky websocket message elif Type == 'obs_sky': Thread(target=websocket.Sky, args=(Msg,self), name="Sky", daemon=True).start() # Extract observations from obs_air websocket message based on device # ID elif Type == 'obs_air': if self.config['Station']['InAirID'] and Msg['device_id'] == int(self.config['Station']['InAirID']): Thread(target=websocket.indoorAir, args=(Msg,self), name="indoorAir", daemon=True).start() if self.config['Station']['OutAirID'] and Msg['device_id'] == int(self.config['Station']['OutAirID']): Thread(target=websocket.outdoorAir, args=(Msg,self), name="outdoorAir", daemon=True).start() # Extract observations from rapid_wind websocket message elif Type == 'rapid_wind': websocket.rapidWind(Msg,self) # Extract observations from evt_strike websocket message elif Type == 'evt_strike': websocket.evtStrike(Msg,self) # UPDATE 'WeatherFlowPiConsole' APP CLASS METHODS AT REQUIRED INTERVALS # -------------------------------------------------------------------------- def UpdateMethods(self,dt): # Get current time in station timezone Tz = pytz.timezone(self.config['Station']['Timezone']) Now = datetime.now(pytz.utc).astimezone(Tz) Now = Now.replace(microsecond=0) # At 5 minutes past each hour, download a new forecast for the Station # location if (Now.minute,Now.second) == (5,0): forecast.Download(self.MetData,self.config) # At the top of each hour update the on-screen forecast for the Station # location if self.config['Station']['Country'] == 'GB': if Now.hour > self.MetData['Time'].hour or Now.date() > self.MetData['Time'].date(): forecast.ExtractMetOffice(self.MetData,self.config) self.MetData['Time'] = Now elif self.config['Keys']['DarkSky']: if Now.hour > self.MetData['Time'].hour or Now.date() > self.MetData['Time'].date(): forecast.ExtractDarkSky(self.MetData,self.config) self.MetData['Time'] = Now # Once dusk has passed, calculate new sunrise/sunset times if Now >= self.Astro['Dusk'][0]: self.Astro = astro.SunriseSunset(self.Astro,self.config) # Once moonset has passed, calculate new moonrise/moonset times if Now > self.Astro['Moonset'][0]: self.Astro = astro.MoonriseMoonset(self.Astro,self.config) # At midnight, update Sunset, Sunrise, Moonrise and Moonset Kivy Labels if self.Astro['Reformat'] and Now.replace(second=0).time() == time(0,0,0): self.Astro = astro.Format(self.Astro,self.config,"Sun") self.Astro = astro.Format(self.Astro,self.config,"Moon")
class MyLabel(Button): foo = ConfigParserProperty(None, 'section', 'foo', 'app')
class DialWidget(FloatLayout): """ Speed will become a fixed value of 86400 once completed. Image should, i suppose, be a fixed image? At some point we'll need to add a tuple(?) for sunrise / sunset times. """ angle = NumericProperty(0) config_latlon = latlon = ConfigParserProperty( '', 'global', datahelpers.LOCATION_LATLON, 'app', val_type=str) latlon_point = ObjectProperty() sunrise = NumericProperty() sunset = NumericProperty() sun_angles = ReferenceListProperty(sunrise, sunset) def on_config_latlon(self, instance, value): """Handler for property change event""" self.latlon_point = Point(value) self.redraw() def __init__(self, **kwargs): super(DialWidget, self).__init__(**kwargs) # Widget properties self.day_length = 86400 self.date_increase = 1 self.dial_size = (0.8, 0.8) self.date = datetime.now() self.midnight = (self.date + timedelta(days=1)) self.midnight_delta = (datetime(year=self.midnight.year, month=self.midnight.month, day=self.midnight.day, hour=0, minute=0, second=0) - self.date).seconds # Set sunrise and sunset through reference list self.sun_angles = self.sun_time() # Shading widget self.dial_shading = DialEffectWidget((self.sunrise, self.sunset)) self.add_widget(self.dial_shading) if self.sun_angles not in [[0, 0], [0, 360]]: self.sun_rise_marker = SunRiseMarker(self.sunrise) self.sun_set_marker = SunSetMarker(self.sunset) self.add_widget(self.sun_rise_marker) self.add_widget(self.sun_set_marker) self.animate_dial() self.clock = Clock.schedule_interval(self.redraw, self.midnight_delta) def animate_dial(self): anim = Animation(angle=359, duration=self.day_length) anim += Animation(angle=359, duration=self.day_length) anim.repeat = True anim.start(self) def redraw(self, a=None): # Split suntime tuple into named variables self.sun_angles = self.sun_time() # Remove widgets self.remove_widget(self.dial_shading) try: self.remove_widget(self.sun_rise_marker) self.remove_widget(self.sun_set_marker) except AttributeError: # Previous day had no sunrise/sunset, no widgets to remove pass # Shading widget self.dial_shading = DialEffectWidget((self.sunrise, self.sunset)) self.add_widget(self.dial_shading) if self.sun_angles not in [[0, 0], [0, 360]]: self.sun_rise_marker = SunRiseMarker(self.sunrise) self.sun_set_marker = SunSetMarker(self.sunset) self.add_widget(self.sun_rise_marker) self.add_widget(self.sun_set_marker) # Restart the clock! self.clock.cancel() self.clock = Clock.schedule_interval(self.redraw, self.midnight_delta) def sun_time(self): sun_time = Sun(self.latlon_point.latitude, self.latlon_point.longitude) self.date = self.date + timedelta(days=self.date_increase) try: today_sunrise = sun_time.get_sunrise_time(self.date) except SunTimeException: if date(year=self.date.year, month=3, day=21)\ < self.date.date()\ < date(year=self.date.year, month=9, day=22): return 0, 360 return 0, 0 try: today_sunset = sun_time.get_sunset_time(self.date) except SunTimeException: if date(year=self.date.year, month=3, day=21)\ < self.date.date()\ < date(year=self.date.year, month=9, day=22): return 0, 360 return 0, 0 # This is *super* ugly, I'm sure we can find a more elegant way to do this now = datetime.utcnow() - timedelta(hours=0) today_sunrise = today_sunrise.replace(tzinfo=None) today_sunset = today_sunset.replace(tzinfo=None) # After Sunrise, after Sunset if now > today_sunrise and today_sunset: # Get timedelta for each today_sunrise = now - today_sunrise today_sunset = now - today_sunset # Convert timedelta into minutes and round today_sunrise = round(today_sunrise.seconds / 60) today_sunset = round(today_sunset.seconds / 60) # Convert minutes into angles today_sunrise = today_sunrise * 0.25 today_sunset = today_sunset * 0.25 # Before Sunrise, after Sunset elif now < today_sunrise and today_sunset: today_sunrise = today_sunrise - now today_sunset = today_sunset - now today_sunrise = round(today_sunrise.seconds / 60) today_sunset = round(today_sunset.seconds / 60) today_sunrise = 360 - (today_sunrise * 0.25) today_sunset = 360 - (today_sunset * 0.25) # After Sunrise, before Sunset else: today_sunrise = now - today_sunrise today_sunset = today_sunset - now today_sunrise = round(today_sunrise.seconds / 60) today_sunset = round(today_sunset.seconds / 60) today_sunrise = today_sunrise * 0.25 today_sunset = 360 - (today_sunset * 0.25) return today_sunrise, today_sunset def on_angle(self, item, angle): if angle == 359: item.angle = 0 self.redraw()
class Profiler(EventDispatcher): profile_path = StringProperty('') ''' Profile settings path :class:`~kivy.properties.StringProperty` and defaults to ''. ''' project_path = StringProperty('') ''' Project path :class:`~kivy.properties.StringProperty` and defaults to ''. ''' designer = ObjectProperty(None) '''Reference of :class:`~designer.app.Designer`. :data:`designer` is a :class:`~kivy.properties.ObjectProperty` ''' profile_config = ObjectProperty(None) '''Reference to a ConfigParser with the profile settings :class:`~kivy.properties.ObjectProperty` and defaults to None. ''' pro_name = ConfigParserProperty('', 'profile', 'name', 'profiler') '''Reference to a ConfigParser with the profile settings Get the profile name :class:`~kivy.properties.ConfigParserProperty` ''' pro_builder = ConfigParserProperty('', 'profile', 'builder', 'profiler') '''Reference to a ConfigParser with the profile settings Get the profile builder :class:`~kivy.properties.ConfigParserProperty` ''' pro_target = ConfigParserProperty('', 'profile', 'target', 'profiler') '''Reference to a ConfigParser with the profile settings Get the profile target :class:`~kivy.properties.ConfigParserProperty` ''' pro_mode = ConfigParserProperty('', 'profile', 'mode', 'profiler') '''Reference to a ConfigParser with the profile settings Get the profile builder :class:`~kivy.properties.ConfigParserProperty` ''' pro_install = ConfigParserProperty('', 'profile', 'install', 'profiler') '''Reference to a ConfigParser with the profile settings Get the profile install_on_device :class:`~kivy.properties.ConfigParserProperty` ''' pro_debug = ConfigParserProperty('', 'profile', 'debug', 'profiler') '''Reference to a ConfigParser with the profile settings Get the profile debug mode :class:`~kivy.properties.ConfigParserProperty` ''' pro_verbose = ConfigParserProperty('', 'profile', 'verbose', 'profiler') '''Reference to a ConfigParser with the profile settings Get the profile verbose mode :class:`~kivy.properties.ConfigParserProperty` ''' builder = ObjectProperty(None) '''Reference to the builder class. Can be Hanga, Buildozer or Desktop :class:`~kivy.properties.ObjectProperty` ''' __events__ = ('on_run', 'on_stop', 'on_error', 'on_message', 'on_build', 'on_deploy', 'on_clean') def __init__(self, **kwargs): super(Profiler, self).__init__(**kwargs) self.profile_config = ConfigParser(name='profiler') def run(self, *args, **kwargs): '''Run project ''' self.builder.run(*args, **kwargs) def stop(self): '''Stop project ''' self.builder.stop() def clean(self): '''Clean project ''' self.builder.clean() def build(self): '''Build project ''' self.builder.build() def rebuild(self): '''Rebuild project ''' self.builder.rebuild() def load_profile(self, prof_path, proj_path): '''Read the settings ''' self.profile_path = prof_path self.project_path = proj_path self.profile_config.read(self.profile_path) if self.pro_target == 'Desktop': self.builder = Desktop(self) else: if self.pro_builder == 'Buildozer': self.builder = Buildozer(self) elif self.pro_builder == 'Hanga': # TODO implement hanga self.builder = Desktop(self) self.dispatch( 'on_error', 'Hanga Builder not yet implemented!\n' 'Using Desktop') else: self.builder = Desktop(self) def on_error(self, *args): '''on_error event handler ''' pass def on_message(self, *args): '''on_message event handler ''' pass def on_run(self, *args): '''on_run event handler ''' pass def on_stop(self, *args): '''on_stop event handler ''' pass def on_build(self, *args): '''on_build event handler ''' pass def on_deploy(self, *args): '''on_deploy event handler ''' pass def on_clean(self, *args): '''on_clean event handler ''' pass
class TaxiApp(App): pen_pos_up = ConfigParserProperty(60, "axidraw", "pen_pos_up", "app") pen_pos_down = ConfigParserProperty(40, "axidraw", "pen_pos_down", "app") speed = ConfigParserProperty(25, "axidraw", "speed_pendown", "app") accel = ConfigParserProperty(75, "axidraw", "accel", "app") const_speed = ConfigParserProperty(False, "axidraw", "const_speed", "app") path = ObjectProperty() layer_list = ObjectProperty() layer_visibility = DictProperty() plot_running = BooleanProperty() page_format = StringProperty("n/a") def __init__(self): super().__init__() self.document: Optional[vp.Document] = None self._plot_thread: Optional[threading.Thread] = None self._clock: Optional[ClockEvent] = None self.plot_running = False def build(self): # Create the screen manager sm = ScreenManager() sm.add_widget(FileChooserScreen(name="file")) sm.add_widget(ParamsScreen(name="params")) sm.add_widget(LayersScreen(name="layers")) sm.add_widget(PlottingScreen(name="plot")) return sm def get_application_config(self, *args, **kwargs): return super().get_application_config("~/.taxi.ini") def build_config(self, config): config.setdefaults( "axidraw", { "speed_pendown": 25, "speed_penup": 75, "accel": 75, "pen_pos_down": 40, "pen_pos_up": 60, "pen_rate_lower": 50, "pen_rate_raise": 75, "pen_delay_down": 0, "pen_delay_up": 0, "const_speed": False, "model": 2, "port": "", }, ) config.setdefaults( "taxi", { "svg_dir": os.path.expanduser("~/.taxi_svgs"), "rotate": False, }, ) config.setdefaults( "notif", { "type": "none", "host": "homeassistant.local", "port": 8123, "service": "notify.notify", "token": "", }, ) def build_settings(self, settings): settings.add_json_panel( "Taxi", self.config, str(pathlib.Path(__file__).parent / "settings.json")) def layer_visible(self, layer_id: int) -> bool: return self.layer_visibility.get(layer_id, True) def load_svg(self) -> None: self.document = vp.read_multilayer_svg(str(self.path), quantization=0.1) self.layer_visibility.clear() self.layer_list.populate(self.document) # create page size label page_size = self.document.page_size if page_size[0] < page_size[1]: landscape = False else: page_size = tuple(reversed(page_size)) landscape = True format_name = "" for name, sz in vp.PAGE_SIZES.items(): if math.isclose(sz[0], page_size[0], abs_tol=0.01) and math.isclose( sz[1], page_size[1], abs_tol=0.01): format_name = name break s = (f"{self.document.page_size[0] / 96 * 25.4:.1f}x" f"{self.document.page_size[1] / 96 * 25.4:.1f}mm") if format_name != "": s += f" ({format_name} {'landscape' if landscape else'portrait'})" self.page_format = s def apply_axy_options(self): axy.set_option("speed_pendown", self.config.getint("axidraw", "speed_pendown")) axy.set_option("speed_penup", self.config.getint("axidraw", "speed_penup")) axy.set_option("accel", self.config.getint("axidraw", "accel")) axy.set_option("pen_pos_down", self.config.getint("axidraw", "pen_pos_down")) axy.set_option("pen_pos_up", self.config.getint("axidraw", "pen_pos_up")) axy.set_option("pen_rate_lower", self.config.getint("axidraw", "pen_rate_lower")) axy.set_option("pen_rate_raise", self.config.getint("axidraw", "pen_rate_raise")) axy.set_option("pen_delay_down", self.config.getint("axidraw", "pen_delay_down")) axy.set_option("pen_delay_up", self.config.getint("axidraw", "pen_delay_up")) axy.set_option("const_speed", self.config.getboolean("axidraw", "const_speed")) axy.set_option("model", self.config.getint("axidraw", "model")) axy.set_option("port", str(self.config.get("axidraw", "port"))) # default option axy.set_option("auto_rotate", False) def pen_up(self): self.apply_axy_options() axy.pen_up() def pen_down(self): self.apply_axy_options() axy.pen_down() def motor_off(self): Logger.info("disabling XY motors") self.apply_axy_options() axy.shutdown() def on_start(self): self.motor_off() def on_stop(self): self.motor_off() def start_plot(self): self.apply_axy_options() self._plot_thread = threading.Thread( target=self.run_plot, args=( self.document, self.config.getboolean("taxi", "rotate"), self.layer_visibility, ), ) Logger.info("starting plot") self.plot_running = True self._plot_thread.start() self._clock = Clock.schedule_interval(self.check_plot, 0.1) def send_notification(self, message: str) -> None: notif_type = self.config.get("notif", "type") if notif_type == "hass": asyncio.get_event_loop().create_task( self.send_notification_hass(message)) async def send_notification_hass(self, message: str): port = self.config.getint("notif", "port") host = self.config.get("notif", "host") token = self.config.get("notif", "token") service = self.config.get("notif", "service").replace(".", "/") Logger.info("Sending notification") async with aiohttp.ClientSession() as session: async with session.post( f"http://{host}:{port}/api/services/{service}", headers={ "Content-Type": "application/json", "Authorization": f"Bearer {token}", }, json={"message": message}, ) as response: Logger.info(f"Notification sent (status: {response.status}, " f"answer: {await response.text()})") # noinspection PyUnusedLocal def check_plot(self, dt): if self._plot_thread is not None and not self._plot_thread.is_alive(): Logger.info("plot completed") self._plot_thread = None self._clock.cancel() self.plot_running = False self.send_notification("Plot completed") @staticmethod def run_plot( document: vp.Document, rotate: bool, layer_visibility: Mapping[int, bool], ): """caution: axy must be pre-configured!""" if document is None: Logger.warning("self.document is None") return doc = document.empty_copy() for layer_id in document.layers: if layer_visibility.get(layer_id, True): Logger.info(f"adding layer {layer_id}") doc.layers[layer_id] = document.layers[layer_id] if rotate: Logger.info("rotating SVG") doc = copy.deepcopy(doc) doc.rotate(-math.pi / 2) doc.translate(0, doc.page_size[0]) doc.page_size = tuple(reversed(doc.page_size)) # convert to SVG str_io = io.StringIO() vp.write_svg(str_io, doc) svg = str_io.getvalue() # plot axy.plot_svg(svg)
class AnkiCardGenApp(MDApp): """Main App.""" # Data template = ObjectProperty(force_dispatch=True) # Config apkg_export_dir = ConfigParserProperty( HOME / "ankicardgen", "Paths", "apkg_export_dir", "app", ) import_dir = ConfigParserProperty(HOME, "Paths", "import_dir", "app") kobo_import_dir = ConfigParserProperty(HOME, "Paths", "kobo_import_dir", "app") anki_template_dir = ConfigParserProperty("vocab_card", "Paths", "anki_template_dir", "app") anki_deck_name = ConfigParserProperty("Portuguese::Vocab", "Anki", "deck_name", "app") primary_palette = ConfigParserProperty("Red", "Theme", "primary_palette", "app") accent_palette = ConfigParserProperty("Amber", "Theme", "accent_palette", "app") theme_style = ConfigParserProperty("Light", "Theme", "theme_style", "app") source_language = ConfigParserProperty("en", "Template", "source_language", "app") target_language = ConfigParserProperty("pt", "Template", "target_language", "app") current_template_name = ConfigParserProperty("Portuguese Vocabulary (en)", "Template", "name", "app") # TODO: fix bug where default value has to be a valid recipe templates = AliasProperty( getter=lambda *_: template_cookbook.get_recipe_names()) word_state_dict = DictProperty() busy = BooleanProperty(False) busy_modal = ObjectProperty(None) file_manager = ObjectProperty(None) dropdown_menu = ObjectProperty(None) def get_anki_template_dir(self): """Return absolute path where html-, css- and js-files for anki-card is located.""" return os.path.join(ANKI_DIR, self.anki_template_dir) @staticmethod def get_application_config(): """Return default path for the config.""" return str(CONFIG_PATH) def build_config(self, config): # pylint: disable=no-self-use """If no config-file exists, sets the default.""" config.setdefaults( "Theme", { "primary_palette": "Red", "accent_palette": "Amber", "theme_style": "Light", }, ) config.setdefaults("Paths", {}) def bind_theme_cls_and_config(self): """Bind :attr:`theme_cls` and the corresponding :class:`~kivy.properties.ConfigParserProperties`.""" keys = self.config["Theme"] self.bind(**{key: self.theme_cls.setter(key) for key in keys}) self.theme_cls.bind(**{key: self.setter(key) for key in keys}) for key in keys: setattr(self.theme_cls, key, getattr(self, key)) def build(self): """Set up App and return :class:`custom_widgets.MainMenu` as root widget.""" self.bind_theme_cls_and_config() self.file_manager = MDFileManager() Config.set("input", "mouse", "mouse,disable_multitouch") os.makedirs(self.apkg_export_dir, exist_ok=True) return MainMenu( screen_dicts=screens.screen_dicts, screen_dir=str(screens.SCREEN_DIR), image_source=str(ASSETS_DIR / "AnkiCardGen.png"), ) @db_session def get_current_template_db(self): """Return data-base object for :attr:`current_template_name`.""" return db.Template.get(name=self.current_template_name) or db.Template( name=self.current_template_name) def get_word_states(self): """Return dict of word-states for current template from data-base.""" with db_session: return { card.name: card.state for card in self.get_current_template_db().cards } def new_template_instance(self): """Return new instance of current template class.""" return template_cookbook.cook(self.current_template_name) def on_current_template_name(self, *_): """Set up new template if :attr:`current_template_name` changes.""" self.template = self.new_template_instance() self.word_state_dict = self.get_word_states() def on_start(self): """Set up template on start of app.""" super().on_start() self.on_current_template_name() self.request_permissions() def on_pause(self): # pylint: disable=no-self-use """Enable coming back to app.""" return True @mainthread def on_busy(self, *_): """Set up :attr:`busy_modal` if necessary. Then open or close it depending on state of :attr:`busy`.""" if not self.busy_modal: self.busy_modal = ModalView( auto_dismiss=False, size_hint=(1.2, 1.2), opacity=0.5, ) spinner = MDSpinner(active=False, size_hint=(0.5, 0.5)) self.busy_modal.add_widget(spinner) self.bind(busy=spinner.setter("active")) if self.busy: self.busy_modal.open() else: self.busy_modal.dismiss() @staticmethod def request_permissions(): """Request storage permissions on android.""" if platform == "android": from android.permissions import ( # pylint: disable=import-outside-toplevel Permission, request_permissions, ) request_permissions([ Permission.READ_EXTERNAL_STORAGE, Permission.WRITE_EXTERNAL_STORAGE ])
class OctoPiPanelApp(App): backlight_timeout = ConfigParserProperty(0, 'pitft', 'backlight_timeout', 'app', val_type=int) def __init__(self, **kwargs): super(OctoPiPanelApp, self).__init__(**kwargs) self.interval = 1 # backlight self.backlight_idle = Clock.get_boottime() self.backlight_on = True self.octoprint = None self.pitft = PiTFT(fake=not cfg.PITFT) # settings are in app path resources.resource_add_path(self.directory) def update(self, _): # Is it time to turn of the backlight? if self.backlight_timeout > 0: if (Clock.get_boottime() - self.backlight_idle > self.backlight_timeout and self.backlight_on): # disable the backlight self.pitft.disable_backlight() self.backlight_idle = Clock.get_boottime() self.backlight_on = False def stop(self): Clock.unschedule(self.update) """ Clean up """ # enable the backlight before quiting self.pitft.enable_backlight() # OctoPiPanel is going down. Logger.info("OctoPiPanel is going down.") super(OctoPiPanelApp, self).stop() def on_config_change(self, config, section, key, value): if section == 'server': self.octoprint = OctoPrintClient( self.config.get('server', 'url'), self.config.get('server', 'api-key'), cfg.FAKE) def build_config(self, config): config.setdefaults('pitft', { 'backlight_timeout': 30, }) config.setdefaults( 'server', { 'url': 'http://octopi.local', 'api-key': '<api key here>', 'job_interval': 5, 'status_interval': 2 }) def build_settings(self, settings): settings.add_json_panel('OPP', self.config, resources.resource_find('app_settings.json')) def build(self): """ Kivy App entry point. :return: """ # window title self.title = 'OctoPi Panel' # todo: font self.octoprint = OctoPrintClient(self.config.get('server', 'url'), self.config.get('server', 'api-key'), cfg.FAKE) # I couldnt seem to get at pin 252 for the backlight using the usual method, # but this seems to work self.pitft.init() root = Builder.load_file('octopipanel.kv') root.ids.print.init(self.octoprint) root.ids.status.init(self.octoprint) Logger.info('OctoPiPanel initiated') # start updates Clock.schedule_interval(self.update, self.interval) # handle re-enabling backlight on click and refreshing timer def wakeup(*_): self.backlight_idle = Clock.get_boottime() if not self.backlight_on: print('turn back on!') self.pitft.enable_backlight() self.backlight_on = True root.bind(on_touch_down=wakeup) return root def reboot(self): self.stop() if not cfg.FAKE: self.octoprint.reboot() # os.system('reboot') def shutdown(self): self.stop() if not cfg.FAKE: self.octoprint.shutdown() # os.system('shutdown -h 0') def restart(self): if not cfg.FAKE: self.octoprint.restart()