Example #1
0
    def __init__(self, **kwargs):
        super(RaceCaptureApp, self).__init__(**kwargs)

        # 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._keyboard = Window.request_keyboard(self._keyboard_closed, self)
        #self._keyboard.bind(on_key_down=self._on_keyboard_down)
        self.settings = SystemSettings(self.user_data_dir,
                                       base_dir=self.base_dir)
        self._databus = DataBusFactory().create_standard_databus(
            self.settings.systemChannels)
        self.settings.runtimeChannels.data_bus = self._databus

        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.trackManager = TrackManager(
            user_dir=self.settings.get_default_data_dir(),
            base_dir=self.base_dir)
        self.setup_telemetry()
Example #2
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()
Example #3
0
    def __init__(self, **kwargs):
        super(RaceCaptureApp, self).__init__(**kwargs)

        # 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__))

        #RaceCapture serial I/O
        self._rc_api = RcpApi(on_disconnect=self._on_rcp_disconnect)

        #self._keyboard = Window.request_keyboard(self._keyboard_closed, self)
        #self._keyboard.bind(on_key_down=self._on_keyboard_down)
        self.settings = SystemSettings(self.user_data_dir, base_dir=self.base_dir)
        self._databus = DataBusFactory().create_standard_databus(self.settings.systemChannels)
        self.settings.runtimeChannels.data_bus = self._databus
        HelpInfo.settings = self.settings

        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.trackManager = TrackManager(user_dir=self.settings.get_default_data_dir(), base_dir=self.base_dir)
        self.setup_telemetry()
Example #4
0
    def __init__(self, **kwargs):
        super(RaceCaptureApp, self).__init__(**kwargs)

        if kivy.platform == 'ios' or kivy.platform == 'macosx':
            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._databus = DataBusFactory().create_standard_databus(self.settings.systemChannels)
        self.settings.runtimeChannels.data_bus = self._databus

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

        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.trackManager = TrackManager(user_dir=self.settings.get_default_data_dir(), base_dir=self.base_dir)
        self.setup_telemetry()
Example #5
0
    def __init__(self, **kwargs):
        super(RaceCaptureApp, self).__init__(**kwargs)
        #self._keyboard = Window.request_keyboard(self._keyboard_closed, self)
        #self._keyboard.bind(on_key_down=self._on_keyboard_down)
        self.settings = SystemSettings(self.user_data_dir)
        self._data_bus = DataBusFactory().create_standard_databus(
            self.settings.systemChannels)

        Window.bind(on_key_down=self._on_keyboard_down)
        self.register_event_type('on_tracks_updated')
        self.processArgs()
        self.settings.appConfig.setUserDir(self.user_data_dir)
        self.trackManager = TrackManager(user_dir=self.user_data_dir)
