def waiting_screen(size: Tuple[int, int], config: WahooConfig) -> Image.Image:
    '''Generate a "waiting" image to display on the scoreboard.'''
    img = Image.new(mode="RGBA", size=size, color=config.get_str("color_bg"))
    center = (int(size[0] * 0.5), int(size[1] * 0.8))
    normal = _fontname_to_file(config.get_str("normal_font"))
    font_size = 72
    fnt = ImageFont.truetype(normal, font_size)
    draw = ImageDraw.Draw(img)
    color = config.get_str("color_fg")
    draw.text(center,
              "Waiting for results...",
              font=fnt,
              fill=color,
              anchor="ms")
    return img
def _mockup(config: WahooConfig) -> Heat:
    heat = Heat(allow_inconsistent=not config.get_bool("inhibit_inconsistent"))
    #pylint: disable=protected-access
    heat._parse_do4("""129;1;1;All
Lane1;123.00;122.00;0
Lane2;60;60;0
Lane3;75.51;75.59;
Lane4;13.33;13.32;
Lane5;0;0;0
Lane6;75.95;75.93;
Lane7;615.01;615.15;0
Lane8;87.65;87.88;0
Lane9;300.00;300.10;0
Lane10;121.02;120.80;0
F679E29E3D8A4CC4""".split("\n"))
    heat._parse_scb("""#129 GIRLS 13 & OVER 200 FLY
SWIMMER, FIRST Z    --ABC             
SWIMMER, ANOTHER    --                
REALLYREALLYLONGNAME--                
PERSON, JUST A      --TEAM            
SWIM, NO            --                
BIGBIGBIGLY, NAMENAM--LONGLONGLONGLONG
GO, INEDA           --FAST            
SWIM, SLOW          --                
NAMES, BORING       --                
IDEAS, OUT O        --                """.split("\n"))
    return heat
Exemple #3
0
def _main():
    config = WahooConfig()

    root = tk.Tk()
    root.resizable(False, False)
    root.columnconfigure(0, weight=1)
    root.rowconfigure(0, weight=1)
    def exitfn() -> None:
        root.destroy()
    def publishfn(_: Image.Image) -> None:
        "Don't publish images for the mockup"
    manual = _DolphinManual(root, exitfn, publishfn, config)
    manual.grid(column=0, row=0, sticky="news")
    tk.mainloop()
Exemple #4
0
 def __init__(self, container: tkContainer, config: WahooConfig, file_cb: Callable[[], None]):
     super().__init__(container, padding=5, text="Race results")
     self._config = config
     self.chooser = tk.StringVar(self)
     # https://bytes.com/topic/python/answers/829768-tkinter-tab-focus-traversal-causes-listbox-selection-clear-ie-lose-selected-item
     self._lbox = tk.Listbox(self, listvariable=self.chooser, width=25, exportselection=0)
     self._lbox.grid(column=0, row=0, sticky="news")
     scr = ttk.Scrollbar(self, orient="vertical", command=self._lbox.yview)
     scr.grid(column=1, row=0, sticky="ns")
     self._lbox.configure(yscrollcommand=scr.set)
     entries = list(os.scandir(config.get_str("dolphin_dir")))
     # Only interested in .do4 files
     entries = [x for x in entries if x.name.endswith(".do4")]
     # Sort, most recent first
     entries.sort(key=lambda e: -e.stat().st_mtime)
     self._names = [x.name for x in entries]
     self.chooser.set(self._names) # type: ignore
     def file_selected(_):
         file_cb()
     self._lbox.bind("<<ListboxSelect>>", file_selected)
     self._lbox.selection_set(0)
     self._lbox.see(0)
