Exemplo n.º 1
0
    def __init__(self, **kwargs):
        super(RaceCaptureApp, self).__init__(**kwargs)

        if kivy.platform in ['ios', 'macosx', 'linux']:
            kivy.resources.resource_add_path(
                os.path.join(os.path.dirname(os.path.realpath(__file__)),
                             "data"))

        # We do this because when this app is bundled into a standalone app
        # by pyinstaller we must reference all files by their absolute paths
        # sys._MEIPASS is provided by pyinstaller
        if getattr(sys, 'frozen', False):
            self.base_dir = sys._MEIPASS
        else:
            self.base_dir = os.path.dirname(os.path.abspath(__file__))

        self.settings = SystemSettings(self.user_data_dir,
                                       base_dir=self.base_dir)
        self.settings.userPrefs.bind(on_pref_change=self._on_preference_change)

        self.track_manager = TrackManager(
            user_dir=self.settings.get_default_data_dir(),
            base_dir=self.base_dir)
        self.preset_manager = PresetManager(
            user_dir=self.settings.get_default_data_dir(),
            base_dir=self.base_dir)

        # RaceCapture communications API
        self._rc_api = RcpApi(on_disconnect=self._on_rcp_disconnect,
                              settings=self.settings)

        self._databus = DataBusFactory().create_standard_databus(
            self.settings.systemChannels)
        self.settings.runtimeChannels.data_bus = self._databus
        self._datastore = CachingAnalysisDatastore(databus=self._databus)
        self._session_recorder = SessionRecorder(self._datastore,
                                                 self._databus, self._rc_api,
                                                 self.settings,
                                                 self.track_manager,
                                                 self._status_pump)
        self._session_recorder.bind(on_recording=self._on_session_recording)

        HelpInfo.settings = self.settings

        # Ensure soft input mode text inputs aren't obstructed
        Window.softinput_mode = 'below_target'

        # Capture keyboard events for handling escape / back
        Window.bind(on_keyboard=self._on_keyboard)

        self.register_event_type('on_tracks_updated')
        self.processArgs()
        self.settings.appConfig.setUserDir(self.user_data_dir)
        self.setup_telemetry()
Exemplo n.º 2
0
 def __init__(self, **kwargs):
     Builder.load_file(ANALYSIS_VIEW_KV)
     super(AnalysisView, self).__init__(**kwargs)
     self._datastore = CachingAnalysisDatastore()
     self.register_event_type('on_tracks_updated')
     self._databus = kwargs.get('dataBus')
     self._settings = kwargs.get('settings')
     self._track_manager = kwargs.get('track_manager')
     self.ids.sessions_view.bind(on_lap_selection=self.lap_selection)
     self.ids.channelvalues.color_sequence = self._color_sequence
     self.ids.mainchart.color_sequence = self._color_sequence
     self.stream_connecting = False
     Window.bind(mouse_pos=self.on_mouse_pos)
     Window.bind(on_motion=self.on_motion)
     self.init_view()
Exemplo n.º 3
0
 def __init__(self, **kwargs):
     Builder.load_file(ANALYSIS_VIEW_KV)
     super(AnalysisView, self).__init__(**kwargs)
     self._datastore = CachingAnalysisDatastore()
     self.register_event_type('on_tracks_updated')
     self._databus = kwargs.get('dataBus')
     self._settings = kwargs.get('settings') 
     self._track_manager = kwargs.get('track_manager')
     self.ids.sessions_view.bind(on_lap_selected=self.lap_selected)
     self.ids.channelvalues.color_sequence = self._color_sequence
     self.ids.mainchart.color_sequence = self._color_sequence
     self.stream_connecting = False
     self.init_view()
Exemplo n.º 4
0
 def __init__(self, **kwargs):
     super(AnalysisView, self).__init__(**kwargs)
     self._datastore = CachingAnalysisDatastore()
     self.register_event_type('on_tracks_updated')
     self._databus = kwargs.get('dataBus')
     self._settings = kwargs.get('settings')
     self._track_manager = kwargs.get('track_manager')
     self.ids.sessions_view.bind(on_lap_selection=self.lap_selection)
     self.ids.sessions_view.bind(on_session_updated=self.session_updated)
     self.ids.sessions_view.bind(on_sessions_loaded=self.sessions_loaded)
     self.ids.channelvalues.color_sequence = self._color_sequence
     self.ids.mainchart.color_sequence = self._color_sequence
     self.stream_connecting = False
     Window.bind(mouse_pos=self.on_mouse_pos)
     Window.bind(on_motion=self.on_motion)
     self._layout_complete = False
Exemplo n.º 5
0
    def __init__(self, **kwargs):
        super(RaceCaptureApp, self).__init__(**kwargs)

        if kivy.platform in ['ios', 'macosx', 'linux']:
            kivy.resources.resource_add_path(os.path.join(os.path.dirname(os.path.realpath(__file__)), "data"))

        # We do this because when this app is bundled into a standalone app
        # by pyinstaller we must reference all files by their absolute paths
        # sys._MEIPASS is provided by pyinstaller
        if getattr(sys, 'frozen', False):
            self.base_dir = sys._MEIPASS
        else:
            self.base_dir = os.path.dirname(os.path.abspath(__file__))

        self.settings = SystemSettings(self.user_data_dir, base_dir=self.base_dir)
        self.settings.userPrefs.bind(on_pref_change=self._on_preference_change)

        self.track_manager = TrackManager(user_dir=self.settings.get_default_data_dir(), base_dir=self.base_dir)
        self.preset_manager = PresetManager(user_dir=self.settings.get_default_data_dir(), base_dir=self.base_dir)

        # RaceCapture communications API
        self._rc_api = RcpApi(on_disconnect=self._on_rcp_disconnect, settings=self.settings)

        self._databus = DataBusFactory().create_standard_databus(self.settings.systemChannels)
        self.settings.runtimeChannels.data_bus = self._databus
        self._datastore = CachingAnalysisDatastore(databus=self._databus)
        self._session_recorder = SessionRecorder(self._datastore, self._databus, self._rc_api, self.settings, self.track_manager, self._status_pump)
        self._session_recorder.bind(on_recording=self._on_session_recording)


        HelpInfo.settings = self.settings

        # Ensure soft input mode text inputs aren't obstructed
        Window.softinput_mode = 'below_target'

        # Capture keyboard events for handling escape / back
        Window.bind(on_keyboard=self._on_keyboard)

        self.register_event_type('on_tracks_updated')
        self.processArgs()
        self.settings.appConfig.setUserDir(self.user_data_dir)
        self.setup_telemetry()
