537 lines
		
	
	
		
			18 KiB
		
	
	
	
		
			Python
		
	
	
	
			
		
		
	
	
			537 lines
		
	
	
		
			18 KiB
		
	
	
	
		
			Python
		
	
	
	
| """
 | |
| Tracker mit MQTT client
 | |
| 
 | |
| Es gibt zwei Modi: Track und Race
 | |
|   A) Track: Es wird nur der Track gesended / aufgezeichnet
 | |
|   B) Race: Es werden zusätzlich Regattadaten angezeigt, das beinhaltet
 | |
|     die Auswahl einer Regatta, das Teilnehmen, das Aufgeben und die
 | |
|     Signalisierung vor und während einer Wettfahrt.
 | |
| 
 | |
|   Das Tracking kann über eine Taste ein- und ausgeschaltet werden.
 | |
|   Um versehentliches Umschalten zu vermeiden ist eine Nachfrage
 | |
|   integriert.
 | |
| 
 | |
|  - Momentan wird nur Regatta Hero unterstützt
 | |
| 
 | |
| Behandlung von Verbindungsabbrüchen:
 | |
|   - on_disconnect
 | |
| 
 | |
| TODO
 | |
|     
 | |
| 
 | |
| """
 | |
| 
 | |
| import os
 | |
| import cairo
 | |
| from .page import Page
 | |
| 
 | |
| class RaceTracker(Page):
 | |
| 
 | |
|     def __init__(self, pageno, cfg, appdata, boatdata):
 | |
|         super().__init__(pageno, cfg, appdata, boatdata)
 | |
|         self.ttype = self.app.track.ttype
 | |
|         self.bv_lat = boatdata.getRef("LAT")
 | |
|         self.bv_lon = boatdata.getRef("LON")
 | |
|         self.bv_sog = boatdata.getRef("SOG")
 | |
|         self.bv_hdop = boatdata.getRef("HDOP")
 | |
|         self.races = None
 | |
|         self.raceid = None           
 | |
|         if self.ttype == 'HERO':
 | |
|             self.raceid = self.app.track.hero_raceid # Ausgewählte Regatta
 | |
|             self.buttonlabel[1] = 'MODE'
 | |
|             self.buttonlabel[2] = 'INFO'
 | |
|             self.buttonlabel[5] = 'ABORT'
 | |
|         elif self.ttype in ('LOCAL','SDCARD'):
 | |
|             self.buttonlabel[5] = 'ON'
 | |
|         self.menupos = 0
 | |
|         self.mode = 'N' # (N)ormal, (C)onfiguration, (M)itteilung
 | |
|         self.query_active = False
 | |
|         self.savebuttons = None
 | |
| 
 | |
|         # Flaggengröße: 96 x 64 Pixel
 | |
|         self.flagpos = ((208, 140), (308, 140), (208, 210), (308, 210))
 | |
| 
 | |
|         # Flaggen laden
 | |
|         flag = ('alpha', 'answer', 'black', 'blue', 'charlie', 'class',
 | |
|                 'finish', 'foxtrot', 'hotel', 'india', 'november',
 | |
|                 'orange', 'papa', 'repeat_one', 'sierra', 'start',
 | |
|                 'uniform', 'xray', 'yankee', 'zulu')
 | |
|         # Mapping
 | |
|         self.flagmap = {
 | |
|               3: 'blue',        # Zielflagge
 | |
|               7: 'xray',        # Einzelrückruf
 | |
|               8: 'sierra',      # Bahnverkürzung
 | |
|               9: 'november',    # Abbruch
 | |
|              10: 'yankee',      # Schwimmwesten
 | |
|              11: 'repeat_one',  # Allgemeiner Rückruf
 | |
|              12: 'answer',      # Startverschiebung
 | |
|              14: 'start',       # Startflagge
 | |
|              15: 'class',       # Klassenflagge
 | |
|              18: 'alpha',
 | |
|              19: 'hotel',
 | |
|              20: 'charlie',
 | |
|             100: 'papa',        # Vorbereitung, Frühstart: Zurückfallen über Startlinie
 | |
|             101: 'india',       # Vorbereitung, Frühstart: Starttonne umrunden
 | |
|             102: 'zulu',        # Frühstart: 20%-Strafe
 | |
|             103: 'uniform',     # Frühstart: Disqualifikation, Wiederholung erlaubt
 | |
|             104: 'black'        # Frühstart: Disqualifikation, Wiederholung nicht erlaubt
 | |
|         }
 | |
