Update cloud-init scripts and documentation for enhanced DNS management and provisioning steps</message>
<message>Modify the first-boot.sh script to include an additional step for managing screen brightness during the provisioning process. Update user-data.bootstrap to improve DNS configuration by ensuring NetworkManager manages /etc/resolv.conf correctly, and remove obsolete scripts related to systemd-resolved. Enhance documentation to reflect these changes and clarify the setup process for users, improving overall network boot functionality and user experience.
This commit is contained in:
302
emmc-provisioning/cloud-init/fileserver/brightness-overlay.py
Normal file
302
emmc-provisioning/cloud-init/fileserver/brightness-overlay.py
Normal file
@@ -0,0 +1,302 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Brightness control overlay — always-on-top icon in bottom-right; click to show controls.
|
||||
Uses Wayland layer-shell OVERLAY so it stays above Chromium fullscreen/kiosk.
|
||||
Requires: brightness-api.service running (listens 127.0.0.1:8765).
|
||||
"""
|
||||
import logging
|
||||
import sys
|
||||
import urllib.request
|
||||
import urllib.error
|
||||
|
||||
LOG_TAG = "brightness-overlay"
|
||||
logging.basicConfig(
|
||||
stream=sys.stderr,
|
||||
level=logging.INFO,
|
||||
format=f"{LOG_TAG}: %(message)s",
|
||||
)
|
||||
log = logging.getLogger(LOG_TAG)
|
||||
|
||||
try:
|
||||
import gi
|
||||
gi.require_version("Gdk", "3.0")
|
||||
gi.require_version("Gtk", "3.0")
|
||||
from gi.repository import Gdk, Gtk, GLib
|
||||
except Exception as e:
|
||||
log.error("need PyGObject Gtk: %s", e)
|
||||
sys.exit(1)
|
||||
|
||||
HAS_LAYER_SHELL = False
|
||||
try:
|
||||
gi.require_version("GtkLayerShell", "0.1")
|
||||
from gi.repository import GtkLayerShell
|
||||
HAS_LAYER_SHELL = True
|
||||
except Exception:
|
||||
log.warning("GtkLayerShell not available, using regular window")
|
||||
|
||||
API_URL = "http://127.0.0.1:8765/brightness"
|
||||
API_STATE_URL = "http://127.0.0.1:8765/state"
|
||||
|
||||
# Sizes
|
||||
ICON_SIZE = 44
|
||||
EXPANDED_WIDTH = 300
|
||||
PANEL_HEIGHT = 48
|
||||
|
||||
|
||||
def get_current_state():
|
||||
"""Return (level 1–5, is_auto). On error return (None, True) so Auto appears active."""
|
||||
try:
|
||||
with urllib.request.urlopen(API_STATE_URL, timeout=1) as r:
|
||||
text = r.read().decode()
|
||||
level = None
|
||||
override = "0"
|
||||
for line in text.strip().splitlines():
|
||||
line = line.strip()
|
||||
if line.startswith("level="):
|
||||
level = line.split("=", 1)[1].strip()
|
||||
elif line.startswith("override="):
|
||||
override = line.split("=", 1)[1].strip()
|
||||
if level and level.isdigit() and 1 <= int(level) <= 5:
|
||||
return int(level), override == "0"
|
||||
return (None, True)
|
||||
except Exception:
|
||||
return (None, True)
|
||||
|
||||
|
||||
def set_brightness(level):
|
||||
try:
|
||||
req = urllib.request.Request(
|
||||
API_URL,
|
||||
data=f"level={level}".encode(),
|
||||
method="POST",
|
||||
headers={"Content-Type": "application/x-www-form-urlencoded"},
|
||||
)
|
||||
with urllib.request.urlopen(req, timeout=2) as r:
|
||||
log.info("brightness %s: %s", level, r.read().decode().strip())
|
||||
except urllib.error.URLError as e:
|
||||
log.warning("API call failed (is brightness-api.service running?): %s", e)
|
||||
except Exception as e:
|
||||
log.warning("set_brightness failed: %s", e)
|
||||
|
||||
|
||||
def update_active_buttons(level_buttons, auto_btn, current_level, is_auto):
|
||||
"""Set .active style on the button that matches current state."""
|
||||
for i, btn in enumerate(level_buttons):
|
||||
ctx = btn.get_style_context()
|
||||
if not is_auto and current_level == i + 1:
|
||||
ctx.add_class("active")
|
||||
else:
|
||||
ctx.remove_class("active")
|
||||
if auto_btn:
|
||||
ctx = auto_btn.get_style_context()
|
||||
if is_auto:
|
||||
ctx.add_class("active")
|
||||
else:
|
||||
ctx.remove_class("active")
|
||||
|
||||
|
||||
def make_window_layer_shell(win):
|
||||
GtkLayerShell.init_for_window(win)
|
||||
GtkLayerShell.set_layer(win, GtkLayerShell.Layer.OVERLAY)
|
||||
# Anchor only to bottom and right so the surface sits in the bottom-right corner
|
||||
GtkLayerShell.set_anchor(win, GtkLayerShell.Edge.TOP, False)
|
||||
GtkLayerShell.set_anchor(win, GtkLayerShell.Edge.LEFT, False)
|
||||
GtkLayerShell.set_anchor(win, GtkLayerShell.Edge.BOTTOM, True)
|
||||
GtkLayerShell.set_anchor(win, GtkLayerShell.Edge.RIGHT, True)
|
||||
GtkLayerShell.set_keyboard_mode(win, GtkLayerShell.KeyboardMode.NONE)
|
||||
GtkLayerShell.set_margin(win, GtkLayerShell.Edge.BOTTOM, 12)
|
||||
GtkLayerShell.set_margin(win, GtkLayerShell.Edge.RIGHT, 12)
|
||||
if hasattr(GtkLayerShell, "set_namespace"):
|
||||
GtkLayerShell.set_namespace(win, "brightness-overlay")
|
||||
if hasattr(GtkLayerShell, "set_exclusive_zone"):
|
||||
GtkLayerShell.set_exclusive_zone(win, 0)
|
||||
log.info("layer-shell OVERLAY anchored bottom-right (always on top)")
|
||||
|
||||
|
||||
def position_fallback_bottom_right(win, width=None, height=None):
|
||||
"""Position window in bottom-right (X11: only works with toplevel, not POPUP)."""
|
||||
if width is None:
|
||||
width = ICON_SIZE
|
||||
if height is None:
|
||||
height = ICON_SIZE
|
||||
display = Gdk.Display.get_default()
|
||||
if not display:
|
||||
return
|
||||
n = display.get_n_monitors()
|
||||
monitor = display.get_monitor(0) if n else None
|
||||
if not monitor:
|
||||
return
|
||||
geom = monitor.get_geometry()
|
||||
x = geom.x + geom.width - width - 12
|
||||
y = geom.y + geom.height - height - 12
|
||||
win.move(x, y)
|
||||
|
||||
|
||||
def make_window_fallback(win):
|
||||
# Toplevel (not POPUP) so we can position and have no decorations on X11
|
||||
win.set_keep_above(True)
|
||||
win.set_decorated(False)
|
||||
win.set_type_hint(Gdk.WindowTypeHint.UTILITY) # tool-style window, often undecorated
|
||||
# Position after map so WM doesn't override
|
||||
win.connect("map-event", lambda w, e: position_fallback_bottom_right(w))
|
||||
log.info("X11 fallback: keep-above, undecorated, bottom-right")
|
||||
|
||||
|
||||
def main():
|
||||
# Normal toplevel: on X11 we can position it and set_decorated(False) removes title bar.
|
||||
# (POPUP without parent cannot be positioned on X11 and gets "temporary window" / centering.)
|
||||
win = Gtk.Window()
|
||||
win.set_decorated(False)
|
||||
win.set_resizable(False)
|
||||
win.set_default_size(ICON_SIZE, ICON_SIZE)
|
||||
win.set_skip_taskbar_hint(True)
|
||||
win.set_skip_pager_hint(True)
|
||||
# Transparent window: need RGBA visual and app-paintable
|
||||
screen = Gdk.Screen.get_default()
|
||||
if screen and screen.get_rgba_visual():
|
||||
win.set_visual(screen.get_rgba_visual())
|
||||
win.set_app_paintable(True)
|
||||
|
||||
# CSS: transparent window and overlay; only subtle hover so icon stays visible
|
||||
style = """
|
||||
GtkWindow, window {
|
||||
background-color: transparent;
|
||||
background-image: none;
|
||||
}
|
||||
.brightness-icon-btn {
|
||||
background-color: transparent;
|
||||
border-radius: 8px;
|
||||
padding: 4px;
|
||||
min-width: 36px;
|
||||
min-height: 36px;
|
||||
}
|
||||
.brightness-icon-btn:hover {
|
||||
background-color: rgba(0, 0, 0, 0.25);
|
||||
}
|
||||
.brightness-panel {
|
||||
background-color: transparent;
|
||||
border-radius: 8px;
|
||||
padding: 6px;
|
||||
}
|
||||
.brightness-panel.expanded {
|
||||
background-color: rgba(0, 0, 0, 0.5);
|
||||
}
|
||||
.brightness-panel button {
|
||||
min-width: 36px;
|
||||
font-weight: bold;
|
||||
}
|
||||
.brightness-panel button.level-btn.active {
|
||||
background-color: rgba(255, 255, 255, 0.35);
|
||||
border: 1px solid rgba(255, 255, 255, 0.6);
|
||||
}
|
||||
"""
|
||||
css = Gtk.CssProvider()
|
||||
css.load_from_data(style.encode())
|
||||
Gtk.StyleContext.add_provider_for_screen(
|
||||
Gdk.Screen.get_default(),
|
||||
css,
|
||||
Gtk.STYLE_PROVIDER_PRIORITY_APPLICATION,
|
||||
)
|
||||
|
||||
# Main horizontal box: [revealer with controls] [icon button]
|
||||
main_box = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=4)
|
||||
|
||||
# Left part: revealer with Brightness + 1–5 + Auto
|
||||
strip_box = Gtk.Box(spacing=6)
|
||||
strip_box.set_margin_start(8)
|
||||
strip_box.set_margin_top(6)
|
||||
strip_box.set_margin_bottom(6)
|
||||
strip_box.set_margin_end(4)
|
||||
strip_box.get_style_context().add_class("brightness-panel")
|
||||
|
||||
strip_box.pack_start(Gtk.Label(label="Brightness "), False, False, 0)
|
||||
level_buttons = []
|
||||
for i in range(1, 6):
|
||||
btn = Gtk.Button(label=str(i))
|
||||
btn.get_style_context().add_class("level-btn")
|
||||
btn.set_can_focus(False)
|
||||
strip_box.pack_start(btn, False, False, 0)
|
||||
level_buttons.append(btn)
|
||||
auto_btn = Gtk.Button(label="Auto")
|
||||
auto_btn.get_style_context().add_class("level-btn")
|
||||
auto_btn.set_can_focus(False)
|
||||
strip_box.pack_start(auto_btn, False, False, 0)
|
||||
|
||||
def refresh_active_state():
|
||||
level, is_auto = get_current_state()
|
||||
update_active_buttons(level_buttons, auto_btn, level or 3, is_auto)
|
||||
|
||||
def on_level_click(btn, level):
|
||||
set_brightness(level)
|
||||
# Optimistic update so active state shows immediately
|
||||
if level == "auto":
|
||||
update_active_buttons(level_buttons, auto_btn, 3, True)
|
||||
else:
|
||||
update_active_buttons(level_buttons, auto_btn, int(level), False)
|
||||
# Sync from API after a short delay (daemon updates state file periodically)
|
||||
def delayed_refresh():
|
||||
refresh_active_state()
|
||||
return False
|
||||
GLib.timeout_add(500, delayed_refresh)
|
||||
|
||||
for i, btn in enumerate(level_buttons):
|
||||
btn.connect("clicked", on_level_click, str(i + 1))
|
||||
auto_btn.connect("clicked", on_level_click, "auto")
|
||||
|
||||
revealer = Gtk.Revealer()
|
||||
revealer.set_reveal_child(False)
|
||||
revealer.set_transition_type(Gtk.RevealerTransitionType.SLIDE_LEFT)
|
||||
revealer.set_transition_duration(150)
|
||||
revealer.add(strip_box)
|
||||
|
||||
main_box.pack_start(revealer, False, False, 0)
|
||||
|
||||
# Right part: always-visible icon button (click toggles panel)
|
||||
try:
|
||||
icon_img = Gtk.Image.new_from_icon_name("display-brightness-symbolic", Gtk.IconSize.BUTTON)
|
||||
except Exception:
|
||||
icon_img = Gtk.Label(label="☀")
|
||||
icon_btn = Gtk.Button()
|
||||
icon_btn.get_style_context().add_class("brightness-icon-btn")
|
||||
icon_btn.set_can_focus(False)
|
||||
icon_btn.add(icon_img)
|
||||
icon_btn.set_relief(Gtk.ReliefStyle.NONE)
|
||||
|
||||
expanded = [False] # use list so closure can mutate
|
||||
use_fallback = not (HAS_LAYER_SHELL and GtkLayerShell.is_supported())
|
||||
|
||||
def on_icon_clicked(btn):
|
||||
expanded[0] = not expanded[0]
|
||||
revealer.set_reveal_child(expanded[0])
|
||||
ctx = strip_box.get_style_context()
|
||||
if expanded[0]:
|
||||
ctx.add_class("expanded")
|
||||
GLib.idle_add(refresh_active_state)
|
||||
win.resize(EXPANDED_WIDTH, PANEL_HEIGHT)
|
||||
if use_fallback:
|
||||
position_fallback_bottom_right(win, EXPANDED_WIDTH, PANEL_HEIGHT)
|
||||
else:
|
||||
ctx.remove_class("expanded")
|
||||
win.resize(ICON_SIZE, ICON_SIZE)
|
||||
if use_fallback:
|
||||
position_fallback_bottom_right(win, ICON_SIZE, ICON_SIZE)
|
||||
|
||||
icon_btn.connect("clicked", on_icon_clicked)
|
||||
main_box.pack_end(icon_btn, False, False, 0)
|
||||
|
||||
win.add(main_box)
|
||||
|
||||
if HAS_LAYER_SHELL and GtkLayerShell.is_supported():
|
||||
make_window_layer_shell(win)
|
||||
else:
|
||||
make_window_fallback(win)
|
||||
|
||||
win.connect("destroy", Gtk.main_quit)
|
||||
win.show_all()
|
||||
GLib.idle_add(refresh_active_state)
|
||||
log.info("brightness icon visible (click to show controls)")
|
||||
Gtk.main()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in New Issue
Block a user