Exemplo n.º 6
0
class RaceCaptureApp(App):

    # things that care about configuration being loaded
    config_listeners = []

    # things that care about tracks being loaded
    tracks_listeners = []

    # map of view keys to factory functions for building top level views
    view_builders = {}

    # container for all settings
    settings = None

    # Central RCP configuration object
    rc_config = RcpConfig()

    # dataBus provides an eventing / polling mechanism to parts of the system that care
    _databus = None

    # pumps data from rcApi to dataBus. kind of like a bridge
    _data_bus_pump = DataBusPump()

    _status_pump = StatusPump()

    # Track database manager
    track_manager = None

    # Application Status bars
    status_bar = None

    # main navigation menu
    mainNav = None

    # Main Screen Manager
    screenMgr = None

    # main view references for dispatching notifications
    mainViews = {}

    # application arguments - initialized upon startup
    app_args = []

    use_kivy_settings = False

    base_dir = None

    _telemetry_connection = None

    @staticmethod
    def get_app_version():
        return __version__

    @property
    def user_data_dir(self):
        # this is a workaround for a kivy bug in which /sdcard is the hardcoded path for
        # the user dir.  This fails on Android 7.0 systems.
        # this function should be removed when the bug is fixed in kivy.
        if kivy.platform == 'android':
            from jnius import autoclass
            env = autoclass('android.os.Environment')
            data_dir = os.path.join(env.getExternalStorageDirectory().getPath(), self.name)
        else:
            data_dir = super(RaceCaptureApp, self).user_data_dir
        return data_dir

    def __init__(self, **kwargs):
        super(RaceCaptureApp, self).__init__(**kwargs)

        if kivy.platform in ['ios', 'macosx', 'linux']:
            kivy.resources.resource_add_path(os.path.join(os.path.dirname(os.path.realpath(__file__)), "data"))

        # We do this because when this app is bundled into a standalone app
        # by pyinstaller we must reference all files by their absolute paths
        # sys._MEIPASS is provided by pyinstaller
        if getattr(sys, 'frozen', False):
            self.base_dir = sys._MEIPASS
        else:
            self.base_dir = os.path.dirname(os.path.abspath(__file__))

        self.settings = SystemSettings(self.user_data_dir, base_dir=self.base_dir)
        self.settings.userPrefs.bind(on_pref_change=self._on_preference_change)

        self.track_manager = TrackManager(user_dir=self.settings.get_default_data_dir(), base_dir=self.base_dir)
        self.preset_manager = PresetManager(user_dir=self.settings.get_default_data_dir(), base_dir=self.base_dir)

        # RaceCapture communications API
        self._rc_api = RcpApi(on_disconnect=self._on_rcp_disconnect, settings=self.settings)

        self._databus = DataBusFactory().create_standard_databus(self.settings.systemChannels)
        self.settings.runtimeChannels.data_bus = self._databus
        self._datastore = CachingAnalysisDatastore(databus=self._databus)
        self._session_recorder = SessionRecorder(self._datastore, self._databus, self._rc_api, self.settings, self.track_manager, self._status_pump)
        self._session_recorder.bind(on_recording=self._on_session_recording)


        HelpInfo.settings = self.settings

        # Ensure soft input mode text inputs aren't obstructed
        Window.softinput_mode = 'below_target'

        # Capture keyboard events for handling escape / back
        Window.bind(on_keyboard=self._on_keyboard)

        self.register_event_type('on_tracks_updated')
        self.processArgs()
        self.settings.appConfig.setUserDir(self.user_data_dir)
        self.setup_telemetry()

    def on_pause(self):
        return True

    def _on_keyboard(self, keyboard, key, scancode, codepoint, modifier):
        if key == 27:  # go up to home screen
            self.switchMainView('home')

        if not self.screenMgr.current == 'home':
            return

        if key == 97:  # ASCII 'a'
            self.switchMainView('analysis')

        if key == 100:  # ASCII 'd'
            self.switchMainView('dash')

        if key == 115:  # ASCII 's'
            self.switchMainView('config')

        if key == 112:  # ASCII 'p'
            self.switchMainView('preferences')

        if key == 116:  # ASCII 't'
            self.switchMainView('status')

        if key == 113 and 'ctrl' in modifier:  # ctrl-q
            self._shutdown_app()


    def processArgs(self):
        parser = argparse.ArgumentParser(description='Autosport Labs Race Capture App')
        parser.add_argument('-p', '--port', help='Port', required=False)
        parser.add_argument('--telemetryhost', help='Telemetry host', required=False)
        parser.add_argument('--conn_type', help='Connection type', required=False, choices=['bt', 'serial', 'wifi'])

        if sys.platform == 'win32':
            parser.add_argument('--multiprocessing-fork', required=False, action='store_true')

        self.app_args = vars(parser.parse_args())

    def getAppArg(self, name):
        return self.app_args.get(name, None)

    def _init_tracks_success(self):
        Logger.info('RaceCaptureApp: Current tracks loaded')
        Clock.schedule_once(lambda dt: self.notifyTracksUpdated())

    def _init_tracks_error(self, details):
        Logger.error('RaceCaptureApp: Error initializing tracks: {}'.format(details))

    def _init_presets_success(self):
        Logger.info('RaceCaptureApp: Current presets loaded')

    def _init_presets_error(self, details):
        Logger.error('RaceCaptureApp: Error initializing presets: {}'.format(details))

    def init_data(self):
        self.track_manager.init(None, self._init_tracks_success, self._init_tracks_error)
        self.preset_manager.init(None, self._init_presets_success, self._init_presets_error)
        self._init_datastore()

    def _init_datastore(self):
        def _init_datastore(dstore_path):
            Logger.info('RaceCaptureApp:initializing datastore...')
            self._datastore.open_db(dstore_path)

        dstore_path = self.settings.userPrefs.datastore_location
        Logger.info("RaceCaptureApp:Datastore Path:" + str(dstore_path))
        t = Thread(target=_init_datastore, args=(dstore_path,))
        t.daemon = True
        t.start()

    def _serial_warning(self):
        alertPopup('Warning', 'Command failed. Ensure you have selected a correct serial port')

    # Write Configuration
    def on_write_config(self, instance, *args):
        rcpConfig = self.rc_config
        try:
            self._rc_api.writeRcpCfg(rcpConfig, self.on_write_config_complete, self.on_write_config_error)
            self.showActivity("Writing configuration")
        except:
            logging.exception('')
            self._serial_warning()

    def on_write_config_complete(self, result):
        Logger.info("RaceCaptureApp: Config written")
        self.showActivity("Writing completed")
        self.rc_config.stale = False
        self._data_bus_pump.meta_is_stale()
        for listener in self.config_listeners:
            Clock.schedule_once(lambda dt, inner_listener=listener: inner_listener.dispatch('on_config_written', self.rc_config))

    def on_write_config_error(self, detail):
        alertPopup('Error Writing', 'Could not write configuration:\n\n' + str(detail))

    # Read Configuration
    def on_read_config(self, instance, *args):
        try:
            self._rc_api.getRcpCfg(self.rc_config, self.on_read_config_complete, self.on_read_config_error)
            self.showActivity("Reading configuration")
        except:
            logging.exception('')
            self._serial_warning()

    def on_read_config_complete(self, rcpCfg):
        for listener in self.config_listeners:
            Clock.schedule_once(lambda dt, inner_listener=listener: inner_listener.dispatch('on_config_updated', self.rc_config))
        self.rc_config.stale = False

    def on_read_config_error(self, detail):
        self.showActivity("Error reading configuration")
        toast("Error reading configuration. Check your connection", length_long=True)
        Logger.error("RaceCaptureApp:Error reading configuration: {}".format(str(detail)))

    def on_tracks_updated(self, track_manager):
        for view in self.tracks_listeners:
            view.dispatch('on_tracks_updated', self.track_manager)

    def notifyTracksUpdated(self):
        self.dispatch('on_tracks_updated', self.track_manager)

    def on_main_menu_item(self, instance, value):
        if value == 'exit':
            self._shutdown_app()

        self.switchMainView(value)

    def on_main_menu(self, instance, *args):
        self.mainNav.toggle_state()

    def showStatus(self, status, isAlert):
        self.status_bar.dispatch('on_status', status, isAlert)

    def showActivity(self, status):
        self.status_bar.dispatch('on_activity', status)

    def _setX(self, x):
        pass

    def _getX(self):
        pass

    def on_start(self):
        pass

    def _stop_workers(self):
        self._status_pump.stop()
        self._data_bus_pump.stop()
        self._rc_api.shutdown_api()
        self._telemetry_connection.telemetry_enabled = False

    def _shutdown_app(self):
        Logger.info('RaceCaptureApp: Shutting down app')
        self._stop_workers()
        App.get_running_app().stop()

    def on_stop(self):
        self._stop_workers()

    def _get_main_screen(self, view_name):
        view = self.mainViews.get(view_name)
        if not view:
            view = self.view_builders[view_name]()
            self.mainViews[view_name] = view
        return view

    def _show_main_view(self, view_name):
        screen = self._get_main_screen(view_name)

        screen_mgr = self.screenMgr
        if screen_mgr.has_screen(screen.name):
            screen_mgr.current = screen.name
        else:
            self.screenMgr.switch_to(screen)

        self._session_recorder.on_view_change(view_name)
        self._data_bus_pump.on_view_change(view_name)

    def switchMainView(self, view_name):
            self.mainNav.anim_to_state('closed')
            Clock.schedule_once(lambda dt: self._show_main_view(view_name), 0.25)

    def build_config_view(self):
        config_view = ConfigView(name='config',
                                rcpConfig=self.rc_config,
                                rc_api=self._rc_api,
                                databus=self._databus,
                                settings=self.settings,
                                base_dir=self.base_dir,
                                track_manager=self.track_manager,
                                preset_manager=self.preset_manager,
                                 status_pump=self._status_pump)
        config_view.bind(on_read_config=self.on_read_config)
        config_view.bind(on_write_config=self.on_write_config)
        self.config_listeners.append(config_view)
        self.tracks_listeners.append(config_view)
        return config_view

    def build_status_view(self):
        status_view = StatusView(self.track_manager, self._status_pump, name='status')
        self.tracks_listeners.append(status_view)
        return status_view

    def build_dash_view(self):
        dash_view = DashboardView(self._status_pump, self.track_manager, self._rc_api, self.rc_config, self._databus, self.settings, name='dash')
        self.config_listeners.append(dash_view)
        self.tracks_listeners.append(dash_view)
        return dash_view

    def build_analysis_view(self):
        analysis_view = AnalysisView(name='analysis', datastore=self._datastore, databus=self._databus, settings=self.settings, track_manager=self.track_manager, session_recorder=self._session_recorder)
        self.tracks_listeners.append(analysis_view)
        return analysis_view

    def build_preferences_view(self):
        preferences_view = PreferencesView(name='preferences', settings=self.settings, base_dir=self.base_dir)
        preferences_view.bind(on_pref_change=self._on_preference_change)
        return preferences_view

    def build_homepage_view(self):
        homepage_view = HomePageView(name='home')
        homepage_view.bind(on_select_view=lambda instance, view_name: self.switchMainView(view_name))
        return homepage_view

    def build_setup_view(self):
        setup_view = SetupView(name='setup',
                               track_manager=self.track_manager,
                               preset_manager=self.preset_manager,
                               settings=self.settings,
                               databus=self._databus,
                               base_dir=self.base_dir,
                               rc_api=self._rc_api,
                               rc_config=self.rc_config)
        return setup_view

    def init_view_builders(self):
        self.view_builders = {'config': self.build_config_view,
                              'dash': self.build_dash_view,
                              'analysis': self.build_analysis_view,
                              'preferences': self.build_preferences_view,
                              'status': self.build_status_view,
                              'home': self.build_homepage_view,
                              'setup': self.build_setup_view
                              }

    def build(self):
        self.init_view_builders()

        Builder.load_file('racecapture.kv')
        root = self.root

        status_bar = root.ids.status_bar
        status_bar.bind(on_main_menu=self.on_main_menu)
        self.status_bar = status_bar

        root.ids.main_menu.bind(on_main_menu_item=self.on_main_menu_item)

        self.mainNav = root.ids.main_nav

        # reveal_below_anim
        # reveal_below_simple
        # slide_above_anim
        # slide_above_simple
        # fade_in
        self.mainNav.anim_type = 'slide_above_anim'

        rc_api = self._rc_api
        rc_api.on_progress = lambda value: status_bar.dispatch('on_progress', value)
        rc_api.on_rx = lambda value: status_bar.dispatch('on_data_rx', value)

        screenMgr = root.ids.main
        # NoTransition
        # SlideTransition
        # SwapTransition
        # FadeTransition
        # WipeTransition
        # FallOutTransition
        # RiseInTransition
        screenMgr.transition = NoTransition()  # FallOutTransition()  # NoTransition()

        self.screenMgr = screenMgr
        self.icon = ('resource/images/app_icon_128x128.ico' if sys.platform == 'win32' else 'resource/images/app_icon_128x128.png')
        Clock.schedule_once(lambda dt: self.post_launch(), 1.0)

    def post_launch(self):
        self._setup_toolbar()
        Clock.schedule_once(lambda dt: self.init_data())
        Clock.schedule_once(lambda dt: self.init_rc_comms())
        Clock.schedule_once(lambda dt: self._show_startup_view())

    def _show_preferred_view(self):
        settings_to_view = {'Home Page':'home',
                            'Dashboard':'dash',
                            'Analysis': 'analysis',
                            'Setup': 'config' }
        view_pref = self.settings.userPrefs.get_pref('preferences', 'startup_screen')
        self._show_main_view(settings_to_view.get(view_pref, 'home'))

    def _show_startup_view(self):
        # should we show the stetup wizard?
        setup_enabled = self.settings.userPrefs.get_pref_bool('setup', 'setup_enabled')
        if setup_enabled:
            setup_view = self._get_main_screen('setup')
            setup_view.bind(on_setup_complete=lambda x: self._show_preferred_view())
            self._show_main_view('setup')
        else:
            self._show_preferred_view()

    def init_rc_comms(self):
        port = self.getAppArg('port')
        conn_type = self.settings.userPrefs.get_pref('preferences', 'conn_type', default=None)

        cli_conn_type = self.getAppArg('conn_type')

        if cli_conn_type:
            conn_type = cli_conn_type

        Logger.info("RaceCaptureApp: initializing rc comms with, conn type: {}".format(conn_type))

        comms = comms_factory(port, conn_type)
        rc_api = self._rc_api
        rc_api.detect_win_callback = self.rc_detect_win
        rc_api.detect_fail_callback = self.rc_detect_fail
        rc_api.detect_activity_callback = self.rc_detect_activity
        rc_api.init_api(comms)
        rc_api.run_auto_detect()

    def rc_detect_win(self, version):
        if version.is_compatible_version():
            version_string = version.git_info if version.git_info is not '' else 'v' + version.version_string()
            self.showStatus("{} {}".format(version.friendlyName, version_string), False)
            self._data_bus_pump.start(self._databus, self._rc_api, self._session_recorder, self._rc_api.comms.supports_streaming)
            self._status_pump.start(self._rc_api)
            self._telemetry_connection.data_connected = True

            if self.rc_config.loaded == False:
                Clock.schedule_once(lambda dt: self.on_read_config(self))
            else:
                self.showActivity('Connected')
        else:
            alertPopup('Incompatible Firmware', 'Detected {} v{}\n\nPlease upgrade firmware to {} or higher'.format(
                               version.friendlyName,
                               version.version_string(),
                               VersionConfig.get_minimum_version().version_string()
                               ))

    def rc_detect_fail(self):

        def re_detect():
            if not self._rc_api.comms.isOpen():
                self._rc_api.run_auto_detect()

        self.showStatus("Connecting...", True)
        Clock.schedule_once(lambda dt: re_detect(), 1.0)

    def rc_detect_activity(self, info):
        self.showActivity('Searching {}'.format(info))

    def _on_rcp_disconnect(self):
        if self._telemetry_connection.data_connected:
            self._telemetry_connection.data_connected = False


    def open_settings(self, *largs):
        self.switchMainView('preferences')

    def _setup_toolbar(self):
        status_bar = self.root.ids.status_bar
        status_bar.status_pump = self._status_pump
        status_bar.track_manager = self.track_manager

    def setup_telemetry(self):
        host = self.getAppArg('telemetryhost')

        telemetry_enabled = True if self.settings.userPrefs.get_pref('preferences', 'send_telemetry') == "1" else False

        self._telemetry_connection = TelemetryManager(self._databus, host=host, telemetry_enabled=telemetry_enabled)
        self.config_listeners.append(self._telemetry_connection)
        self._telemetry_connection.bind(on_connecting=self.telemetry_connecting)
        self._telemetry_connection.bind(on_connected=self.telemetry_connected)
        self._telemetry_connection.bind(on_disconnected=self.telemetry_disconnected)
        self._telemetry_connection.bind(on_streaming=self.telemetry_streaming)
        self._telemetry_connection.bind(on_error=self.telemetry_error)
        self._telemetry_connection.bind(on_auth_error=self.telemetry_auth_error)

    def telemetry_connecting(self, instance, msg):
        self.status_bar.dispatch('on_tele_status', ToolbarView.TELEMETRY_CONNECTING)
        self.showActivity(msg)

    def telemetry_connected(self, instance, msg):
        self.status_bar.dispatch('on_tele_status', ToolbarView.TELEMETRY_CONNECTING)
        self.showActivity(msg)

    def telemetry_disconnected(self, instance, msg):
        self.status_bar.dispatch('on_tele_status', ToolbarView.TELEMETRY_IDLE)
        self.showActivity(msg)

    def telemetry_streaming(self, instance, msg):
        self.status_bar.dispatch('on_tele_status', ToolbarView.TELEMETRY_ACTIVE)

    def telemetry_auth_error(self, instance, msg):
        self.status_bar.dispatch('on_tele_status', ToolbarView.TELEMETRY_ERROR)
        self.showActivity(msg)

    def telemetry_error(self, instance, msg):
        self.showActivity(msg)
        self.status_bar.dispatch('on_tele_status', ToolbarView.TELEMETRY_ERROR)

    def _on_preference_change(self, instance, section, key, value):
        """Called any time the app preferences are changed
        """
        token = (section, key)

        if token == ('preferences', 'send_telemetry'):
            if value == "1":  # Boolean settings values are 1/0, not True/False
                if self.rc_config.connectivityConfig.cellConfig.cellEnabled:
                    alertPopup('Telemetry error', "Disable the telemetry module before enabling app telemetry.")
                Clock.schedule_once(lambda dt: self._enable_telemetry())
            else:
                Clock.schedule_once(lambda dt: self._disable_telemetry())

        if token == ('preferences', 'conn_type'):
            # User changed their RC connection type
            Logger.info("RaceCaptureApp: RC connection type changed to {}, restarting comms".format(value))
            Clock.schedule_once(lambda dt: self._restart_comms())

    def _enable_telemetry(self):
        self._telemetry_connection.telemetry_enabled = True

    def _disable_telemetry(self):
        self._telemetry_connection.telemetry_enabled = False

    def _restart_comms(self):
        self._data_bus_pump.stop()
        self._status_pump.stop()
        self._rc_api.shutdown_api()
        self.init_rc_comms()

    def _on_session_recording(self, instance, is_recording):
        toast('Session recording started' if is_recording else 'Session recording stopped', length_long=True)