|         self.sym_flag = {}
 | |
|         for f in flag:
 | |
|             flagfile = os.path.join(cfg['imgpath'], 'flags', f + '.png')
 | |
|             self.sym_flag[f] = cairo.ImageSurface.create_from_png(flagfile)
 | |
| 
 | |
|     def handle_key(self, buttonid):
 | |
|         if self.ttype == 'NONE':
 | |
|             return False
 | |
|         if self.query_active:
 | |
|             if buttonid == 2:
 | |
|                 self.query_active = False
 | |
|                 self.app.track.hero_giveup()
 | |
|                 self.buttonlabel = self.savebuttons.copy()
 | |
|             elif buttonid == 5:
 | |
|                 self.query_active = False
 | |
|                 self.buttonlabel = self.savebuttons.copy()
 | |
|             return True
 | |
|         if buttonid == 1:
 | |
|             if not self.ttype == 'HERO':
 | |
|                 return False
 | |
|             # Modus umschalten
 | |
|             if self.mode == 'N':
 | |
|                 self.mode = 'C'
 | |
|                 self.buttonlabel[2] = '#UP'
 | |
|                 self.buttonlabel[3] = '#DOWN'
 | |
|                 self.buttonlabel[4] = 'SET'
 | |
|                 if self.app.track.is_active():
 | |
|                     self.buttonlabel[5] = 'OFF'
 | |
|                 else:
 | |
|                     self.buttonlabel[5] = 'ON'
 | |
|             else:
 | |
|                 self.mode = 'N'
 | |
|                 self.buttonlabel[2] = 'INFO'
 | |
|                 self.buttonlabel[3] = '#PREV'
 | |
|                 self.buttonlabel[4] = '#NEXT'
 | |
|                 self.buttonlabel[5] = 'ABORT'
 | |
|             return True
 | |
|         elif buttonid == 2:
 | |
|             if not self.ttype == 'HERO':
 | |
|                 return False
 | |
|             if self.mode == 'N':
 | |
|                 self.mode = 'M' # Nachrichten der Wettfahrtleitung
 | |
|                 self.buttonlabel[2] = ''
 | |
|                 self.buttonlabel[5] = ''
 | |
|                 return True
 | |
|             if self.mode == 'C':
 | |
|                 # Up
 | |
|                 if self.menupos > 1:
 | |
|                     self.menupos -= 1
 | |
|                 else:
 | |
|                     self.menupos = len(self.races)
 | |
|                 return True
 | |
|         elif buttonid == 3:
 | |
|             if self.mode == 'C':
 | |
|                 # Down
 | |
|                 if self.menupos < len(self.races):
 | |
|                     self.menupos += 1
 | |
|                 else:
 | |
|                     self.menupos = 1
 | |
|                 return True
 | |
|         elif buttonid == 4:
 | |
|             if self.mode == 'C':
 | |
|                 # Set / Select regatta
 | |
|                 # TODO Nur möglich wenn nicht in einer anderen Wettfahrt gerade aktiv
 | |
|                 if self.menupos > 0:
 | |
|                     self.raceid = self.races[self.menupos - 1] # Nullbasiert
 | |
|                     last_id = self.app.track.hero_raceid
 | |
|                     self.app.track.hero_raceid = self.raceid
 | |
|                     self.app.track.hero_mqtt_subscribe(self.raceid, last_id)
 | |
|                     self.app.log.info(f"Selected race '{self.raceid}'")
 | |
|                 return True
 | |
|         elif buttonid == 5:
 | |
|             if self.mode == 'N':
 | |
|                 if self.ttype in ('LOCAL','SDCARD'):
 | |
|                     # Tracking ein/-ausschalten
 | |
|                     if self.app.track.is_active():
 | |
|                         self.app.track.set_active(False)
 | |
|                         self.buttonlabel[5] = 'ON'
 | |
|                     else:
 | |
|                         self.app.track.set_active(True)
 | |
|                         self.buttonlabel[5] = 'OFF'
 | |
|                 else:
 | |
|                     if self.ttype == ('HERO'):
 | |
|                         # Rennabbruch verarbeiten
 | |
|                         self.query_active = True;
 | |
|                         self.savebuttons = self.buttonlabel.copy()
 | |
|                         self.buttonlabel[1] = ""
 | |
|                         self.buttonlabel[2] = "YES"
 | |
