Files
Classroom_Desktop/lib/classroom_config.py
T
ekklesiaandClaude Opus 5 34fd02e08f feat: give every room the broker, one scale, and its own panel back
Little Waders took the Plasma build and reported what a container
cannot. A disc player on a machine with no optical drive, a clock nobody
at the back of the room could read, a browser at half the size of the
panel around it, and Parent Call with no broker to call.

The broker is the same for every classroom, so it is a default now and
not something to fill in room by room. classroom-migrate brings an
existing room to the same place without touching its name, its username
or its password, and Parent Call distinguishes a broker it cannot reach
from one that was never set up: those send somebody to look at
completely different things.

The disc player's detection was right and ran too late — alongside the
session rather than before it, and the panel is built from the layout the
moment a session starts. It runs before the display manager now, and
again from udev when a drive is plugged in.

UI_SCALE had a ceiling of 1.5, which cannot express a 4K television; a
clock pinned at 15 points, which is the same size at every scale; and a
browser that was never told the number at all. All three are fixed and
the room doubles together at 200%.

This also brings 0.1.47's work into the repository, where it had only
ever existed in a build directory under /tmp: the hidden display dialog,
its authenticated writer, the polkit action, and the church's own
wallpaper. One piece of it is deliberately left out. The custom volume
display needed a shell package of our own, and with one selected
plasmashell builds no panel at all — measured twice, with that build's
own files, on a classroom that deletes its panel configuration at every
login by design.

The lab connected to the real church broker the moment the package
defaulted to it. Nothing was left behind, which was checked, but the
guard that should have stopped it had never worked: it used iptables,
which the lab image does not have, and probed with /dev/tcp under dash,
where that always fails. Both reported success.

Co-Authored-By: Claude Opus 5 <[email protected]>
2026-09-12 19:05:09 -05:00

211 lines
6.7 KiB
Python