Exemple #5
0
def _main():
    root = tk.Tk()
    root.columnconfigure(0, weight=1)
    root.rowconfigure(0, weight=1)

    root.resizable(False, False)
    options = WahooConfig()

    def sel_cb(items):
        print(items)

    settings = Settings(root, None, None, None, sel_cb, None, None, options)
    settings.grid(column=0, row=0, sticky="news")
    root.update()
    print(f"Root window: {root.winfo_width()}x{root.winfo_height()}")
    items = {}
    items["abc"] = {"name": "cc1", "enabled": True}
    items["def"] = {"name": "cc2", "enabled": False}
    items["ghi"] = {"name": "cc1.5", "enabled": True}
    settings.set_items(items)
    root.update()
    image = Image.open("file.png")
    settings.set_preview(image)
    tk.mainloop()
def _main():
    '''Display a scoreboard mockup'''
    root = tk.Tk()
    # Chromecast images are always 1280x720, so we design the image to look
    # good at those dimensions.
    size = (1280, 720)
    canvas = tk.Canvas(root, width=size[0], height=size[1])
    canvas.configure(bg="black", borderwidth=0)
    canvas.pack()
    root.update()

    # Create the SB image
    config = WahooConfig()
    heat = _mockup(config)
    image = ScoreboardImage(heat, size, config).image
    image.save("file.png")

    pimage = ImageTk.PhotoImage(image)
    canvas.create_image(canvas.winfo_width() // 2,
                        canvas.winfo_height() // 2,
                        image=pimage,
                        anchor="center")

    root.mainloop()
Exemple #7
0
 def __init__(self, container: TkContainer, config: WahooConfig, color_option: str):
     super().__init__(container, bg=config.get_str(color_option), relief="solid",
                      padx=7, borderwidth=1, command=self._btn_cb)
     self._color_option = color_option
     self._config = config
Exemple #8
0
def main():
    '''Runs the Wahoo! Results scoreboard'''
    global FILE_WATCHER  # pylint: disable=global-statement
    global IC  # pylint: disable=global-statement

    # Determine if running as a PyInstaller exe bundle
    dsn = None
    execution_environment = "source"
    if getattr(sys, 'frozen', False) and hasattr(sys, '_MEIPASS'):
        execution_environment = "executable"
        dsn = SENTRY_DSN  # only report if in executable mode

    # pylint: disable=abstract-class-instantiated
    # https://github.com/getsentry/sentry-python/issues/1081
    # Initialize Sentry crash reporting
    sentry_sdk.init(
        dsn=dsn,
        sample_rate=1.0,
        traces_sample_rate=1.0,
        environment=execution_environment,
        release=f"wahoo-results@{WAHOO_RESULTS_VERSION}",
        with_locals=True,
        integrations=[ThreadingIntegration(propagate_hub=True)],
        debug=False,
    )
    uname = platform.uname()
    sentry_sdk.set_tag("os_system", uname.system)
    sentry_sdk.set_tag("os_release", uname.release)
    sentry_sdk.set_tag("os_version", uname.version)
    sentry_sdk.set_tag("os_machine", uname.machine)
    config = WahooConfig()
    sentry_sdk.set_user({
        "id": config.get_str("client_id"),
        "ip_address": "{{auto}}",
    })
    hub = sentry_sdk.Hub.current
    hub.start_session(session_mode="application")

    bundle_dir = getattr(sys, '_MEIPASS',
                         os.path.abspath(os.path.dirname(__file__)))

    root = Tk()

    screen_size = f"{root.winfo_screenwidth()}x{root.winfo_screenheight()}"
    sentry_sdk.set_context("display", {
        "size": screen_size,
    })

    root.title("Wahoo! Results")
    icon_file = os.path.abspath(os.path.join(bundle_dir, 'wahoo-results.ico'))
    root.iconbitmap(icon_file)
    root.columnconfigure(0, weight=1)
    root.rowconfigure(0, weight=1)

    FILE_WATCHER = watchdog.observers.Observer()
    FILE_WATCHER.start()

    IC = ImageCast(8011)

    settings_window(root, config)

    # Intercept the close button so we can save the config before destroying
    # the main window and exiting the event loop in case we need to display an
    # error dialog.
    def close_cb():
        try:
            config.save()
        except PermissionError as err:
            messagebox.showerror(
                title="Error saving configuration",
                message=
                f'Unable to write configuration file "{err.filename}". {err.strerror}',
                detail="Please ensure the working directory is writable.")
        root.destroy()

    root.protocol("WM_DELETE_WINDOW", close_cb)

    root.mainloop()

    FILE_WATCHER.stop()
    FILE_WATCHER.join()
    IC.stop()