|                         self.buttonlabel[3] = ""
 | |
|                         self.buttonlabel[4] = ""
 | |
|                         self.buttonlabel[5] = "NO"
 | |
|                         self.buttonlabel[6] = ""
 | |
|                         # Taste 2 = JA, Taste 5 = NEIN
 | |
|             elif self.mode == 'C':
 | |
|                 # Tracking ein/-ausschalten
 | |
|                 if self.app.track.is_active():
 | |
|                     self.app.track.set_active(False)
 | |
|                     self.buttonlabel[5] = 'ON'
 | |
|                 else:
 | |
|                     self.app.track.set_active(True)
 | |
|                     self.buttonlabel[5] = 'OFF'
 | |
|             elif self.mode == 'M':
 | |
|                 self.app.frontend.flashled.setColor('yellow')
 | |
|                 #self.app.frontend.flashled.switchOn(4)
 | |
|                 self.app.frontend.flashled.doFlash(2)
 | |
|             return True
 | |
|         return False
 | |
| 
 | |
|     def draw_none(self, ctx):
 | |
|         """
 | |
|         TODO Funktion schreiben, die einen längeren Text mit
 | |
|         Absätzen auf dem Bildschirma ausgibt.
 | |
|         """
 | |
|         x = 8
 | |
|         y = 48
 | |
|         ctx.select_font_face("Ubuntu", cairo.FontSlant.NORMAL, cairo.FontWeight.BOLD)
 | |
|         ctx.set_font_size(24)
 | |
|         ctx.move_to(x, y)
 | |
|         ctx.show_text("Tracker")
 | |
| 
 | |
|         y += 25
 | |
|         ctx.set_font_size(16)
 | |
|         ctx.move_to(x, y)
 | |
|         ctx.show_text("Disabled by 'NONE' in configuration.")
 | |
|         y += 30
 | |
|         ctx.move_to(x, y)
 | |
|         ctx.show_text("Currently only tracker types 'HERO' and 'LOCAL'")
 | |
|         y += 20
 | |
|         ctx.move_to(x, y)
 | |
|         ctx.show_text("are implemented.")
 | |
|         y += 30
 | |
|         ctx.move_to(x, y)
 | |
|         ctx.show_text("'LOCAL' tracks positions in file system.")
 | |
|         y += 30
 | |
|         ctx.move_to(x, y)
 | |
|         ctx.show_text("For 'HERO' pleast visit the Regatta Hero")
 | |
|         y += 20
 | |
|         ctx.move_to(x, y)
 | |
|         ctx.show_text("web page for detailed description.")
 | |
|         y += 20
 | |
|         ctx.move_to(x, y)
 | |
|         ctx.show_text("Additionally you should contact your local")
 | |
|         y += 20
 | |
|         ctx.move_to(x, y)
 | |
|         ctx.show_text("race officer to get configuration data.")
 | |
| 
 | |
|     def draw_local(self, ctx):
 | |
|         x = 8
 | |
|         x1 = 130
 | |
|         y = 48
 | |
|         ctx.select_font_face("Ubuntu", cairo.FontSlant.NORMAL, cairo.FontWeight.BOLD)
 | |
|         ctx.set_font_size(24)
 | |
|         ctx.move_to(x, y)
 | |
|         ctx.show_text("Local Tracking")
 | |
| 
 | |
|         ctx.select_font_face("Ubuntu", cairo.FontSlant.NORMAL, cairo.FontWeight.BOLD)
 | |
|         ctx.set_font_size(16)
 | |
| 
 | |
|         y += 30
 | |
|         ctx.move_to(x, y)
 | |
|         ctx.show_text("Activated: ")
 | |
|         ctx.show_text("Yes" if self.app.track.is_active() else "No")
 | |
|         y += 20
 | |
|         ctx.move_to(x, y)
 | |
|         ctx.show_text(f"Log interval: {self.app.track.local_dt} seconds")
 | |
|         #ctx.show_text(str(self.app.track.local_dt))
 | |
|         y += 20
 | |
|         ctx.move_to(x, y)
 | |
|         ctx.show_text("Log entries written: ")
 | |
|         ctx.show_text(str(self.app.track.local_lfdno))
 | |
| 
 | |
|         # Anzeige
 | |
|         # - LAT, LON
 | |
|         # - bisher gespeicherte Anzahl Positionen
 | |
|         # - Zeitabstand zwischen den einzelnen Messungen
 | |