Example #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
    trackManager = 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__

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

        if kivy.platform == 'ios' or kivy.platform == 'macosx':
            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.trackManager = TrackManager(
            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 = DataStore(databus=self._databus)
        self._session_recorder = SessionRecorder(self._datastore,
                                                 self._databus, self._rc_api,
                                                 self.settings,
                                                 self.trackManager)
        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, keycode, *args):
        if keycode == 27:
            self.switchMainView('home')

    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 first_time_setup(self):
        self.settings.userPrefs.set_pref('preferences', 'first_time_setup',
                                         False)

    def loadCurrentTracksSuccess(self):
        Logger.info('RaceCaptureApp: Current Tracks Loaded')
        Clock.schedule_once(lambda dt: self.notifyTracksUpdated())

    def loadCurrentTracksError(self, details):
        alertPopup('Error Loading Tracks', str(details))

    def init_data(self):
        self.trackManager.init(None, self.loadCurrentTracksSuccess,
                               self.loadCurrentTracksError)
        self._init_datastore()

    def _init_datastore(self):
        def _init_datastore(dstore_path):
            if os.path.isfile(dstore_path):
                self._datastore.open_db(dstore_path)
            else:
                Logger.info('Main: creating datastore...')
                self._datastore.new(dstore_path)

        dstore_path = self.settings.userPrefs.datastore_location
        Logger.info("Main: 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")
        Logger.error("Main: Error reading configuration: {}".format(
            str(detail)))

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

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

    def on_main_menu_item(self, instance, value):
        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 on_stop(self):
        self._status_pump.stop()
        self._data_bus_pump.stop()
        self._rc_api.shutdown_api()
        self._telemetry_connection.telemetry_enabled = False

    def showMainView(self, view_name):
        view = self.mainViews.get(view_name)
        if not view:
            view = self.view_builders[view_name]()
            self.screenMgr.add_widget(view)
            self.mainViews[view_name] = view
        self.screenMgr.current = view_name
        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.showMainView(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.trackManager,
                                 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.trackManager,
                                 self._status_pump,
                                 name='status')
        self.tracks_listeners.append(status_view)
        return status_view

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

    def build_analysis_view(self):
        analysis_view = AnalysisView(name='analysis',
                                     data_bus=self._databus,
                                     settings=self.settings,
                                     track_manager=self.trackManager)
        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.settings_view.bind(
            on_config_change=self._on_preferences_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 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
        }

    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()

        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())
        self.check_first_time_setup()

    def check_first_time_setup(self):
        if self.settings.userPrefs.get_pref('preferences',
                                            'first_time_setup') == 'True':
            Clock.schedule_once(lambda dt: self.first_time_setup(), 0.5)

    def show_startup_view(self):
        settings_to_view = {
            'Home Page': 'home',
            'Dashboard': 'dash',
            'Analysis': 'analysis',
            'Configuration': 'config'
        }
        view_pref = self.settings.userPrefs.get_pref('preferences',
                                                     'startup_screen')
        self.showMainView(settings_to_view[view_pref])

    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.trackManager

    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_preferences_change(self, menu, config, 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',
                        "Turn off RaceCapture's telemetry module for app to stream 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)
Example #7
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()

    #RaceCapture serial I/O
    _rc_api = RcpApi()

    #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
    dataBusPump = DataBusPump()

    _status_pump = StatusPump()
    
    #Track database manager
    trackManager = 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

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

        # 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._keyboard = Window.request_keyboard(self._keyboard_closed, self)
        #self._keyboard.bind(on_key_down=self._on_keyboard_down)
        self.settings = SystemSettings(self.user_data_dir, base_dir=self.base_dir)
        self._databus = DataBusFactory().create_standard_databus(self.settings.systemChannels)
        self.settings.runtimeChannels.data_bus = self._databus        

        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.trackManager = TrackManager(user_dir=self.settings.get_default_data_dir(), base_dir=self.base_dir)
        self.setup_telemetry()

    def on_pause(self):
        return True
    
    def _on_keyboard(self, keyboard, keycode, *args):
        if keycode == 27:
            self.switchMainView('home')

    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)

        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 first_time_setup(self):
        popup = None 
        def _on_answer(instance, answer):
            popup.dismiss()
            if answer:
                self.showMainView('tracks')
                Clock.schedule_once(lambda dt: self.mainViews['tracks'].check_for_update(), 0.5)
        popup = confirmPopup('Race Tracks', 'Looks like this is your first time running.\n\nShould I update the Race Track database?', _on_answer)
        self.settings.userPrefs.set_pref('preferences', 'first_time_setup', False)
        
    def loadCurrentTracksSuccess(self):
        Logger.info('RaceCaptureApp: Current Tracks Loaded')
        Clock.schedule_once(lambda dt: self.notifyTracksUpdated())            

    def loadCurrentTracksError(self, details):
        alertPopup('Error Loading Tracks', str(details))

    def init_data(self):
        self.trackManager.init(None, self.loadCurrentTracksSuccess, self.loadCurrentTracksError)

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

    #Logfile
    def on_poll_logfile(self, instance):
        try:
            self._rc_api.getLogfile()
        except:
            pass


    def on_set_logfile_level(self, instance, level):
        try:
            self._rc_api.setLogfileLevel(level, None, self.on_set_logfile_level_error)
        except:
            logging.exception('')
            self._serial_warning()

    def on_set_logfile_level_error(self, detail):
        alertPopup('Error', 'Error Setting Logfile Level:\n\n' + str(detail))

    #Run Script
    def on_run_script(self, instance):
        self._rc_api.runScript(self.on_run_script_complete, self.on_run_script_error)

    def on_run_script_complete(self, result):
        Logger.info('RaceCaptureApp: run script complete: ' + str(result))

    def on_run_script_error(self, detail):
        alertPopup('Error Running', 'Error Running Script:\n\n' + str(detail))

    #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.dataBusPump.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))
        Clock.schedule_once(lambda dt: self.showActivity(''), 5.0)

    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
        self.showActivity('')

    def on_read_config_error(self, detail):
        alertPopup('Error Reading', 'Could not read configuration:\n\n' + str(detail))

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

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

    def on_main_menu_item(self, instance, value):
        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 on_stop(self):
        self._rc_api.cleanup_comms()
        self._telemetry_connection.telemetry_enabled = False

    def showMainView(self, view_name):
        try:
            view = self.mainViews.get(view_name)
            if not view:
                view = self.view_builders[view_name]()
                self.screenMgr.add_widget(view)
                self.mainViews[view_name] = view
            self.screenMgr.current = view_name
        except Exception as detail:
            Logger.info('RaceCaptureApp: Failed to load main view ' + str(view_name) + ' ' + str(detail))

    def switchMainView(self, view_name):
            self.mainNav.anim_to_state('closed')
            Clock.schedule_once(lambda dt: self.showMainView(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.trackManager)
        config_view.bind(on_read_config=self.on_read_config)
        config_view.bind(on_write_config=self.on_write_config)
        config_view.bind(on_run_script=self.on_run_script)
        config_view.bind(on_poll_logfile=self.on_poll_logfile)
        config_view.bind(on_set_logfile_level=self.on_set_logfile_level)
        self._rc_api.addListener('logfile', lambda value: Clock.schedule_once(lambda dt: config_view.on_logfile(value)))
        self.config_listeners.append(config_view)
        self.tracks_listeners.append(config_view)
        return config_view
    
    def build_status_view(self):
        status_view = StatusView(self.trackManager, self._status_pump, name='status')
        self.tracks_listeners.append(status_view)
        return status_view
    
    def build_tracks_view(self):
        tracks_view = TracksView(name='tracks', track_manager=self.trackManager)
        self.tracks_listeners.append(tracks_view)
        return tracks_view
    
    def build_dash_view(self):
        dash_view = DashboardView(name='dash', dataBus=self._databus, settings=self.settings)
        self.tracks_listeners.append(dash_view)
        return dash_view
    
    def build_analysis_view(self):
        analysis_view = AnalysisView(name='analysis', data_bus=self._databus, settings=self.settings)
        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.settings_view.bind(on_config_change=self._on_preferences_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 init_view_builders(self):
        self.view_builders = {'config': self.build_config_view,
                              'tracks': self.build_tracks_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
                              }
        
    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_rc_rx', value)
        rc_api.on_tx = lambda value: status_bar.dispatch('on_rc_tx', value)

        screenMgr = root.ids.main
        #NoTransition
        #SlideTransition
        #SwapTransition
        #FadeTransition
        #WipeTransition
        #FallOutTransition
        #RiseInTransition
        screenMgr.transition=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.showMainView('home')
        Clock.schedule_once(lambda dt: self.init_data())
        Clock.schedule_once(lambda dt: self.init_rc_comms())
        self.check_first_time_setup()
        
    def check_first_time_setup(self):
        if self.settings.userPrefs.get_pref('preferences', 'first_time_setup') == 'True':
            Clock.schedule_once(lambda dt: self.first_time_setup(), 0.5)

    def init_rc_comms(self):
        port = self.getAppArg('port')
        comms = comms_factory(port)
        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_comms(comms)
        rc_api.run_auto_detect()

    def rc_detect_win(self, version):
        if version.is_compatible_version():
            self.showStatus("{} v{}.{}.{}".format(version.friendlyName, version.major, version.minor, version.bugfix), False)
            self.dataBusPump.startDataPump(self._databus, self._rc_api)
            self._status_pump.start(self._rc_api)        
            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("Could not detect RaceCapture/Pro", True)
        Clock.schedule_once(lambda dt: re_detect(), 1.0)

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

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

    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_preferences_change(self, menu, config, 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', "Turn off RaceCapture's telemetry module for app to stream telemetry.")
                self._telemetry_connection.telemetry_enabled = True
            else:
                self._telemetry_connection.telemetry_enabled = False
Example #8
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)
Example #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()

    #RaceCapture serial I/O
    _rc_api = RcpApi()

    #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
    dataBusPump = DataBusPump()

    _status_pump = StatusPump()

    #Track database manager
    trackManager = 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

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

        # 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._keyboard = Window.request_keyboard(self._keyboard_closed, self)
        #self._keyboard.bind(on_key_down=self._on_keyboard_down)
        self.settings = SystemSettings(self.user_data_dir,
                                       base_dir=self.base_dir)
        self._databus = DataBusFactory().create_standard_databus(
            self.settings.systemChannels)
        self.settings.runtimeChannels.data_bus = self._databus

        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.trackManager = TrackManager(
            user_dir=self.settings.get_default_data_dir(),
            base_dir=self.base_dir)
        self.setup_telemetry()

    def on_pause(self):
        return True

    def _on_keyboard(self, keyboard, keycode, *args):
        if keycode == 27:
            self.switchMainView('home')

    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)

        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 first_time_setup(self):
        popup = None

        def _on_answer(instance, answer):
            popup.dismiss()
            if answer:
                self.showMainView('tracks')
                Clock.schedule_once(
                    lambda dt: self.mainViews['tracks'].check_for_update(),
                    0.5)

        popup = confirmPopup(
            'Race Tracks',
            'Looks like this is your first time running.\n\nShould I update the Race Track database?',
            _on_answer)
        self.settings.userPrefs.set_pref('preferences', 'first_time_setup',
                                         False)

    def loadCurrentTracksSuccess(self):
        Logger.info('RaceCaptureApp: Current Tracks Loaded')
        Clock.schedule_once(lambda dt: self.notifyTracksUpdated())

    def loadCurrentTracksError(self, details):
        alertPopup('Error Loading Tracks', str(details))

    def init_data(self):
        self.trackManager.init(None, self.loadCurrentTracksSuccess,
                               self.loadCurrentTracksError)

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

    #Logfile
    def on_poll_logfile(self, instance):
        try:
            self._rc_api.getLogfile()
        except:
            pass

    def on_set_logfile_level(self, instance, level):
        try:
            self._rc_api.setLogfileLevel(level, None,
                                         self.on_set_logfile_level_error)
        except:
            logging.exception('')
            self._serial_warning()

    def on_set_logfile_level_error(self, detail):
        alertPopup('Error', 'Error Setting Logfile Level:\n\n' + str(detail))

    #Run Script
    def on_run_script(self, instance):
        self._rc_api.runScript(self.on_run_script_complete,
                               self.on_run_script_error)

    def on_run_script_complete(self, result):
        Logger.info('RaceCaptureApp: run script complete: ' + str(result))

    def on_run_script_error(self, detail):
        alertPopup('Error Running', 'Error Running Script:\n\n' + str(detail))

    #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.dataBusPump.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))
        Clock.schedule_once(lambda dt: self.showActivity(''), 5.0)

    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
        self.showActivity('')

    def on_read_config_error(self, detail):
        alertPopup('Error Reading',
                   'Could not read configuration:\n\n' + str(detail))

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

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

    def on_main_menu_item(self, instance, value):
        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 on_stop(self):
        self._rc_api.cleanup_comms()
        self._telemetry_connection.telemetry_enabled = False

    def showMainView(self, view_name):
        try:
            view = self.mainViews.get(view_name)
            if not view:
                view = self.view_builders[view_name]()
                self.screenMgr.add_widget(view)
                self.mainViews[view_name] = view
            self.screenMgr.current = view_name
        except Exception as detail:
            Logger.info('RaceCaptureApp: Failed to load main view ' +
                        str(view_name) + ' ' + str(detail))

    def switchMainView(self, view_name):
        self.mainNav.anim_to_state('closed')
        Clock.schedule_once(lambda dt: self.showMainView(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.trackManager)
        config_view.bind(on_read_config=self.on_read_config)
        config_view.bind(on_write_config=self.on_write_config)
        config_view.bind(on_run_script=self.on_run_script)
        config_view.bind(on_poll_logfile=self.on_poll_logfile)
        config_view.bind(on_set_logfile_level=self.on_set_logfile_level)
        self._rc_api.addListener(
            'logfile', lambda value: Clock.schedule_once(lambda dt: config_view
                                                         .on_logfile(value)))
        self.config_listeners.append(config_view)
        self.tracks_listeners.append(config_view)
        return config_view

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

    def build_tracks_view(self):
        tracks_view = TracksView(name='tracks',
                                 track_manager=self.trackManager)
        self.tracks_listeners.append(tracks_view)
        return tracks_view

    def build_dash_view(self):
        dash_view = DashboardView(name='dash',
                                  dataBus=self._databus,
                                  settings=self.settings)
        self.tracks_listeners.append(dash_view)
        return dash_view

    def build_analysis_view(self):
        analysis_view = AnalysisView(name='analysis',
                                     data_bus=self._databus,
                                     settings=self.settings)
        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.settings_view.bind(
            on_config_change=self._on_preferences_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 init_view_builders(self):
        self.view_builders = {
            'config': self.build_config_view,
            'tracks': self.build_tracks_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
        }

    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_rc_rx', value)
        rc_api.on_tx = lambda value: status_bar.dispatch('on_rc_tx', value)

        screenMgr = root.ids.main
        #NoTransition
        #SlideTransition
        #SwapTransition
        #FadeTransition
        #WipeTransition
        #FallOutTransition
        #RiseInTransition
        screenMgr.transition = 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.showMainView('home')
        Clock.schedule_once(lambda dt: self.init_data())
        Clock.schedule_once(lambda dt: self.init_rc_comms())
        self.check_first_time_setup()

    def check_first_time_setup(self):
        if self.settings.userPrefs.get_pref('preferences',
                                            'first_time_setup') == 'True':
            Clock.schedule_once(lambda dt: self.first_time_setup(), 0.5)

    def init_rc_comms(self):
        port = self.getAppArg('port')
        comms = comms_factory(port)
        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_comms(comms)
        rc_api.run_auto_detect()

    def rc_detect_win(self, version):
        if version.is_compatible_version():
            self.showStatus(
                "{} v{}.{}.{}".format(version.friendlyName, version.major,
                                      version.minor, version.bugfix), False)
            self.dataBusPump.startDataPump(self._databus, self._rc_api)
            self._status_pump.start(self._rc_api)
            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("Could not detect RaceCapture/Pro", True)
        Clock.schedule_once(lambda dt: re_detect(), 1.0)

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

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

    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_preferences_change(self, menu, config, 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',
                        "Turn off RaceCapture's telemetry module for app to stream telemetry."
                    )
                self._telemetry_connection.telemetry_enabled = True
            else:
                self._telemetry_connection.telemetry_enabled = False