"""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= # Whether this device has Wonder Ink at all. # # Every classroom does. A laptop that goes out with a volunteer does not, # and a room that is offered a lesson application it has no licence for is # a button that can only disappoint. # # false takes the launcher off the panel and stops the command starting. # The ordinary browser, Parent Call and everything else are untouched. WONDERINK_ENABLED=true # What kind of machine this is. # # desktop a classroom tower. This is every existing room. # laptop a machine with a lid and an internal panel. # # This says what the hardware IS. It decides nothing about which # applications the device offers: a laptop running children's church needs # Wonder Ink, and a desktop used for Bible study does not. Every # application below is its own setting, on purpose, so that moving a # machine between rooms is a matter of saying what it is for rather than # what shape it is. DEVICE_TYPE=desktop # Whether this device offers the disc player. # # The machine is asked as well: a room with no optical drive never shows # the player whatever this says, because a button that cannot work is # worse than no button. This setting is how a device that HAS a drive is # told not to offer it anyway. DVD_ENABLED=true # Whether this device offers Parent Call. # # A room full of children needs it. A Bible study does not, and the button # in the corner is then one more thing to explain. PARENT_CALL_ENABLED=true # Which network this device is meant to be on. # # wired an Ethernet cable is the intended connection. # wifi this device is meant to be on Wi-Fi. Normal and expected; the # room is not warned about a missing cable. # # This changes what the network indicator offers, never what # NetworkManager is allowed to connect to. Wi-Fi hardware is not disabled # on a wired device, and a wired device is not stopped from falling back. NETWORK_PREFERENCE=wired # Which displays this device drives. # # all every connected display, which is what a tower with one # television wants and what every existing room gets. # external-only the external display only, with an internal laptop # panel switched off. For a laptop installed as a fixed # appliance with its lid shut. # # external-only is a setting rather than something inferred from having a # lid, because another laptop may want its own screen or both. DISPLAY_MODE=all """ # 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 """, ), ( "WONDERINK_ENABLED", '''# Whether this device has Wonder Ink at all. # # Every classroom does. A laptop that goes out with a volunteer does not, # and a room that is offered a lesson application it has no licence for is # a button that can only disappoint. # # false takes the launcher off the panel and stops the command starting. # The ordinary browser, Parent Call and everything else are untouched. WONDERINK_ENABLED=true ''', ), ( "DEVICE_TYPE", """# What kind of machine this is. # # desktop a classroom tower. This is every existing room. # laptop a machine with a lid and an internal panel. # # This says what the hardware IS. It decides nothing about which # applications the device offers: a laptop running children's church needs # Wonder Ink, and a desktop used for Bible study does not. Every # application below is its own setting, on purpose, so that moving a # machine between rooms is a matter of saying what it is for rather than # what shape it is. DEVICE_TYPE=desktop """, ), ( "DVD_ENABLED", """# Whether this device offers the disc player. # # The machine is asked as well: a room with no optical drive never shows # the player whatever this says, because a button that cannot work is # worse than no button. This setting is how a device that HAS a drive is # told not to offer it anyway. DVD_ENABLED=true """, ), ( "PARENT_CALL_ENABLED", """# Whether this device offers Parent Call. # # A room full of children needs it. A Bible study does not, and the button # in the corner is then one more thing to explain. PARENT_CALL_ENABLED=true """, ), ( "NETWORK_PREFERENCE", """# Which network this device is meant to be on. # # wired an Ethernet cable is the intended connection. # wifi this device is meant to be on Wi-Fi. Normal and expected; the # room is not warned about a missing cable. # # This changes what the network indicator offers, never what # NetworkManager is allowed to connect to. Wi-Fi hardware is not disabled # on a wired device, and a wired device is not stopped from falling back. NETWORK_PREFERENCE=wired """, ), ( "DISPLAY_MODE", """# Which displays this device drives. # # all every connected display, which is what a tower with one # television wants and what every existing room gets. # external-only the external display only, with an internal laptop # panel switched off. For a laptop installed as a fixed # appliance with its lid shut. # # external-only is a setting rather than something inferred from having a # lid, because another laptop may want its own screen or both. DISPLAY_MODE=all """, ), ( "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") DEVICE_TYPES = ("desktop", "laptop") NETWORK_PREFERENCES = ("wired", "wifi") DISPLAY_MODES = ("all", "external-only") # Every optional application, and what a device that has never heard of # the setting should do. All true: an existing classroom upgrading to this # release must come up exactly as it was, and every existing classroom has # all of these. FEATURES = { "WONDERINK_ENABLED": True, "DVD_ENABLED": True, "PARENT_CALL_ENABLED": True, } def feature(config, name): """Whether an optional application is offered on this device. Deliberately reads one key per application and never consults DEVICE_TYPE. A laptop taking children's church needs Wonder Ink and a tower used for Bible study does not, so shape cannot be allowed to decide purpose — that rule is the whole reason these are separate settings rather than a profile. """ return enabled(config, name, default=FEATURES.get(name, False)) def _choice(config, key, allowed, default): value = (config.get(key) or "").strip().lower() return value if value in allowed else default def device_type(config): """What the hardware is. Never what it is for.""" return _choice(config, "DEVICE_TYPE", DEVICE_TYPES, "desktop") def network_preference(config): return _choice(config, "NETWORK_PREFERENCE", NETWORK_PREFERENCES, "wired") def display_mode(config): return _choice(config, "DISPLAY_MODE", DISPLAY_MODES, "all") 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)))