|         # - Hinweis wo gespeichert wird
 | |
|         y += 30
 | |
|         ctx.move_to(x, y)
 | |
|         ctx.show_text("Latitude: ")
 | |
|         ctx.move_to(x1, y)
 | |
|         ctx.show_text(self.bv_lat.format())
 | |
|         y += 20
 | |
|         ctx.move_to(x, y)
 | |
|         ctx.show_text("Longitude: ")
 | |
|         ctx.move_to(x1, y)
 | |
|         ctx.show_text(self.bv_lon.format())
 | |
|         y += 20
 | |
|         ctx.move_to(x, y)
 | |
|         ctx.show_text("HDOP: ")
 | |
|         ctx.move_to(x1, y)
 | |
|         ctx.show_text(self.bv_hdop.format() or '---')
 | |
|         y += 20
 | |
|         ctx.move_to(x, y)
 | |
|         ctx.show_text("Speed: ")
 | |
|         ctx.move_to(x1, y)
 | |
|         ctx.show_text(self.bv_sog.format())
 | |
| 
 | |
|     def draw_hero(self, ctx):
 | |
|         """
 | |
|         Regatta Hero Normalansicht
 | |
|         TODO
 | |
|         racephase anzeige zu Debuggingzwecken
 | |
|         """
 | |
| 
 | |
|         ctx.select_font_face("DSEG7 Classic")
 | |
|         ctx.set_font_size(80)
 | |
| 
 | |
|         if self.app.track.is_active():
 | |
|             if self.app.track.hero_racestatus:
 | |
|                 counter = self.app.track.hero_racestatus['time']
 | |
|                 minutes, seconds = divmod(abs(counter), 60)
 | |
|                 if counter < 0:
 | |
|                     ctx.move_to(16, 120)
 | |
|                     ctx.show_text(f"-{minutes:02d}:{seconds:02d}")
 | |
|                 else:
 | |
|                     ctx.move_to(28, 120)
 | |
|                     ctx.show_text(f"{minutes:03d}:{seconds:02d}")
 | |
|             else:
 | |
|                 ctx.move_to(48, 120)
 | |
|                 ctx.show_text("--:--")
 | |
|         else:
 | |
|             ctx.move_to(100, 120)
 | |
|             ctx.show_text("off")
 | |
| 
 | |
|         if self.app.track.hero_timedelta > 5:
 | |
|             ctx.select_font_face("Ubuntu", cairo.FontSlant.NORMAL, cairo.FontWeight.BOLD)
 | |
|             ctx.set_font_size(16)
 | |
|             ctx.move_to(8, 260)
 | |
|             ctx.show_text(f"!!! Time drift of {self.app.track.hero_timedelta} seconds")
 | |
| 
 | |
| 
 | |
|         x0 = 8
 | |
|         x1 = 96
 | |
|         y0 = 150
 | |
|         yoffset = 18
 | |
|         ctx.select_font_face("Ubuntu", cairo.FontSlant.NORMAL, cairo.FontWeight.BOLD)
 | |
|         ctx.set_font_size(16)
 | |
| 
 | |
|         # Debug: Phase anzeigen
 | |
|         ctx.move_to(360, 35)
 | |
|         ctx.show_text(str(self.app.track.hero_racephase))
 | |
| 
 | |
|         ctx.move_to(x0, y0)
 | |
|         ctx.show_text("Type")
 | |
|         ctx.move_to(x1, y0)
 | |
|         ctx.show_text(self.app.track.ttype)
 | |
| 
 | |
|         y0 += yoffset
 | |
|         ctx.move_to(x0, y0)
 | |
|         ctx.show_text("Regatta")
 | |
|         ctx.move_to(x1, y0)
 | |
|         ctx.show_text(self.app.track.hero_raceid or '[not selected]')
 | |
| 
 | |
|         y0 += yoffset
 | |
|         ctx.move_to(x0, y0)
 | |
|         ctx.show_text("Course")
 | |
|         ctx.move_to(x1, y0)
 | |
|         if self.app.track.hero_orgstatus and self.app.track.hero_raceid:
 | |
|             ctx.show_text(self.app.track.hero_orgstatus['races'][self.app.track.hero_raceid]['courseid'])
 | |
|         else:
 | |
|             ctx.show_text('[not selected]')
 | |
| 
 | |
|         y0 += yoffset
 | |
|         ctx.move_to(x0, y0)
 | |
