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