"""Device configuration for the Ekklesia Classroom shell.
Everything that has to agree about /etc/ekklesia-classroom/device.conf
reads it through here, so the dock, the notification daemon and the DVD
commands cannot drift apart over what a setting means.
"""
import math
from pathlib import Path
CONFIG_FILE = Path("/etc/ekklesia-classroom/device.conf")
TRUTHY = {"1", "true", "yes", "on", "enabled"}
# Tuned for a 1080p classroom television at 1.0. A 4K set wants 2.0,
# which is the same interface at the same physical size on a display with
# twice the pixels.
UI_SCALE_DEFAULT = 1.0
UI_SCALE_MIN = 0.8
# 2.0 is a 4K television drawn at 1080p sizes, which is the setting the
# large classroom screens actually need. The old ceiling of 1.5 could not
# express it, so a room asking for 2.0 silently got 1.5.
UI_SCALE_MAX = 3.0
DEVICE_CONFIG_TEMPLATE = """# Ekklesia Classroom device configuration
UPDATE_URL=https://updates.riverofglorymn.org
CHANNEL=stable
# Show the Sanctuary LIVE placeholder in the dock. Live viewing is not
# implemented yet; this only controls whether the current placeholder appears.
LIVE_ENABLED=false
# Which output the room plays through.
#
# auto prefer a connected HDMI or DisplayPort television, and fall
# back to the analog jack. This is right for almost every room.
# hdmi always the television, even if it does not report itself.
# analog always the jack.
#
# auto re-decides whenever the hardware changes, so a television switched
# on after the computer is picked up without anybody touching anything,
# and sound that is already playing is moved across to it.
AUDIO_OUTPUT=auto
# How large everything in the room is drawn: the panel, the clock, the
# browser, Wonder Ink, the disc player, notifications and the volume
# display, all from this one number. 1.0 to 3.0.
#
# 1.0 suits a 1080p television at ordinary classroom distance. 2.0 is
# right for a 4K set, where everything is otherwise half the size it
# looks. This is an administrator's setting: there is no way to reach it
# from the room, on purpose.
UI_SCALE=1.0
# Church message broker. Every classroom uses the same one, so the address
# is a default rather than something to fill in room by room. The room is
# this machine's hostname unless ROOM is set. A username and password are
# only needed if the broker asks for them.
MQTT_ENABLED=true
MQTT_HOST=10.40.0.2
MQTT_PORT=1883
MQTT_USERNAME=
MQTT_PASSWORD=
"""
# Sections added after the first classroom release. Keeping them separate lets
# an upgrade make a setting discoverable without replacing anything an
# administrator has already put in device.conf.
DEVICE_CONFIG_SECTIONS = (
(
"LIVE_ENABLED",
"""# Show the Sanctuary LIVE placeholder in the dock. Live viewing is not
# implemented yet; this only controls whether the current placeholder appears.
LIVE_ENABLED=false
""",
),
(
"AUDIO_OUTPUT",
"""# Which output the room plays through.
#
# auto prefer a connected HDMI or DisplayPort television, and fall
# back to the analog jack. This is right for almost every room.
# hdmi always the television, even if it does not report itself.
# analog always the jack.
#
# auto re-decides whenever the hardware changes, so a television switched
# on after the computer is picked up without anybody touching anything,
# and sound that is already playing is moved across to it.
AUDIO_OUTPUT=auto
""",
),
(
"UI_SCALE",
"""# How large everything in the room is drawn: the panel, the clock, the
# browser, Wonder Ink, the disc player, notifications and the volume
# display, all from this one number. 1.0 to 3.0.
#
# 1.0 suits a 1080p television at ordinary classroom distance. 2.0 is
# right for a 4K set, where everything is otherwise half the size it
# looks. This is an administrator's setting: there is no way to reach it
# from the room, on purpose.
UI_SCALE=1.0
""",
),
(
"MQTT_ENABLED",
"""# Church message broker. Every classroom uses the same one, so the address
# is a default rather than something to fill in room by room. The room is
# this machine's hostname unless ROOM is set. A username and password are
# only needed if the broker asks for them.
MQTT_ENABLED=true
MQTT_HOST=10.40.0.2
MQTT_PORT=1883
MQTT_USERNAME=
MQTT_PASSWORD=
""",
),
)
def read_device_config(path=None):
config = {}
path = Path(path) if path is not None else CONFIG_FILE
if not path.exists():
return config
for raw_line in path.read_text().splitlines():
line = raw_line.strip()
if not line or line.startswith("#") or "=" not in line:
continue
key, value = line.split("=", 1)
config[key.strip()] = value.strip()
return config
def enabled(config, key, default=False):
if key not in config:
return default
return config[key].lower() in TRUTHY
def ensure_device_config(path=None):
"""Create device.conf or append any missing settings exactly once.
Existing bytes are otherwise left alone: per-room settings, comments and
even keys this release does not know about all survive an upgrade.
"""
path = Path(path) if path is not None else CONFIG_FILE
if not path.exists():
path.parent.mkdir(parents=True, exist_ok=True)
path.write_text(DEVICE_CONFIG_TEMPLATE)
return
text = path.read_text()
config = read_device_config(path)
additions = []
for key, section in DEVICE_CONFIG_SECTIONS:
if key not in config:
additions.append(section.rstrip("\n"))
if not additions:
return
separator = "" if not text or text.endswith("\n") else "\n"
path.write_text(text + separator + "\n\n".join(additions) + "\n")
def ui_scale(config):
"""UI_SCALE, clamped to something a classroom can actually use."""
raw = config.get("UI_SCALE", "")
try:
value = float(raw)
except (TypeError, ValueError):
return UI_SCALE_DEFAULT
if not math.isfinite(value):
return UI_SCALE_DEFAULT
return min(max(value, UI_SCALE_MIN), UI_SCALE_MAX)
# The scale the classroom's own GTK surfaces render at.
#
# UI_SCALE is applied to the whole output (see classroom-scale),
# which scales Chrome, VLC and every other application along with the
# panel. Our surfaces are measured in the same logical pixels the
# compositor is
# already scaling, so scaling them here as well would apply UI_SCALE
# twice and the dock would come out half again too big.
#
# This is the seam: if compositor scaling ever has to be abandoned,
# changing this back to ui_scale(config) restores per-surface scaling
# without touching the templates.
SHELL_SCALE = 1.0
def scaled(value, scale):
"""A pixel measurement at the given scale, never smaller than 1."""
return max(1, int(round(value * scale)))