|         ctx.show_text("Latitude")
 | |
|         ctx.move_to(x1, y0)
 | |
|         ctx.show_text(self.bv_lat.format())
 | |
| 
 | |
|         y0 += yoffset
 | |
|         ctx.move_to(x0, y0)
 | |
|         ctx.show_text("Longitude")
 | |
|         ctx.move_to(x1, y0)
 | |
|         ctx.show_text(self.bv_lon.format())
 | |
| 
 | |
|         y0 += yoffset
 | |
|         ctx.move_to(x0, y0)
 | |
|         ctx.show_text("Speed")
 | |
|         ctx.move_to(x1, y0)
 | |
|         ctx.show_text(self.bv_sog.format())
 | |
| 
 | |
|         # Flaggen
 | |
|         if self.app.track.hero_racestatus:
 | |
|             pos = 0
 | |
|             for f in self.app.track.hero_racestatus['flags']:
 | |
|                 if f in self.flagmap:
 | |
|                     # TODO Context save/restore erforderlich?
 | |
|                     ctx.save()
 | |
|                     ctx.set_source_surface(self.sym_flag[self.flagmap[f]], *self.flagpos[pos])
 | |
|                     ctx.paint()
 | |
|                     ctx.restore()
 | |
|                 pos += 1
 | |
|                 
 | |
|     def draw_config(self, ctx):
 | |
| 
 | |
|         ctx.select_font_face("Ubuntu", cairo.FontSlant.NORMAL, cairo.FontWeight.BOLD)
 | |
|         ctx.set_font_size(24)
 | |
|         ctx.move_to(4, 42)
 | |
|         ctx.show_text("Tracker configuration")
 | |
| 
 | |
|         # Linke Spalte mit Daten
 | |
| 
 | |
|         x0 = 8  # Labelspalte
 | |
|         x1 = 88 # Datenspalte
 | |
|         y0 = 70
 | |
|         yoffset = 16
 | |
| 
 | |
|         # Bootsdaten
 | |
|         ctx.set_font_size(20)
 | |
|         ctx.move_to(x0, y0)
 | |
|         ctx.show_text("Boat data")
 | |
| 
 | |
|         y0 += yoffset + 5
 | |
|         ctx.set_font_size(16)
 | |
| 
 | |
|         ctx.move_to(x0, y0)
 | |
|         ctx.show_text("Name")
 | |
|         ctx.move_to(x1, y0)
 | |
|         ctx.show_text(self.cfg['boat']['name'])
 | |
| 
 | |
|         y0 += yoffset
 | |
|         ctx.move_to(x0, y0)
 | |
|         ctx.show_text("Class")
 | |
|         ctx.move_to(x1, y0)
 | |
|         ctx.show_text(self.cfg['boat']['class'])
 | |
| 
 | |
|         y0 += yoffset
 | |
|         ctx.move_to(x0, y0)
 | |
|         ctx.show_text("Handicap")
 | |
|         ctx.move_to(x1, y0)
 | |
|         ctx.show_text(str(self.cfg['boat']['handicap']))
 | |
| 
 | |
|         y0 += yoffset
 | |
|         ctx.move_to(x0, y0)
 | |
|         ctx.show_text("Club")
 | |
|         ctx.move_to(x1, y0)
 | |
|         ctx.show_text(self.cfg['boat']['club'])
 | |
| 
 | |
|         y0 += yoffset
 | |
|         ctx.move_to(x0, y0)
 | |
|         ctx.show_text("Sailno.")
 | |
|         ctx.move_to(x1, y0)
 | |
|         ctx.show_text(self.cfg['boat']['sailno'])
 | |
| 
 | |
|         # Trackerdaten
 | |
|         y0 += yoffset + 10
 | |
|         ctx.set_font_size(20)
 | |
|         ctx.move_to(x0, y0)
 | |
|         ctx.show_text("Tracker info")
 | |
| 
 | |
|         y0 += yoffset + 5
 | |
|         ctx.set_font_size(16)
 | |
| 
 | |
|         ctx.move_to(x0, y0)
 | |
|         ctx.show_text("Type")
 | |
|         ctx.move_to(x1, y0)
 | |
|         ctx.show_text(self.app.track.ttype)
 | |
| 
 | |
|         y0 += yoffset
 | |
|         ctx.move_to(x0, y0)
 | |
|         ctx.show_text("Org.")
 | |