Exemple #9
0
def settings_window(root: Tk, options: WahooConfig) -> None:
    '''Display the settings window'''
    # Settings window is fixed size
    root.state("normal")
    root.overrideredirect(False)  # show titlebar
    root.resizable(False, False)
    root.geometry("")  # allow automatic size

    settings: Settings

    def clear_cb():
        image = waiting_screen((1280, 720), options)
        settings.set_preview(image)
        IC.publish(image)

    def test_cb() -> None:
        heat = results.Heat(
            allow_inconsistent=not options.get_bool("inhibit_inconsistent"))
        #pylint: disable=protected-access
        heat._parse_do4("""432;1;1;All
Lane1;991.03;991.02;
Lane2;48.00;48.00;
Lane3;600.20;600.20;
Lane4;0;0;0
Lane5;312.34;312.34;
Lane6;678.12;679.12;
Lane7;1000.03;1000.03;
Lane8;1000.03;1000.03;
Lane9;1010.03;1010.03;
Lane10;1000.03;1000.03;
F679E29E3D8A4CC4""".split("\n"))
        # Names from https://www.name-generator.org.uk/
        #pylint: disable=protected-access
        heat._parse_scb("""#432 GIRLS 13&O 1650 FREE
MILLER, STEPHANIE   --TEAM1           
DAVIS, SARAH        --TEAM1           
GARCIA, ASHLEY      --TEAM1           
WILSON, JESSICA     --TEAM1           
                    --                
MOORE, SAMANTHA     --TEAM1           
JACKSON, AMBER      --TEAM1           
TAYLOR, MELISSA     --TEAM1           
ANDERSON, RACHEL    --TEAM1           
WHITE, MEGAN        --TEAM1           """.split("\n"))
        heat_cb(heat)

    def selection_cb(enabled_uuids: List[uuid.UUID]) -> None:
        for status in IC.get_devices():
            if status["uuid"] in enabled_uuids:
                IC.enable(status["uuid"], True)
            else:
                IC.enable(status["uuid"], False)

    def watchdir_cb(_dir: str) -> None:
        FILE_WATCHER.unschedule_all()
        path = options.get_str("dolphin_dir")
        if os.path.exists(path):
            FILE_WATCHER.schedule(do4_handler, path)

    def heat_cb(heat: results.Heat) -> None:
        sbi = ScoreboardImage(heat, (1280, 720), options)
        settings.set_preview(sbi.image)
        IC.publish(sbi.image)

    def cast_discovery_cb() -> None:
        items = {}
        for status in IC.get_devices():
            items[status["uuid"]] = {
                "name": status["name"],
                "enabled": status["enabled"]
            }
        settings.set_items(items)

    def manual_publish(img: Image.Image) -> None:
        settings.set_preview(img)
        IC.publish(img)

    def manual_btn_cb() -> None:
        show_manual_chooser(root, manual_publish, options)

    do4_handler = Do4Handler(heat_cb, options)
    path = options.get_str("dolphin_dir")
    if os.path.exists(path):
        FILE_WATCHER.schedule(do4_handler, path)

    settings = Settings(root, generate_dolphin_csv, clear_cb, test_cb,
                        selection_cb, watchdir_cb, manual_btn_cb, options)
    settings.grid(column=0, row=0, sticky="news")

    IC.set_discovery_callback(cast_discovery_cb)
    IC.start()
    clear_cb()