Exemplo n.º 7
0
class AnalysisView(Screen):
    SUGGESTED_CHART_CHANNELS = ['Speed']
    INIT_DATASTORE_TIMEOUT = 10.0
    _settings = None
    _databus = None
    _track_manager = None
    _popup = None
    _color_sequence = ColorSequence()
    sessions = ObjectProperty(None)

    def __init__(self, **kwargs):
        Builder.load_file(ANALYSIS_VIEW_KV)
        super(AnalysisView, self).__init__(**kwargs)
        self._datastore = CachingAnalysisDatastore()
        self.register_event_type('on_tracks_updated')
        self._databus = kwargs.get('dataBus')
        self._settings = kwargs.get('settings') 
        self._track_manager = kwargs.get('track_manager')
        self.ids.sessions_view.bind(on_lap_selected=self.lap_selected)
        self.ids.channelvalues.color_sequence = self._color_sequence
        self.ids.mainchart.color_sequence = self._color_sequence
        self.stream_connecting = False
        self.init_view()

    def on_sessions(self, instance, value):
        self.ids.channelvalues.sessions = value
        
    def lap_selected(self, instance, source_ref, selected):
        source_key = str(source_ref)
        if selected:
            self.ids.mainchart.add_lap(source_ref)
            self.ids.channelvalues.add_lap(source_ref)
            map_path_color = self._color_sequence.get_color(source_key)
            self.ids.analysismap.add_reference_mark(source_key, map_path_color)
            cache = self._datastore.get_location_data(source_ref)
            self._sync_analysis_map(source_ref.session)
            self.ids.analysismap.add_map_path(source_ref, cache, map_path_color)

        else:
            self.ids.mainchart.remove_lap(source_ref)
            self.ids.channelvalues.remove_lap(source_ref)
            self.ids.analysismap.remove_reference_mark(source_key)
            self.ids.analysismap.remove_map_path(source_ref)
            self.ids.analysismap.remove_heat_values(source_ref)
    
    def on_tracks_updated(self, track_manager):
        self.ids.analysismap.track_manager = track_manager

    def on_channel_selected(self, instance, value):
        self.ids.channelvalues.merge_selected_channels(value)

    def on_marker(self, instance, marker):
        source = marker.sourceref
        cache = self._datastore.get_location_data(source)
        if cache != None:
            try:
                point = cache[marker.data_index]
            except IndexError:
                point = cache[len(cache) - 1]
            self.ids.analysismap.update_reference_mark(source, point)
            self.ids.channelvalues.update_reference_mark(source, marker.data_index)
                          
    def _sync_analysis_map(self, session):
        analysis_map = self.ids.analysismap
        if not analysis_map.track:
            lat_avg, lon_avg = self._datastore.get_location_center([session])
            analysis_map.select_map(lat_avg, lon_avg)

    def open_datastore(self):
        pass

    def on_add_stream(self, *args):
        self.show_add_stream_dialog()
                
    def on_stream_connected(self, instance, new_session_id):
        self.stream_connecting = False
        self._dismiss_popup()
        self.ids.sessions_view.refresh_session_list()
        self.check_load_suggested_lap(new_session_id)
    
    #The following selects a best lap if there are no other laps currently selected
    def check_load_suggested_lap(self, new_session_id):
        sessions_view = self.ids.sessions_view
        if len(sessions_view.selected_laps) == 0:
            best_lap = self._datastore.get_channel_min('LapTime', [new_session_id], ['LapCount'])
            if best_lap:
                best_lap_id = best_lap[1]
                Logger.info('AnalysisView: Convenience selected a suggested session {} / lap {}'.format(new_session_id, best_lap_id))
                sessions_view.select_lap(new_session_id, best_lap_id, True)
                main_chart = self.ids.mainchart
                main_chart.select_channels(AnalysisView.SUGGESTED_CHART_CHANNELS)
                HelpInfo.help_popup('suggested_lap', main_chart, arrow_pos='left_mid')
            else:
                Logger.warn('AnalysisView: Could not determine best lap for session {}'.format(new_session_id))
        
    def on_stream_connecting(self, *args):
        self.stream_connecting = True
        
    def show_add_stream_dialog(self):
        self.stream_connecting = False
        content = AddStreamView(settings=self._settings, datastore=self._datastore)
        content.bind(on_connect_stream_start=self.on_stream_connecting)
        content.bind(on_connect_stream_complete=self.on_stream_connected)
        
        popup = Popup(title="Add Telemetry Stream", content=content, size_hint=(0.7, 0.7))
        popup.bind(on_dismiss=self.popup_dismissed)
        popup.open()
        self._popup = popup

    def init_datastore(self):
        def _init_datastore(dstore_path):
            if os.path.isfile(dstore_path):
                self._datastore.open_db(dstore_path)
            else:
                Logger.info('AnalysisView: creating datastore...')
                self._datastore.new(dstore_path)
            self.ids.sessions_view.datastore = self._datastore

        dstore_path = self._settings.userPrefs.datastore_location
        Logger.info("AnalysisView: Datastore Path:" + str(dstore_path))
        t = Thread(target=_init_datastore, args=(dstore_path,))
        t.daemon = True
        t.start()
                
    def init_view(self):
        self.init_datastore()
        mainchart = self.ids.mainchart
        mainchart.settings = self._settings
        mainchart.datastore = self._datastore
        channelvalues = self.ids.channelvalues
        channelvalues.datastore = self._datastore
        channelvalues.settings = self._settings
        self.ids.analysismap.track_manager = self._track_manager
        self.ids.analysismap.datastore = self._datastore
        Clock.schedule_once(lambda dt: HelpInfo.help_popup('beta_analysis_welcome', self, arrow_pos='right_mid'), 0.5)


    def popup_dismissed(self, *args):
        if self.stream_connecting:
            return True
        self._popup = None
        
    def _dismiss_popup(self, *args):
        if self._popup is not None:
            self._popup.dismiss()
            self._popup = None