|         ctx.move_to(x1, y0)
 | |
|         if self.app.track.hero_orgstatus:
 | |
|             ctx.show_text(self.app.track.hero_orgstatus['orgname'])
 | |
|         else:
 | |
|             ctx.show_text("n/a")
 | |
| 
 | |
|         y0 += yoffset
 | |
|         ctx.move_to(x0, y0)
 | |
|         ctx.show_text("Status")
 | |
|         ctx.move_to(x1, y0)
 | |
|         if not self.app.track.hero_racestatus:
 | |
|             ctx.show_text("inactive")
 | |
|         else:
 | |
|             #TODO Mehr Details
 | |
|             ctx.show_text("active")
 | |
| 
 | |
|         y0 += yoffset
 | |
|         ctx.move_to(x0, y0)
 | |
|         ctx.show_text("Team")
 | |
|         ctx.move_to(x1, y0)
 | |
|         ctx.show_text(self.app.track.team)
 | |
| 
 | |
|         y0 += yoffset
 | |
|         ctx.move_to(x0, y0)
 | |
|         ctx.show_text("Server")
 | |
|         ctx.move_to(x1, y0)
 | |
|         if self.app.track.mqtt_connected:
 | |
|             ctx.show_text("MQTT")
 | |
|         else:
 | |
|             ctx.show_text("offline")
 | |
| 
 | |
|         # Rechte Spalte mit Regattaauswahl
 | |
| 
 | |
|         x = 208
 | |
|         y = 70
 | |
|         yoffset = 21
 | |
| 
 | |
|         # Mögliche Regatten
 | |
|         self.races = self.app.track.hero_get_races()
 | |
|         if len(self.races) == 1:
 | |
|             self.hero_raceid = self.races[0]
 | |
|             self.menupos = 1
 | |
| 
 | |
|         ctx.set_font_size(20)
 | |
|         ctx.move_to(x, y)
 | |
|         ctx.show_text("Select Regatta")
 | |
|         ctx.set_font_size(16)
 | |
|         y += 4
 | |
|         if self.menupos > len(self.races):
 | |
|             # Nichts auswählen
 | |
|             self.menupos = 0
 | |
|             self.raceid = None
 | |
|         i = 0
 | |
|         for r in self.races:
 | |
|             i += 1
 | |
|             if r == self.raceid:
 | |
|                 r = f"\xbb {r} \xab"
 | |
|             self.draw_text_boxed(ctx, x, y, 180, 20, r, (self.menupos == i))
 | |
|             y += 20
 | |
|         if i == 0:
 | |
|             ctx.move_to(x, y + 20)
 | |
|             ctx.show_text("[ none ]")
 | |
| 
 | |
|     def draw_info(self, ctx):
 | |
|         """
 | |
|         Nachricht der Wettfahrtleitung
 | |
|         """
 | |
|         ctx.select_font_face("Ubuntu", cairo.FontSlant.NORMAL, cairo.FontWeight.BOLD)
 | |
|         ctx.set_font_size(24)
 | |
|         ctx.move_to(4, 42)
 | |
|         ctx.show_text("Message from race officers")
 | |
| 
 | |
|         ctx.set_font_size(16)
 | |
|         if not self.app.track.hero_orgstatus or self.app.track.hero_orgstatus['message'] == '':
 | |
|             ctx.move_to(8, 72)
 | |
|             ctx.show_text("[ empty ]")
 | |
|         else:
 | |
|             lines = self.app.track.hero_orgstatus['message'].splitlines()
 | |
|             y = 72
 | |
|             for l in lines:
 | |
|                 ctx.move_to(8, y)
 | |
|                 ctx.show_text(l)
 | |
|                 y += 18
 | |
| 
 | |
|     def draw(self, ctx):
 | |
|         if self.mode == 'N':
 | |
|             if self.ttype in ('LOCAL', 'SDCARD'):
 | |
|                 self.draw_local(ctx)
 | |
|             elif self.ttype == 'HERO':
 | |
|                 self.draw_hero(ctx)
 | |
|             else:
 | |
|                 self.draw_none(ctx)
 | |
|         elif self.mode == 'C':
 | |
|             self.draw_config(ctx)
 | |
|         else:
 | |
|             self.draw_info(ctx)
 | |
|         if self.query_active:
 | |
|             self.draw_query(ctx, "A B O R T   R A C E", "Are you sure to abort the current race? This cannot be undone. Are you sure?")
 |