From: Advaith Menon Date: Thu, 29 Jan 2026 15:16:30 +0000 (-0500) Subject: Initial Commit X-Git-Url: https://git.devinivas.org/?a=commitdiff_plain;h=1733f7b4bdeacdd194f3771240c9e132dbe502e8;p=4180kiosk.git Initial Commit --- 1733f7b4bdeacdd194f3771240c9e132dbe502e8 diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..e15106e --- /dev/null +++ b/.gitignore @@ -0,0 +1,216 @@ +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[codz] +*$py.class + +# C extensions +*.so + +# Distribution / packaging +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +share/python-wheels/ +*.egg-info/ +.installed.cfg +*.egg +MANIFEST + +# PyInstaller +# Usually these files are written by a python script from a template +# before PyInstaller builds the exe, so as to inject date/other infos into it. +*.manifest +*.spec + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ +.nox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*.cover +*.py.cover +.hypothesis/ +.pytest_cache/ +cover/ + +# Translations +*.mo +*.pot + +# Django stuff: +*.log +local_settings.py +db.sqlite3 +db.sqlite3-journal + +# Flask stuff: +instance/ +.webassets-cache + +# Scrapy stuff: +.scrapy + +# Sphinx documentation +docs/_build/ + +# PyBuilder +.pybuilder/ +target/ + +# Jupyter Notebook +.ipynb_checkpoints + +# IPython +profile_default/ +ipython_config.py + +# pyenv +# For a library or package, you might want to ignore these files since the code is +# intended to run in multiple environments; otherwise, check them in: +# .python-version + +# pipenv +# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. +# However, in case of collaboration, if having platform-specific dependencies or dependencies +# having no cross-platform support, pipenv may install dependencies that don't work, or not +# install all needed dependencies. +# Pipfile.lock + +# UV +# Similar to Pipfile.lock, it is generally recommended to include uv.lock in version control. +# This is especially recommended for binary packages to ensure reproducibility, and is more +# commonly ignored for libraries. +# uv.lock + +# poetry +# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. +# This is especially recommended for binary packages to ensure reproducibility, and is more +# commonly ignored for libraries. +# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control +# poetry.lock +# poetry.toml + +# pdm +# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control. +# pdm recommends including project-wide configuration in pdm.toml, but excluding .pdm-python. +# https://pdm-project.org/en/latest/usage/project/#working-with-version-control +# pdm.lock +# pdm.toml +.pdm-python +.pdm-build/ + +# pixi +# Similar to Pipfile.lock, it is generally recommended to include pixi.lock in version control. +# pixi.lock +# Pixi creates a virtual environment in the .pixi directory, just like venv module creates one +# in the .venv directory. It is recommended not to include this directory in version control. +.pixi + +# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm +__pypackages__/ + +# Celery stuff +celerybeat-schedule +celerybeat.pid + +# Redis +*.rdb +*.aof +*.pid + +# RabbitMQ +mnesia/ +rabbitmq/ +rabbitmq-data/ + +# ActiveMQ +activemq-data/ + +# SageMath parsed files +*.sage.py + +# Environments +.env +.envrc +.venv +env/ +venv/ +ENV/ +env.bak/ +venv.bak/ + +# Spyder project settings +.spyderproject +.spyproject + +# Rope project settings +.ropeproject + +# mkdocs documentation +/site + +# mypy +.mypy_cache/ +.dmypy.json +dmypy.json + +# Pyre type checker +.pyre/ + +# pytype static type analyzer +.pytype/ + +# Cython debug symbols +cython_debug/ + +# PyCharm +# JetBrains specific template is maintained in a separate JetBrains.gitignore that can +# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore +# and can be added to the global gitignore or merged into this file. For a more nuclear +# option (not recommended) you can uncomment the following to ignore the entire idea folder. +# .idea/ + +# Abstra +# Abstra is an AI-powered process automation framework. +# Ignore directories containing user credentials, local state, and settings. +# Learn more at https://abstra.io/docs +.abstra/ + +# Visual Studio Code +# Visual Studio Code specific template is maintained in a separate VisualStudioCode.gitignore +# that can be found at https://github.com/github/gitignore/blob/main/Global/VisualStudioCode.gitignore +# and can be added to the global gitignore or merged into this file. However, if you prefer, +# you could uncomment the following to ignore the entire vscode folder +# .vscode/ + +# Ruff stuff: +.ruff_cache/ + +# PyPI configuration file +.pypirc + +# Marimo +marimo/_static/ +marimo/_lsp/ +__marimo__/ + +# Streamlit +.streamlit/secrets.toml diff --git a/default_confs/pictures/ed_qr.png b/default_confs/pictures/ed_qr.png new file mode 100644 index 0000000..f0fee30 Binary files /dev/null and b/default_confs/pictures/ed_qr.png differ diff --git a/default_confs/pictures/placeholder.png b/default_confs/pictures/placeholder.png new file mode 100644 index 0000000..b0f13d1 Binary files /dev/null and b/default_confs/pictures/placeholder.png differ diff --git a/default_confs/site.conf b/default_confs/site.conf new file mode 100644 index 0000000..e6d104c --- /dev/null +++ b/default_confs/site.conf @@ -0,0 +1,14 @@ +[fratta.ece.gatech.edu] +; Strings +window_title = ECE 4180 TA Experience +site_title = ECE 4180 Office Hours +queue_title = Queue + +nobody_avatar = default_confs/pictures/ed_qr.png + +; Actual conf +api_path_q = https://proxyembedded.bp4k.net/4180q.py +api_q_element = queue.checkoffQueue,queue.helpQueue + + +api_key = doesnt-matter diff --git a/default_confs/style.conf b/default_confs/style.conf new file mode 100644 index 0000000..5eee955 --- /dev/null +++ b/default_confs/style.conf @@ -0,0 +1,39 @@ +[fratta.ece.gatech.edu] +title__family = Lato +title__size = 60 +title__weight = bold +title__slant = roman +title__bgcolor = #000000 +title__fgcolor = #ffff00 + +queue__family = Lato +queue__size = 30 +queue__weight = bold +queue__slant = roman +queue__bgcolor = #000000 +queue__fgcolor = #ffff00 + +picframe__bgcolor = #000000 + +qframe__bgcolor = #000000 + +q_checkoff__family = Lato +q_checkoff__size = 15 +q_checkoff__weight = normal +q_checkoff__slant = italic +q_checkoff__bgcolor = #006600 +q_checkoff__fgcolor = #ffffff + +q_help__family = Lato +q_help__size = 15 +q_help__weight = normal +q_help__slant = italic +q_help__bgcolor = #ff3300 +q_help__fgcolor = #ffffff + +q_empty__family = Helvetica +q_empty__size = 15 +q_empty__weight = normal +q_empty__slant = italic +q_empty__bgcolor = #000000 +q_empty__fgcolor = #ffffff diff --git a/ohdisp/__init__.py b/ohdisp/__init__.py new file mode 100644 index 0000000..d3177cf --- /dev/null +++ b/ohdisp/__init__.py @@ -0,0 +1,33 @@ +import argparse +import sys + +from .views import Root +from .config import ConfigContainer +from .config import SITE_CONF_PATH, STYLE_CONF_PATH + + +def main(argv): + parser = argparse.ArgumentParser( + prog="ohdisp", + description="Displays TA Office Hours"); + parser.add_argument('-c', '--config', default=SITE_CONF_PATH); + parser.add_argument('-d', '--domain', default="DEFAULT"); + parser.add_argument('-s', '--stylesheet', default=STYLE_CONF_PATH); + parser.add_argument('-f', '--fullscreen', action="store_true"); + + args = parser.parse_args(argv[1:]) + + config = ConfigContainer(site=args.config, style=args.stylesheet); + config.site._set_site(args.domain); + config.style._set_site(args.domain); + + root_tk = Root(config); + + if args.fullscreen: + root_tk.fullscreen(); + + root_tk.mainloop(); + + +if __name__ == "__main__": + main(sys.argv) diff --git a/ohdisp/config.py b/ohdisp/config.py new file mode 100644 index 0000000..bee6bde --- /dev/null +++ b/ohdisp/config.py @@ -0,0 +1,49 @@ +import os +import configparser + + +__all__ = ['Config'] + + +DEFAULT_SITE = "DEFAULT"; +SITE_CONF_PATH = "~/.ohdisp/site.conf"; +STYLE_CONF_PATH = "~/.ohdisp/style.conf"; + + +class Config(object): + def __init__(self, conf_path): + self._conf_path = conf_path; + self._config = configparser.ConfigParser(comment_prefixes=(';',)); + self._config.read(os.path.expanduser(self._conf_path)); + + self._set_site(DEFAULT_SITE); + + def _set_site(self, site): + if site not in self._config: + raise KeyError("'{}' does not exist in config".format(site)); + self._site = site; + + def _save(self): + with open(os.path.expanduser(self._conf_path), 'w') as fp: + self._config.write(fp); + + def __getattr__(self, attr): + if attr.startswith("_"): + return super().__getattr__(attr); + return self._config[self._site][attr]; + + def __setattr__(self, attr, value): + if attr.startswith("_"): + super().__setattr__(attr, value); + return + self._config[self._site][attr] = value; + + +class ConfigContainer(object): + def __init__(self, *_, **config_paths): + # map from config name to path + self._confs = {x: Config(config_paths[x]) for x in config_paths}; + + def __getattr__(self, attr): + return self._confs[attr]; + diff --git a/ohdisp/model.py b/ohdisp/model.py new file mode 100644 index 0000000..e551ef9 --- /dev/null +++ b/ohdisp/model.py @@ -0,0 +1,159 @@ +import time +import datetime + +import tkinter as tk +from tkinter.font import Font + +import urllib3 +import pyttsx3 + +class QueueModel(object): + def __init__(self, config, *args, **kwargs): + self._cfg = config; + self._funcs = tuple(self.parse_cfg(self._cfg.site.api_q_element)); + + def fetch(self): + resp = urllib3.request( + "GET", + self._cfg.site.api_path_q) + json = resp.json(); + result = []; + for func in self._funcs: + result += func(json); + + return result; + + def thread(self, tk_parent): + n_students = 0 + cur_students = []; + self.build_elements(cur_students, tk_parent); + while True: + students = self.fetch(); + nu_students = len(students); + if (nu_students != n_students): + d = self.diff_students(cur_students, students); + self.build_elements(students, tk_parent); + self.announce(d); + n_students = nu_students; + cur_students = students; + time.sleep(2); + + def announce(self, data): + new_joinees, d = data + if new_joinees: + sts = "Student{} {} {} joined the queue!".format( + "" if len(d) < 2 else "s", + ", ".join(map(lambda x: x["studentName"], d)), + "has" if len(d) < 2 else "have", + ); + pyttsx3.speak(sts) + + def build_elements(self, students, tk_parent): + for el in tk_parent.winfo_children(): + el.destroy() + + checkoff_fnt = Font(tk_parent, + family=self._cfg.style.q_checkoff__family, + size=int(self._cfg.style.q_checkoff__size), + weight=self._cfg.style.q_checkoff__weight, + slant=self._cfg.style.q_checkoff__slant, + ); + + help_fnt = Font(tk_parent, + family=self._cfg.style.q_help__family, + size=int(self._cfg.style.q_help__size), + weight=self._cfg.style.q_help__weight, + slant=self._cfg.style.q_help__slant, + ); + + null_fnt = Font(tk_parent, + family=self._cfg.style.q_empty__family, + size=int(self._cfg.style.q_empty__size), + weight=self._cfg.style.q_empty__weight, + slant=self._cfg.style.q_empty__slant, + ); + + if len(students) == 0: + lbl = tk.Label(tk_parent, + text="The queue is empty\nFeel free to join!", + font=checkoff_fnt, + fg=self._cfg.style.q_empty__fgcolor, + bg=self._cfg.style.q_empty__bgcolor, + padx=4, + pady=4, + anchor='n' + ); + lbl.pack(side='top', fill='x', anchor='n', expand=1); + return + + for stu in students: + if stu["queueType"] == "checkoff": + lbl = tk.Label(tk_parent, + text="{} ({} {})".format( + stu["studentName"], stu["labId"], + stu["partId"]), + font=checkoff_fnt, + fg=self._cfg.style.q_checkoff__fgcolor, + bg=self._cfg.style.q_checkoff__bgcolor, + padx=4, + pady=4, + anchor='w' + ); + else: + lbl = tk.Label(tk_parent, + text=stu["studentName"], + font=checkoff_fnt, + fg=self._cfg.style.q_help__fgcolor, + bg=self._cfg.style.q_help__bgcolor, + padx=4, + pady=4, + anchor='w' + ); + + lbl.pack(side='top', fill='x', anchor='n', expand=0); + + @classmethod + def diff_students(cls, cur_students, students): + cur_students = sorted(cur_students, + key=lambda x: datetime.datetime.fromisoformat( + x["joinedAt"])); + students = sorted(students, + key=lambda x: datetime.datetime.fromisoformat( + x["joinedAt"])); + + i = 0 + j = 0 + l = list() + + while i < len(cur_students) and j < len(students): + if cur_students[i]["entryId"] == students[j]["entryId"]: + i += 1 + j += 1 + else: + if len(cur_students) > len(students): + l.append(cur_students[i]); + i += 1 + elif len(students) > len(cur_students): + l.append(students[j]); + j += 1 + + if i < len(cur_students): + l += cur_students[i:] + elif j < len(students): + l += students[j:] + + return ( + len(students) > len(cur_students), + l + ) + + def parse_cfg(self, line): + for arg in line.split(","): + data = arg.split("."); + def func(dic, x=data): + curr = dic; + for i in x: + curr = curr[i]; + return curr; + yield func; + diff --git a/ohdisp/views.py b/ohdisp/views.py new file mode 100644 index 0000000..490fb06 --- /dev/null +++ b/ohdisp/views.py @@ -0,0 +1,105 @@ +import os +import threading + +import tkinter as tk +from tkinter.font import Font +from .model import QueueModel + +from PIL import ImageTk, Image + + +class TADisplayFrame(tk.Frame): + def __init__(self, parent, config, *args, **kwargs): + super().__init__(parent, *args, **kwargs); + + self._cnf = config; + + title_font = Font(self, + family=self._cnf.style.title__family, + size=int(self._cnf.style.title__size), + weight=self._cnf.style.title__weight, + slant=self._cnf.style.title__slant, + ); + + self._title = tk.Label(self, text=self._cnf.site.site_title, + font=title_font, + bg=self._cnf.style.title__bgcolor, + fg=self._cnf.style.title__fgcolor); + self._title.pack(side='top', anchor='n', fill='x'); + + self._frame = StudentCheckoffFrame(self, self._cnf); + self._frame.pack(side='right', fill="both"); + self._frame = TAPictureFrame(self, self._cnf); + self._frame.pack(side='top', fill='both', expand=1); + + +class TAPictureFrame(tk.Frame): + def __init__(self, parent, config, *args, **kwargs): + super().__init__(parent, *args, **kwargs); + + self._cnf = config; + self['bg'] = self._cnf.style.picframe__bgcolor; + + im = Image.open( + os.path.expanduser( + self._cnf.site.nobody_avatar)); + im.thumbnail((512, 512), Image.Resampling.LANCZOS); + self._img = ImageTk.PhotoImage(im); + lbl = tk.Label(self, image=self._img); + lbl.pack(side="top"); + lbl2 = tk.Label(self, text=("This feature is a work in progress!" + "\nAsk questions on Ed Discussion" + " post office hours" + "\n\nDoing a video checkoff? Make sure " + "to show your code, face and buzzcard!"), + font=("Lato", 20, "normal"), + bg="#000000", + fg="#FFFFFF"); + lbl2.pack(side="top"); + + + +class StudentCheckoffFrame(tk.Frame): + def __init__(self, parent, config, *args, **kwargs): + super().__init__(parent, *args, **kwargs); + + self._cnf = config; + + self._model = QueueModel(self._cnf); + + title_font = Font(self, + family=self._cnf.style.queue__family, + size=int(self._cnf.style.queue__size), + weight=self._cnf.style.queue__weight, + slant=self._cnf.style.queue__slant, + ); + + self._title = tk.Label(self, text=self._cnf.site.queue_title, + font=title_font, + bg=self._cnf.style.queue__bgcolor, + fg=self._cnf.style.queue__fgcolor); + self._title.pack(side='top', anchor='n', fill='x'); + + self._actualframe = tk.Frame(self, + bg=self._cnf.style.qframe__bgcolor); + self._actualframe.pack(side='top', fill='both', expand=1); + + t = threading.Thread(target=self._model.thread, + args=(self._actualframe,), + daemon=True) + t.start() + + +class Root(tk.Tk): + def __init__(self, config, *args, **kwargs): + super().__init__(*args, **kwargs); + + self._cnf = config; + + self.title(self._cnf.site.window_title); + + self._tadf = TADisplayFrame(self, self._cnf); + self._tadf.pack(fill='both', expand=1); + + def fullscreen(self): + self.attributes("-fullscreen", True); diff --git a/run_ohdisp.py b/run_ohdisp.py new file mode 100755 index 0000000..f4de576 --- /dev/null +++ b/run_ohdisp.py @@ -0,0 +1,9 @@ +#!/usr/bin/python3 + +import sys + +from ohdisp import main + + +if __name__ == "__main__": + main(sys.argv)