Exemplo n.º 8
0
class AnalysisView(Screen):
    SUGGESTED_CHART_CHANNELS = ['Speed']
    INIT_DATASTORE_TIMEOUT = 10.0
    _settings = None
    _databus = None
    _track_manager = None
    _popup = None
    _color_sequence = ColorSequence()
    sessions = ObjectProperty(None)
    Builder.load_string(ANALYSIS_VIEW_KV)

    def __init__(self, **kwargs):
        super(AnalysisView, self).__init__(**kwargs)
        self._datastore = CachingAnalysisDatastore()
        self.register_event_type('on_tracks_updated')
        self._databus = kwargs.get('dataBus')
        self._settings = kwargs.get('settings')
        self._track_manager = kwargs.get('track_manager')
        self.ids.sessions_view.bind(on_lap_selection=self.lap_selection)
        self.ids.sessions_view.bind(on_session_updated=self.session_updated)
        self.ids.sessions_view.bind(on_sessions_loaded=self.sessions_loaded)
        self.ids.channelvalues.color_sequence = self._color_sequence
        self.ids.mainchart.color_sequence = self._color_sequence
        self.stream_connecting = False
        Window.bind(mouse_pos=self.on_mouse_pos)
        Window.bind(on_motion=self.on_motion)
        self._layout_complete = False

    def on_motion(self, instance, event, motion_event):
        flyin = self.ids.laps_flyin
        if self.collide_point(motion_event.x, motion_event.y):
            if not flyin.flyin_collide_point(motion_event.x, motion_event.y):
                flyin.schedule_hide()

    def on_mouse_pos(self, x, pos):
        flyin = self.ids.laps_flyin
        x = pos[0]
        y = pos[1]
        self_collide = self.collide_point(x, y)
        flyin_collide = flyin.flyin_collide_point(x, y)
        laps_selected = self.ids.sessions_view.selected_count > 0

        if self_collide and not flyin_collide and laps_selected:
            flyin.schedule_hide()
        return False

    def on_sessions(self, instance, value):
        self.ids.channelvalues.sessions = value

    def session_updated(self, instance, session):
        self.ids.channelvalues.refresh_view()
        self.ids.analysismap.refresh_view()

    def sessions_loaded(self, instance):
        if self.ids.sessions_view.session_count == 0:
            self.show_add_stream_dialog()

    def lap_selection(self, instance, source_ref, selected):
        source_key = str(source_ref)
        if selected:
            self.ids.mainchart.add_lap(source_ref)
            self.ids.channelvalues.add_lap(source_ref)
            map_path_color = self._color_sequence.get_color(source_key)
            self.ids.analysismap.add_reference_mark(source_key, map_path_color)
            self._sync_analysis_map(source_ref.session)
            self._datastore.get_location_data(source_ref, lambda x: self.ids.analysismap.add_map_path(source_ref, x, map_path_color))

        else:
            self.ids.mainchart.remove_lap(source_ref)
            self.ids.channelvalues.remove_lap(source_ref)
            self.ids.analysismap.remove_reference_mark(source_key)
            self.ids.analysismap.remove_map_path(source_ref)

    def on_tracks_updated(self, track_manager):
        self.ids.analysismap.track_manager = track_manager

    def on_channel_selected(self, instance, value):
        self.ids.channelvalues.merge_selected_channels(value)

    def on_marker(self, instance, marker):
        source = marker.sourceref
        self.ids.channelvalues.update_reference_mark(source, marker.data_index)
        cache = self._datastore.get_location_data(source)
        if cache != None:
            try:
                point = cache[marker.data_index]
            except IndexError:
                point = cache[len(cache) - 1]
            self.ids.analysismap.update_reference_mark(source, point)

    def _sync_analysis_map(self, session):
        analysis_map = self.ids.analysismap
        current_track = analysis_map.track

        lat_avg, lon_avg = self._datastore.get_location_center([session])
        new_track = analysis_map.select_map(lat_avg, lon_avg)

        if current_track != new_track:
            # if a new track is selected, then
            # unselect all laps for all other sessions
            sessions_view = self.ids.sessions_view
            sessions_view.deselect_other_laps(session)

    def open_datastore(self):
        pass

    def on_add_stream(self, *args):
        self.show_add_stream_dialog()

    def on_stream_connected(self, instance, new_session_id):
        self.stream_connecting = False
        self._dismiss_popup()
        session = self._datastore.get_session_by_id(new_session_id)
        self.ids.sessions_view.append_session(session)
        self.check_load_suggested_lap(new_session_id)

    # The following selects a best lap if there are no other laps currently selected
    def check_load_suggested_lap(self, new_session_id):
        sessions_view = self.ids.sessions_view
        if len(sessions_view.selected_laps) == 0:
            best_lap = self._datastore.get_channel_min('LapTime', [new_session_id], ['LapCount'])
            best_lap_id = best_lap[1]
            if best_lap_id:
                Logger.info('AnalysisView: Convenience selected a suggested session {} / lap {}'.format(new_session_id, best_lap_id))
                main_chart = self.ids.mainchart
                main_chart.select_channels(AnalysisView.SUGGESTED_CHART_CHANNELS)
                self.ids.channelvalues.select_channels(AnalysisView.SUGGESTED_CHART_CHANNELS)
                sessions_view.select_lap(new_session_id, best_lap_id, True)
                HelpInfo.help_popup('suggested_lap', main_chart, arrow_pos='left_mid')
            else:
                Logger.info('AnalysisView: No best lap could be determined; selecting first lap by default for session {}'.format(new_session_id))
                sessions_view.select_lap(new_session_id, 0, True)

    def on_stream_connecting(self, *args):
        self.stream_connecting = True

    def show_add_stream_dialog(self):
        self.stream_connecting = False
        content = AddStreamView(settings=self._settings, datastore=self._datastore)
        content.bind(on_connect_stream_start=self.on_stream_connecting)
        content.bind(on_connect_stream_complete=self.on_stream_connected)
        content.bind(on_add_session=self.on_add_session)
        content.bind(on_delete_session=self.on_delete_session)
        content.bind(on_close=self.close_popup)

        popup = Popup(title="Add Session", content=content, size_hint=(0.8, 0.7))
        popup.bind(on_dismiss=self.popup_dismissed)
        popup.open()
        self._popup = popup

    def close_popup(self, *args):
        self._popup.dismiss()

    def on_add_session(self, instance, session):
        Logger.info("AnalysisView: on_add_session: {}".format(session))
        self.check_load_suggested_lap(session.session_id)
        self.ids.sessions_view.append_session(session)
        self.check_load_suggested_lap(session.session_id)

    def on_delete_session(self, instance, session):
        self.ids.sessions_view.session_deleted(session)

    def init_view(self):
        self._init_datastore()
        mainchart = self.ids.mainchart
        mainchart.settings = self._settings
        mainchart.datastore = self._datastore
        channelvalues = self.ids.channelvalues
        channelvalues.datastore = self._datastore
        channelvalues.settings = self._settings
        self.ids.analysismap.track_manager = self._track_manager
        self.ids.analysismap.datastore = self._datastore
        self.ids.sessions_view.datastore = self._datastore
        self.ids.sessions_view.settings = self._settings
        self.ids.sessions_view.init_view()
        Clock.schedule_once(lambda dt: HelpInfo.help_popup('beta_analysis_welcome', self, arrow_pos='right_mid'), 0.5)

    def do_layout(self, *largs):
        super(AnalysisView, self).do_layout(largs)
        if not self._layout_complete:
            Clock.schedule_once(lambda dt: self.init_view(), 0.5)
        self._layout_complete = True

    def _init_datastore(self):
        dstore_path = self._settings.userPrefs.datastore_location
        if os.path.isfile(dstore_path):
            self._datastore.open_db(dstore_path)
        else:
            Logger.info('AnalysisView: creating datastore...')
            self._datastore.new(dstore_path)

    def popup_dismissed(self, *args):
        if self.stream_connecting:
            return True
        self._popup = None

    def _dismiss_popup(self, *args):
        if self._popup is not None:
            self._popup.dismiss()
            self._popup = None
