Beispiel #1
0
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
Beispiel #2
0
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)
Beispiel #4
0
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()
Beispiel #5
0
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)
Beispiel #6
0
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}'
Beispiel #7
0
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)
Beispiel #8
0
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
Beispiel #9
0
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()
Beispiel #10
0
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)
Beispiel #11
0
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
Beispiel #12
0
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"
    )
Beispiel #13
0
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.."
Beispiel #14
0
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)
Beispiel #15
0
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()
Beispiel #20
0
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
Beispiel #21
0
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)
Beispiel #22
0
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)
Beispiel #24
0
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")
Beispiel #25
0
class MyLabel(Button):
    foo = ConfigParserProperty(None, 'section', 'foo', 'app')
Beispiel #26
0
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
Beispiel #28
0
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)
Beispiel #29
0
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
            ])
Beispiel #30
0
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()