Exemplo n.º 9
0
class RaceCaptureApp(App):

    # things that care about configuration being loaded
    config_listeners = []

    # things that care about tracks being loaded
    tracks_listeners = []

    # map of view keys to factory functions for building top level views
    view_builders = {}

    # container for all settings
    settings = None

    # Central RCP configuration object
    rc_config = RcpConfig()

    # dataBus provides an eventing / polling mechanism to parts of the system that care
    _databus = None

    # pumps data from rcApi to dataBus. kind of like a bridge
    _data_bus_pump = DataBusPump()

    _status_pump = StatusPump()

    # Track database manager
    track_manager = None

    # Application Status bars
    status_bar = None

    # main navigation menu
    mainNav = None

    # Main Screen Manager
    screenMgr = None

    # main view references for dispatching notifications
    mainViews = {}

    # application arguments - initialized upon startup
    app_args = []

    use_kivy_settings = False

    base_dir = None

    _telemetry_connection = None

    @staticmethod
    def get_app_version():
        return __version__

    @property
    def user_data_dir(self):
        # this is a workaround for a kivy bug in which /sdcard is the hardcoded path for
        # the user dir.  This fails on Android 7.0 systems.
        # this function should be removed when the bug is fixed in kivy.
        if kivy.platform == 'android':
            from jnius import autoclass
            env = autoclass('android.os.Environment')
            data_dir = os.path.join(env.getExternalStorageDirectory().getPath(), self.name)
        else:
            data_dir = super(RaceCaptureApp, self).user_data_dir
        return data_dir

    def __init__(self, **kwargs):
        super(RaceCaptureApp, self).__init__(**kwargs)

        if kivy.platform in ['ios', 'macosx', 'linux']:
            kivy.resources.resource_add_path(os.path.join(os.path.dirname(os.path.realpath(__file__)), "data"))

        # We do this because when this app is bundled into a standalone app
        # by pyinstaller we must reference all files by their absolute paths
        # sys._MEIPASS is provided by pyinstaller
        if getattr(sys, 'frozen', False):
            self.base_dir = sys._MEIPASS
        else:
            self.base_dir = os.path.dirname(os.path.abspath(__file__))

        self.settings = SystemSettings(self.user_data_dir, base_dir=self.base_dir)
        self.settings.userPrefs.bind(on_pref_change=self._on_preference_change)

        self.track_manager = TrackManager(user_dir=self.settings.get_default_data_dir(), base_dir=self.base_dir)
        self.preset_manager = PresetManager(user_dir=self.settings.get_default_data_dir(), base_dir=self.base_dir)

        # RaceCapture communications API
        self._rc_api = RcpApi(on_disconnect=self._on_rcp_disconnect, settings=self.settings)

        self._databus = DataBusFactory().create_standard_databus(self.settings.systemChannels)
        self.settings.runtimeChannels.data_bus = self._databus
        self._datastore = CachingAnalysisDatastore(databus=self._databus)
        self._session_recorder = SessionRecorder(self._datastore, self._databus, self._rc_api, self.settings, self.track_manager, self._status_pump)
        self._session_recorder.bind(on_recording=self._on_session_recording)


        HelpInfo.settings = self.settings

        # Ensure soft input mode text inputs aren't obstructed
        Window.softinput_mode = 'below_target'

        # Capture keyboard events for handling escape / back
        Window.bind(on_keyboard=self._on_keyboard)

        self.register_event_type('on_tracks_updated')
        self.processArgs()
        self.settings.appConfig.setUserDir(self.user_data_dir)
        self.setup_telemetry()

    def on_pause(self):
        return True

    def _on_keyboard(self, keyboard, key, scancode, codepoint, modifier):
        if key == 27:  # go up to home screen
            self.switchMainView('home')

        if not self.screenMgr.current == 'home':
            return

        if key == 97:  # ASCII 'a'
            self.switchMainView('analysis')

        if key == 100:  # ASCII 'd'
            self.switchMainView('dash')

        if key == 115:  # ASCII 's'
            self.switchMainView('config')

        if key == 112:  # ASCII 'p'
            self.switchMainView('preferences')

        if key == 116:  # ASCII 't'
            self.switchMainView('status')

        if key == 113 and 'ctrl' in modifier:  # ctrl-q
            self._shutdown_app()


    def processArgs(self):
        parser = argparse.ArgumentParser(description='Autosport Labs Race Capture App')
        parser.add_argument('-p', '--port', help='Port', required=False)
        parser.add_argument('--telemetryhost', help='Telemetry host', required=False)
        parser.add_argument('--conn_type', help='Connection type', required=False, choices=['bt', 'serial', 'wifi'])

        if sys.platform == 'win32':
            parser.add_argument('--multiprocessing-fork', required=False, action='store_true')

        self.app_args = vars(parser.parse_args())

    def getAppArg(self, name):
        return self.app_args.get(name, None)

    def _init_tracks_success(self):
        Logger.info('RaceCaptureApp: Current tracks loaded')
        Clock.schedule_once(lambda dt: self.notifyTracksUpdated())

    def _init_tracks_error(self, details):
        Logger.error('RaceCaptureApp: Error initializing tracks: {}'.format(details))

    def _init_presets_success(self):
        Logger.info('RaceCaptureApp: Current presets loaded')

    def _init_presets_error(self, details):
        Logger.error('RaceCaptureApp: Error initializing presets: {}'.format(details))

    def init_data(self):
        self.track_manager.init(None, self._init_tracks_success, self._init_tracks_error)
        self.preset_manager.init(None, self._init_presets_success, self._init_presets_error)
        self._init_datastore()

    def _init_datastore(self):
        def _init_datastore(dstore_path):
            Logger.info('RaceCaptureApp:initializing datastore...')
            self._datastore.open_db(dstore_path)

        dstore_path = self.settings.userPrefs.datastore_location
        Logger.info("RaceCaptureApp:Datastore Path:" + str(dstore_path))
        t = Thread(target=_init_datastore, args=(dstore_path,))
        t.daemon = True
        t.start()

    def _serial_warning(self):
        alertPopup('Warning', 'Command failed. Ensure you have selected a correct serial port')

    # Write Configuration
    def on_write_config(self, instance, *args):
        rcpConfig = self.rc_config
        try:
            self._rc_api.writeRcpCfg(rcpConfig, self.on_write_config_complete, self.on_write_config_error)
            self.showActivity("Writing configuration")
        except:
            logging.exception('')
            self._serial_warning()

    def on_write_config_complete(self, result):
        Logger.info("RaceCaptureApp: Config written")
        self.showActivity("Writing completed")
        self.rc_config.stale = False
        self._data_bus_pump.meta_is_stale()
        for listener in self.config_listeners:
            Clock.schedule_once(lambda dt, inner_listener=listener: inner_listener.dispatch('on_config_written', self.rc_config))

    def on_write_config_error(self, detail):
        alertPopup('Error Writing', 'Could not write configuration:\n\n' + str(detail))

    # Read Configuration
    def on_read_config(self, instance, *args):
        try:
            self._rc_api.getRcpCfg(self.rc_config, self.on_read_config_complete, self.on_read_config_error)
            self.showActivity("Reading configuration")
        except:
            logging.exception('')
            self._serial_warning()

    def on_read_config_complete(self, rcpCfg):
        for listener in self.config_listeners:
            Clock.schedule_once(lambda dt, inner_listener=listener: inner_listener.dispatch('on_config_updated', self.rc_config))
        self.rc_config.stale = False

    def on_read_config_error(self, detail):
        self.showActivity("Error reading configuration")
        toast("Error reading configuration. Check your connection", length_long=True)
        Logger.error("RaceCaptureApp:Error reading configuration: {}".format(str(detail)))

    def on_tracks_updated(self, track_manager):
        for view in self.tracks_listeners:
            view.dispatch('on_tracks_updated', self.track_manager)

    def notifyTracksUpdated(self):
        self.dispatch('on_tracks_updated', self.track_manager)

    def on_main_menu_item(self, instance, value):
        if value == 'exit':
            self._shutdown_app()

        self.switchMainView(value)

    def on_main_menu(self, instance, *args):
        self.mainNav.toggle_state()

    def showStatus(self, status, isAlert):
        self.status_bar.dispatch('on_status', status, isAlert)

    def showActivity(self, status):
        self.status_bar.dispatch('on_activity', status)

    def _setX(self, x):
        pass

    def _getX(self):
        pass

    def on_start(self):
        pass

    def _stop_workers(self):
        self._status_pump.stop()
        self._data_bus_pump.stop()
        self._rc_api.shutdown_api()
        self._telemetry_connection.telemetry_enabled = False

    def _shutdown_app(self):
        Logger.info('RaceCaptureApp: Shutting down app')
        self._stop_workers()
        App.get_running_app().stop()

    def on_stop(self):
        self._stop_workers()

    def _get_main_screen(self, view_name):
        view = self.mainViews.get(view_name)
        if not view:
            view = self.view_builders[view_name]()
            self.mainViews[view_name] = view
        return view

    def _show_main_view(self, view_name):
        screen = self._get_main_screen(view_name)

        screen_mgr = self.screenMgr
        if screen_mgr.has_screen(screen.name):
            screen_mgr.current = screen.name
        else:
            self.screenMgr.switch_to(screen)

        self._session_recorder.on_view_change(view_name)
        self._data_bus_pump.on_view_change(view_name)

    def switchMainView(self, view_name):
            self.mainNav.anim_to_state('closed')
            Clock.schedule_once(lambda dt: self._show_main_view(view_name), 0.25)

    def build_config_view(self):
        config_view = ConfigView(name='config',
                                rcpConfig=self.rc_config,
                                rc_api=self._rc_api,
                                databus=self._databus,
                                settings=self.settings,
                                base_dir=self.base_dir,
                                track_manager=self.track_manager,
                                preset_manager=self.preset_manager,
                                 status_pump=self._status_pump)
        config_view.bind(on_read_config=self.on_read_config)
        config_view.bind(on_write_config=self.on_write_config)
        config_view.bind(on_show_main_view=lambda instance, view: self.switchMainView(view))
        self.config_listeners.append(config_view)
        self.tracks_listeners.append(config_view)
        return config_view

    def build_status_view(self):
        status_view = StatusView(self.track_manager, self._status_pump, name='status')
        self.tracks_listeners.append(status_view)
        return status_view

    def build_dash_view(self):
        dash_view = DashboardView(self._status_pump, self.track_manager, self._rc_api, self.rc_config, self._databus, self.settings, name='dash')
        self.config_listeners.append(dash_view)
        self.tracks_listeners.append(dash_view)
        return dash_view

    def build_analysis_view(self):
        analysis_view = AnalysisView(name='analysis', datastore=self._datastore, databus=self._databus, settings=self.settings, track_manager=self.track_manager, session_recorder=self._session_recorder)
        self.tracks_listeners.append(analysis_view)
        return analysis_view

    def build_preferences_view(self):
        preferences_view = PreferencesView(name='preferences', settings=self.settings, base_dir=self.base_dir)
        preferences_view.bind(on_pref_change=self._on_preference_change)
        return preferences_view

    def build_homepage_view(self):
        homepage_view = HomePageView(name='home')
        homepage_view.bind(on_select_view=lambda instance, view_name: self.switchMainView(view_name))
        return homepage_view

    def build_setup_view(self):
        setup_view = SetupView(name='setup',
                               track_manager=self.track_manager,
                               preset_manager=self.preset_manager,
                               settings=self.settings,
                               databus=self._databus,
                               base_dir=self.base_dir,
                               rc_api=self._rc_api,
                               rc_config=self.rc_config)
        setup_view.bind(on_setup_complete=lambda x: self._show_preferred_view())
        return setup_view

    def init_view_builders(self):
        self.view_builders = {'config': self.build_config_view,
                              'dash': self.build_dash_view,
                              'analysis': self.build_analysis_view,
                              'preferences': self.build_preferences_view,
                              'status': self.build_status_view,
                              'home': self.build_homepage_view,
                              'setup': self.build_setup_view
                              }

    def build(self):
        self.init_view_builders()

        Builder.load_file('racecapture.kv')
        root = self.root

        status_bar = root.ids.status_bar
        status_bar.bind(on_main_menu=self.on_main_menu)
        self.status_bar = status_bar

        root.ids.main_menu.bind(on_main_menu_item=self.on_main_menu_item)

        self.mainNav = root.ids.main_nav

        # reveal_below_anim
        # reveal_below_simple
        # slide_above_anim
        # slide_above_simple
        # fade_in
        self.mainNav.anim_type = 'slide_above_anim'

        rc_api = self._rc_api
        rc_api.on_progress = lambda value: status_bar.dispatch('on_progress', value)
        rc_api.on_rx = lambda value: status_bar.dispatch('on_data_rx', value)

        screenMgr = root.ids.main
        # NoTransition
        # SlideTransition
        # SwapTransition
        # FadeTransition
        # WipeTransition
        # FallOutTransition
        # RiseInTransition
        screenMgr.transition = NoTransition()  # FallOutTransition()  # NoTransition()

        self.screenMgr = screenMgr
        self.icon = ('resource/images/app_icon_128x128.ico' if sys.platform == 'win32' else 'resource/images/app_icon_128x128.png')
        Clock.schedule_once(lambda dt: self.post_launch(), 1.0)

    def post_launch(self):
        self._setup_toolbar()
        Clock.schedule_once(lambda dt: self.init_data())
        Clock.schedule_once(lambda dt: self.init_rc_comms())
        Clock.schedule_once(lambda dt: self._show_startup_view())

    def _show_preferred_view(self):
        settings_to_view = {'Home Page':'home',
                            'Dashboard':'dash',
                            'Analysis': 'analysis',
                            'Setup': 'config' }
        view_pref = self.settings.userPrefs.get_pref('preferences', 'startup_screen')
        self._show_main_view(settings_to_view.get(view_pref, 'home'))

    def _show_startup_view(self):
        # should we show the stetup wizard?
        setup_enabled = self.settings.userPrefs.get_pref_bool('setup', 'setup_enabled')
        if setup_enabled:
            setup_view = self._get_main_screen('setup')
            self._show_main_view('setup')
        else:
            self._show_preferred_view()

    def init_rc_comms(self):
        port = self.getAppArg('port')
        conn_type = self.settings.userPrefs.get_pref('preferences', 'conn_type', default=None)

        cli_conn_type = self.getAppArg('conn_type')

        if cli_conn_type:
            conn_type = cli_conn_type

        Logger.info("RaceCaptureApp: initializing rc comms with, conn type: {}".format(conn_type))

        comms = comms_factory(port, conn_type)
        rc_api = self._rc_api
        rc_api.detect_win_callback = self.rc_detect_win
        rc_api.detect_fail_callback = self.rc_detect_fail
        rc_api.detect_activity_callback = self.rc_detect_activity
        rc_api.init_api(comms)
        rc_api.run_auto_detect()

    def rc_detect_win(self, version):
        if version.is_compatible_version():
            version_string = version.git_info if version.git_info is not '' else 'v' + version.version_string()
            self.showStatus("{} {}".format(version.friendlyName, version_string), False)
            self._data_bus_pump.start(self._databus, self._rc_api, self._session_recorder, self._rc_api.comms.supports_streaming)
            self._status_pump.start(self._rc_api)
            self._telemetry_connection.data_connected = True

            if self.rc_config.loaded == False:
                Clock.schedule_once(lambda dt: self.on_read_config(self))
            else:
                self.showActivity('Connected')
        else:
            alertPopup('Incompatible Firmware', 'Detected {} v{}\n\nPlease upgrade firmware to {} or higher'.format(
                               version.friendlyName,
                               version.version_string(),
                               VersionConfig.get_minimum_version().version_string()
                               ))

    def rc_detect_fail(self):

        def re_detect():
            if not self._rc_api.comms.isOpen():
                self._rc_api.run_auto_detect()

        self.showStatus("Connecting...", True)
        Clock.schedule_once(lambda dt: re_detect(), 1.0)

    def rc_detect_activity(self, info):
        self.showActivity('Searching {}'.format(info))

    def _on_rcp_disconnect(self):
        if self._telemetry_connection.data_connected:
            self._telemetry_connection.data_connected = False


    def open_settings(self, *largs):
        self.switchMainView('preferences')

    def _setup_toolbar(self):
        status_bar = self.root.ids.status_bar
        status_bar.status_pump = self._status_pump
        status_bar.track_manager = self.track_manager

    def setup_telemetry(self):
        host = self.getAppArg('telemetryhost')

        telemetry_enabled = True if self.settings.userPrefs.get_pref('preferences', 'send_telemetry') == "1" else False

        tc = self._telemetry_connection = TelemetryManager(self._databus, host=host, telemetry_enabled=telemetry_enabled)
        self.config_listeners.append(tc)
        tc.bind(on_connecting=self.telemetry_connecting)
        tc.bind(on_connected=self.telemetry_connected)
        tc.bind(on_disconnected=self.telemetry_disconnected)
        tc.bind(on_streaming=self.telemetry_streaming)
        tc.bind(on_error=self.telemetry_error)
        tc.bind(on_auth_error=self.telemetry_auth_error)
        tc.bind(on_api_msg=self.telemetry_api_msg)

    def telemetry_connecting(self, instance, msg):
        self.status_bar.dispatch('on_tele_status', ToolbarView.TELEMETRY_CONNECTING)
        self.showActivity(msg)

    def telemetry_connected(self, instance, msg):
        self.status_bar.dispatch('on_tele_status', ToolbarView.TELEMETRY_CONNECTING)
        self.showActivity(msg)

    def telemetry_disconnected(self, instance, msg):
        self.status_bar.dispatch('on_tele_status', ToolbarView.TELEMETRY_IDLE)
        self.showActivity(msg)

    def telemetry_streaming(self, instance, msg):
        self.status_bar.dispatch('on_tele_status', ToolbarView.TELEMETRY_ACTIVE)

    def telemetry_auth_error(self, instance, msg):
        self.status_bar.dispatch('on_tele_status', ToolbarView.TELEMETRY_ERROR)
        self.showActivity(msg)

    def telemetry_error(self, instance, msg):
        self.showActivity(msg)
        self.status_bar.dispatch('on_tele_status', ToolbarView.TELEMETRY_ERROR)

    def telemetry_api_msg(self, instance, msg):
        ApiDispatcher.get_instance().dispatch_msg(msg, instance)

    def _on_preference_change(self, instance, section, key, value):
        """Called any time the app preferences are changed
        """
        token = (section, key)

        if token == ('preferences', 'send_telemetry'):
            if value == "1":  # Boolean settings values are 1/0, not True/False
                if self.rc_config.connectivityConfig.cellConfig.cellEnabled:
                    alertPopup('Telemetry error', "Disable the telemetry module before enabling app telemetry.")
                Clock.schedule_once(lambda dt: self._enable_telemetry())
            else:
                Clock.schedule_once(lambda dt: self._disable_telemetry())

        if token == ('preferences', 'conn_type'):
            # User changed their RC connection type
            Logger.info("RaceCaptureApp: RC connection type changed to {}, restarting comms".format(value))
            Clock.schedule_once(lambda dt: self._restart_comms())

    def _enable_telemetry(self):
        self._telemetry_connection.telemetry_enabled = True

    def _disable_telemetry(self):
        self._telemetry_connection.telemetry_enabled = False

    def _restart_comms(self):
        self._data_bus_pump.stop()
        self._status_pump.stop()
        self._rc_api.shutdown_api()
        self.init_rc_comms()

    def _on_session_recording(self, instance, is_recording):
        toast('Session recording started' if is_recording else 'Session recording stopped', length_long=True)
Exemplo n.º 10
0
class AnalysisView(Screen):
    SUGGESTED_CHART_CHANNELS = ['Speed']
    INIT_DATASTORE_TIMEOUT = 10.0
    _settings = None
    _databus = None
    _track_manager = None
    _popup = None
    _color_sequence = ColorSequence()
    sessions = ObjectProperty(None)

    def __init__(self, **kwargs):
        Builder.load_file(ANALYSIS_VIEW_KV)
        super(AnalysisView, self).__init__(**kwargs)
        self._datastore = CachingAnalysisDatastore()
        self.register_event_type('on_tracks_updated')
        self._databus = kwargs.get('dataBus')
        self._settings = kwargs.get('settings')
        self._track_manager = kwargs.get('track_manager')
        self.ids.sessions_view.bind(on_lap_selection=self.lap_selection)
        self.ids.channelvalues.color_sequence = self._color_sequence
        self.ids.mainchart.color_sequence = self._color_sequence
        self.stream_connecting = False
        Window.bind(mouse_pos=self.on_mouse_pos)
        Window.bind(on_motion=self.on_motion)
        self.init_view()

    def on_motion(self, instance, event, motion_event):
        flyin = self.ids.laps_flyin
        if self.collide_point(motion_event.x, motion_event.y):
            if not flyin.flyin_collide_point(motion_event.x, motion_event.y):
                flyin.schedule_hide()

    def on_mouse_pos(self, x, pos):
        flyin = self.ids.laps_flyin
        x = pos[0]
        y = pos[1]
        self_collide = self.collide_point(x, y)
        flyin_collide = flyin.flyin_collide_point(x, y)
        laps_selected = self.ids.sessions_view.selected_count > 0

        if self_collide and not flyin_collide and laps_selected:
            flyin.schedule_hide()
        return False

    def on_sessions(self, instance, value):
        self.ids.channelvalues.sessions = value

    def lap_selection(self, instance, source_ref, selected):
        source_key = str(source_ref)
        if selected:
            self.ids.mainchart.add_lap(source_ref)
            self.ids.channelvalues.add_lap(source_ref)
            map_path_color = self._color_sequence.get_color(source_key)
            self.ids.analysismap.add_reference_mark(source_key, map_path_color)
            self._sync_analysis_map(source_ref.session)
            self._datastore.get_location_data(
                source_ref, lambda x: self.ids.analysismap.add_map_path(
                    source_ref, x, map_path_color))

        else:
            self.ids.mainchart.remove_lap(source_ref)
            self.ids.channelvalues.remove_lap(source_ref)
            self.ids.analysismap.remove_reference_mark(source_key)
            self.ids.analysismap.remove_map_path(source_ref)

    def on_tracks_updated(self, track_manager):
        self.ids.analysismap.track_manager = track_manager

    def on_channel_selected(self, instance, value):
        self.ids.channelvalues.merge_selected_channels(value)

    def on_marker(self, instance, marker):
        source = marker.sourceref
        cache = self._datastore.get_location_data(source)
        if cache != None:
            try:
                point = cache[marker.data_index]
            except IndexError:
                point = cache[len(cache) - 1]
            self.ids.analysismap.update_reference_mark(source, point)
            self.ids.channelvalues.update_reference_mark(
                source, marker.data_index)

    def _sync_analysis_map(self, session):
        analysis_map = self.ids.analysismap
        current_track = analysis_map.track

        lat_avg, lon_avg = self._datastore.get_location_center([session])
        new_track = analysis_map.select_map(lat_avg, lon_avg)

        if current_track != new_track:
            #if a new track is selected, then
            #unselect all laps for all other sessions
            sessions_view = self.ids.sessions_view
            sessions_view.deselect_other_laps(session)

    def open_datastore(self):
        pass

    def on_add_stream(self, *args):
        self.show_add_stream_dialog()

    def on_stream_connected(self, instance, new_session_id):
        self.stream_connecting = False
        self._dismiss_popup()
        self.ids.sessions_view.refresh_session_list()
        self.check_load_suggested_lap(new_session_id)

    # The following selects a best lap if there are no other laps currently selected
    def check_load_suggested_lap(self, new_session_id):
        sessions_view = self.ids.sessions_view
        if len(sessions_view.selected_laps) == 0:
            best_lap = self._datastore.get_channel_min('LapTime',
                                                       [new_session_id],
                                                       ['LapCount'])
            if best_lap:
                best_lap_id = best_lap[1]
                Logger.info(
                    'AnalysisView: Convenience selected a suggested session {} / lap {}'
                    .format(new_session_id, best_lap_id))
                main_chart = self.ids.mainchart
                main_chart.select_channels(
                    AnalysisView.SUGGESTED_CHART_CHANNELS)
                self.ids.channelvalues.select_channels(
                    AnalysisView.SUGGESTED_CHART_CHANNELS)
                sessions_view.select_lap(new_session_id, best_lap_id, True)
                HelpInfo.help_popup('suggested_lap',
                                    main_chart,
                                    arrow_pos='left_mid')
            else:
                Logger.warn(
                    'AnalysisView: Could not determine best lap for session {}'
                    .format(new_session_id))

    def on_stream_connecting(self, *args):
        self.stream_connecting = True

    def show_add_stream_dialog(self):
        self.stream_connecting = False
        content = AddStreamView(settings=self._settings,
                                datastore=self._datastore)
        content.bind(on_connect_stream_start=self.on_stream_connecting)
        content.bind(on_connect_stream_complete=self.on_stream_connected)

        popup = Popup(title="Add Telemetry Stream",
                      content=content,
                      size_hint=(0.7, 0.7))
        popup.bind(on_dismiss=self.popup_dismissed)
        popup.open()
        self._popup = popup

    def init_datastore(self):
        def _init_datastore(dstore_path):
            if os.path.isfile(dstore_path):
                self._datastore.open_db(dstore_path)
            else:
                Logger.info('AnalysisView: creating datastore...')
                self._datastore.new(dstore_path)
            self.ids.sessions_view.datastore = self._datastore

        dstore_path = self._settings.userPrefs.datastore_location
        Logger.info("AnalysisView: Datastore Path:" + str(dstore_path))
        t = Thread(target=_init_datastore, args=(dstore_path, ))
        t.daemon = True
        t.start()

    def init_view(self):
        self.init_datastore()
        mainchart = self.ids.mainchart
        mainchart.settings = self._settings
        mainchart.datastore = self._datastore
        channelvalues = self.ids.channelvalues
        channelvalues.datastore = self._datastore
        channelvalues.settings = self._settings
        self.ids.analysismap.track_manager = self._track_manager
        self.ids.analysismap.datastore = self._datastore
        Clock.schedule_once(
            lambda dt: HelpInfo.help_popup(
                'beta_analysis_welcome', self, arrow_pos='right_mid'), 0.5)

    def popup_dismissed(self, *args):
        if self.stream_connecting:
            return True
        self._popup = None

    def _dismiss_popup(self, *args):
        if self._popup is not None:
            self._popup.dismiss()
            self._popup = None