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:
334
emmc-provisioning/cloud-init/fileserver/screen-brightness.py
Normal file
334
emmc-provisioning/cloud-init/fileserver/screen-brightness.py
Normal file
@@ -0,0 +1,334 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
reTerminal DM — Bridge Screen Brightness Daemon
|
||||
================================================
|
||||
Regulatory alignment
|
||||
IMO MSC.191(79) §8.1 Display must be dimmable but NEVER extinguished
|
||||
§8.1.2 Navigator must be able to reset to a preset level
|
||||
IMO MSC.302(87) Alarm visuals = high luminance; night mode must not
|
||||
degrade crew night vision
|
||||
IHO S-52 / IEC 62288 Three operating modes: NIGHT · DUSK · DAY
|
||||
|
||||
Operating states
|
||||
NIGHT Very low lux → level 1 (minimum — "not to extinction")
|
||||
DUSK Low lux → level 2 (dim)
|
||||
DAY-* Daylight → levels 3-5 graduated by lux sub-range
|
||||
ALERT Buzzer ON → level 5 (max) immediately — no smooth stepping
|
||||
COOLDOWN Buzzer just → hold level 5 for BUZZER_COOLDOWN_SEC seconds,
|
||||
stopped then fade back one step per poll
|
||||
OVERRIDE Manual file → fixed level set by navigator (per MSC.191(79))
|
||||
|
||||
Sysfs paths (reTerminal DM)
|
||||
Backlight : /sys/class/backlight/lcd_backlight/brightness (0-5)
|
||||
Buzzer : /sys/class/leds/usr-buzzer/brightness (0..max, non-zero = ON; often 255)
|
||||
Light IIO : /sys/bus/iio/devices/iio:device0/in_illuminance_input (lux)
|
||||
|
||||
Navigator override (satisfies MSC.191(79) §8.1.2 preset requirement)
|
||||
echo 3 > /run/screen-brightness/override # force fixed level 3
|
||||
echo auto > /run/screen-brightness/override # return to ambient control
|
||||
|
||||
Read current state (useful for dashboard integration)
|
||||
cat /run/screen-brightness/state
|
||||
|
||||
Config file : /etc/screen-brightness.conf (INI, optional)
|
||||
Reload config: kill -HUP <pid> (or: systemctl reload screen-brightness)
|
||||
"""
|
||||
|
||||
import configparser
|
||||
import logging
|
||||
import logging.handlers
|
||||
import os
|
||||
import signal
|
||||
import sys
|
||||
import time
|
||||
from pathlib import Path
|
||||
from typing import Optional, Tuple
|
||||
|
||||
# ── Default configuration ────────────────────────────────────────────────────
|
||||
DEFAULTS: dict = {
|
||||
# sysfs paths
|
||||
"backlight_path": "/sys/class/backlight/lcd_backlight/brightness",
|
||||
"buzzer_path": "/sys/class/leds/usr-buzzer/brightness",
|
||||
"lux_path": "/sys/bus/iio/devices/iio:device0/in_illuminance_input",
|
||||
|
||||
# timing
|
||||
"poll_interval": "2", # seconds between lux/ambient reads
|
||||
"buzzer_poll": "0.5", # seconds between buzzer checks (faster so we catch alarms)
|
||||
"buzzer_cooldown": "10", # seconds to hold max brightness after buzzer stops
|
||||
|
||||
# lux mode boundaries (IHO S-52 three-mode framework)
|
||||
"lux_night_max": "20", # 0–20 lux → NIGHT mode
|
||||
"lux_dusk_max": "200", # 21–200 lux → DUSK mode
|
||||
"lux_day_dim_max": "500", # 201–500 lux → DAY-DIM
|
||||
"lux_day_normal_max": "1000", # 501–1000 lux→ DAY-NORMAL
|
||||
# >1000 lux → DAY-BRIGHT
|
||||
|
||||
# backlight levels per mode (1 = minimum, 5 = maximum, 0 is NEVER used)
|
||||
"level_night": "1",
|
||||
"level_dusk": "2",
|
||||
"level_day_dim": "3",
|
||||
"level_day_normal": "4",
|
||||
"level_day_bright": "5",
|
||||
"level_alert": "5", # during ALERT and COOLDOWN states
|
||||
|
||||
# hysteresis: require this many consecutive polls agreeing on a new ambient
|
||||
# target before committing the change (prevents flickering near thresholds)
|
||||
"hysteresis_polls": "3",
|
||||
}
|
||||
|
||||
CONFIG_FILE = Path("/etc/screen-brightness.conf")
|
||||
OVERRIDE_FILE = Path("/run/screen-brightness/override")
|
||||
STATE_FILE = Path("/run/screen-brightness/state")
|
||||
RUN_DIR = Path("/run/screen-brightness")
|
||||
|
||||
# ── Logging — stdout (captured by journald) + syslog ────────────────────────
|
||||
log = logging.getLogger("screen-brightness")
|
||||
log.setLevel(logging.INFO)
|
||||
|
||||
_fmt_detail = logging.Formatter("%(asctime)s [screen-brightness] %(levelname)s %(message)s")
|
||||
_fmt_syslog = logging.Formatter("screen-brightness[%(process)d]: %(levelname)s %(message)s")
|
||||
|
||||
_stdout_h = logging.StreamHandler(sys.stdout)
|
||||
_stdout_h.setFormatter(_fmt_detail)
|
||||
log.addHandler(_stdout_h)
|
||||
|
||||
try:
|
||||
_syslog_h = logging.handlers.SysLogHandler(address="/dev/log")
|
||||
_syslog_h.setFormatter(_fmt_syslog)
|
||||
log.addHandler(_syslog_h)
|
||||
except OSError:
|
||||
pass # /dev/log not available; journald captures stdout
|
||||
|
||||
|
||||
# ── Config helpers ───────────────────────────────────────────────────────────
|
||||
def load_config() -> dict:
|
||||
cfg = dict(DEFAULTS)
|
||||
if CONFIG_FILE.exists():
|
||||
parser = configparser.ConfigParser()
|
||||
parser.read(CONFIG_FILE)
|
||||
section = "screen-brightness"
|
||||
if parser.has_section(section):
|
||||
cfg.update(parser[section])
|
||||
log.info("Config loaded from %s", CONFIG_FILE)
|
||||
return cfg
|
||||
|
||||
|
||||
# ── Sysfs helpers ────────────────────────────────────────────────────────────
|
||||
def _read_int(path: Path, default: Optional[int] = None) -> Optional[int]:
|
||||
try:
|
||||
return int(path.read_text().strip())
|
||||
except Exception as exc:
|
||||
log.warning("Cannot read %s: %s", path, exc)
|
||||
return default
|
||||
|
||||
|
||||
def _write_int(path: Path, value: int) -> bool:
|
||||
try:
|
||||
path.write_text(f"{value}\n")
|
||||
return True
|
||||
except Exception as exc:
|
||||
log.error("Cannot write %s = %s: %s", path, value, exc)
|
||||
return False
|
||||
|
||||
|
||||
# ── Override file ────────────────────────────────────────────────────────────
|
||||
def read_override() -> Optional[int]:
|
||||
"""Return int level (1-5) if a manual override is active, else None."""
|
||||
try:
|
||||
if not OVERRIDE_FILE.exists():
|
||||
return None
|
||||
raw = OVERRIDE_FILE.read_text().strip().lower()
|
||||
if raw in ("auto", ""):
|
||||
return None
|
||||
val = int(raw)
|
||||
if 1 <= val <= 5:
|
||||
return val
|
||||
except Exception:
|
||||
pass
|
||||
return None
|
||||
|
||||
|
||||
# ── State file (read by dashboard / other services) ──────────────────────────
|
||||
def write_state(mode: str, level: int, lux: Optional[int],
|
||||
buzzer: bool, override: bool) -> None:
|
||||
try:
|
||||
RUN_DIR.mkdir(parents=True, exist_ok=True)
|
||||
STATE_FILE.write_text(
|
||||
f"mode={mode}\n"
|
||||
f"level={level}\n"
|
||||
f"lux={lux if lux is not None else 'N/A'}\n"
|
||||
f"buzzer={'1' if buzzer else '0'}\n"
|
||||
f"override={'1' if override else '0'}\n"
|
||||
f"updated={int(time.time())}\n"
|
||||
)
|
||||
except Exception as exc:
|
||||
log.debug("Cannot write state file: %s", exc)
|
||||
|
||||
|
||||
# ── Lux → ambient level ───────────────────────────────────────────────────────
|
||||
def lux_to_level(lux: int, cfg: dict) -> Tuple[int, str]:
|
||||
"""Return (backlight_level, mode_name) for the given lux reading."""
|
||||
if lux <= int(cfg["lux_night_max"]):
|
||||
return int(cfg["level_night"]), "NIGHT"
|
||||
if lux <= int(cfg["lux_dusk_max"]):
|
||||
return int(cfg["level_dusk"]), "DUSK"
|
||||
if lux <= int(cfg["lux_day_dim_max"]):
|
||||
return int(cfg["level_day_dim"]), "DAY-DIM"
|
||||
if lux <= int(cfg["lux_day_normal_max"]):
|
||||
return int(cfg["level_day_normal"]), "DAY-NORMAL"
|
||||
return int(cfg["level_day_bright"]), "DAY-BRIGHT"
|
||||
|
||||
|
||||
# ── Main loop ────────────────────────────────────────────────────────────────
|
||||
def main() -> None:
|
||||
cfg = load_config()
|
||||
|
||||
BACKLIGHT = Path(cfg["backlight_path"])
|
||||
BUZZER = Path(cfg["buzzer_path"])
|
||||
LUX = Path(cfg["lux_path"])
|
||||
|
||||
for p in (BACKLIGHT, BUZZER, LUX):
|
||||
if not p.exists():
|
||||
log.warning("sysfs path not found at startup (will retry): %s", p)
|
||||
|
||||
RUN_DIR.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
# SIGHUP → reload config without restart (e.g. systemctl reload)
|
||||
_reload_flag = {"pending": False}
|
||||
|
||||
def _on_sighup(_sig: int, _frame: object) -> None:
|
||||
_reload_flag["pending"] = True
|
||||
|
||||
signal.signal(signal.SIGHUP, _on_sighup)
|
||||
|
||||
buzz_interval = float(cfg.get("buzzer_poll", "0.5"))
|
||||
poll_interval = float(cfg["poll_interval"])
|
||||
|
||||
log.info(
|
||||
"Bridge brightness daemon started | "
|
||||
"buzzer_poll=%ss lux_poll=%ss cooldown=%ss night≤%slux dusk≤%slux",
|
||||
buzz_interval, poll_interval, cfg["buzzer_cooldown"],
|
||||
cfg["lux_night_max"], cfg["lux_dusk_max"],
|
||||
)
|
||||
|
||||
current_level: int = -1 # last level written to sysfs
|
||||
cooldown_until: float = 0.0 # monotonic time when ALERT cooldown ends
|
||||
buzzer_was_on: bool = False
|
||||
|
||||
# Hysteresis state — track how many consecutive polls agree on a new ambient target
|
||||
pending_ambient: int = -1
|
||||
pending_count: int = 0
|
||||
|
||||
# Buzzer is checked every buzzer_poll (0.5s); lux/ambient only every poll_interval (2s)
|
||||
last_lux_poll: float = -999.0 # force first lux read
|
||||
lux: Optional[int] = None # last successful lux read (reused between lux polls)
|
||||
target, mode = 1, "NIGHT" # fallback for step calc before first ambient target
|
||||
|
||||
while True:
|
||||
# ── Config reload on SIGHUP ──────────────────────────────────────────
|
||||
if _reload_flag["pending"]:
|
||||
cfg = load_config()
|
||||
BACKLIGHT = Path(cfg["backlight_path"])
|
||||
BUZZER = Path(cfg["buzzer_path"])
|
||||
LUX = Path(cfg["lux_path"])
|
||||
_reload_flag["pending"] = False
|
||||
pending_ambient = -1
|
||||
pending_count = 0
|
||||
buzz_interval = float(cfg.get("buzzer_poll", "0.5"))
|
||||
poll_interval = float(cfg["poll_interval"])
|
||||
log.info("Config reloaded (SIGHUP)")
|
||||
|
||||
now = time.monotonic()
|
||||
buzzer_raw = _read_int(BUZZER, default=0) or 0
|
||||
buzzer = buzzer_raw > 0
|
||||
override = read_override()
|
||||
|
||||
# Read lux only every poll_interval seconds
|
||||
if now - last_lux_poll >= poll_interval:
|
||||
last_lux_poll = now
|
||||
lux = _read_int(LUX, default=None)
|
||||
|
||||
# ── Determine target level and mode name ─────────────────────────────
|
||||
immediate = False # skip smooth stepping when True
|
||||
|
||||
if override is not None:
|
||||
target, mode = override, "OVERRIDE"
|
||||
immediate = True
|
||||
|
||||
elif buzzer:
|
||||
target, mode = int(cfg["level_alert"]), "ALERT"
|
||||
cooldown_until = now + int(cfg["buzzer_cooldown"])
|
||||
immediate = True # alarm: go to max brightness instantly
|
||||
if not buzzer_was_on:
|
||||
log.info("ALERT: buzzer ON → brightness %d (immediate)", target)
|
||||
|
||||
elif now < cooldown_until:
|
||||
target, mode = int(cfg["level_alert"]), "COOLDOWN"
|
||||
immediate = True # hold alert level until cooldown expires
|
||||
if buzzer_was_on:
|
||||
remaining = cooldown_until - now
|
||||
log.info("COOLDOWN: buzzer OFF → hold brightness %d for %.0fs",
|
||||
target, remaining)
|
||||
|
||||
else:
|
||||
# Ambient mode — lux controls level (use last lux read)
|
||||
if lux is None:
|
||||
# Sensor not read yet or unreadable — keep current level
|
||||
buzzer_was_on = buzzer
|
||||
time.sleep(buzz_interval)
|
||||
continue
|
||||
|
||||
ambient_target, mode = lux_to_level(lux, cfg)
|
||||
|
||||
# Hysteresis: commit new ambient level only after N consecutive
|
||||
# polls agree (avoids flickering near a lux threshold)
|
||||
if ambient_target == pending_ambient:
|
||||
pending_count += 1
|
||||
else:
|
||||
pending_ambient = ambient_target
|
||||
pending_count = 1
|
||||
|
||||
if pending_count >= int(cfg["hysteresis_polls"]):
|
||||
target = ambient_target
|
||||
else:
|
||||
# Not stable yet — keep current level if we have one
|
||||
target = current_level if current_level > 0 else ambient_target
|
||||
|
||||
if buzzer_was_on and not buzzer and now >= cooldown_until:
|
||||
log.info("COOLDOWN done → %s | lux=%d | brightness=%d", mode, lux, target)
|
||||
|
||||
# ── Smooth stepping vs immediate ─────────────────────────────────────
|
||||
# Ambient changes step one level at a time to avoid harsh
|
||||
# brightness jumps that could disrupt night-adapted vision.
|
||||
# Alarm and override changes are immediate (safety requirement).
|
||||
if immediate or current_level < 0:
|
||||
step = target
|
||||
elif target > current_level:
|
||||
step = current_level + 1
|
||||
elif target < current_level:
|
||||
step = current_level - 1
|
||||
else:
|
||||
step = current_level # no change needed
|
||||
|
||||
# ── Write only when level actually changes ───────────────────────────
|
||||
if step != current_level:
|
||||
if _write_int(BACKLIGHT, step):
|
||||
log.info(
|
||||
"Brightness %s→%d | mode=%-10s | lux=%s",
|
||||
current_level if current_level >= 0 else "—",
|
||||
step, mode,
|
||||
lux if lux is not None else "N/A",
|
||||
)
|
||||
current_level = step
|
||||
|
||||
write_state(mode, current_level, lux, buzzer, override is not None)
|
||||
buzzer_was_on = buzzer
|
||||
time.sleep(buzz_interval)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
try:
|
||||
main()
|
||||
except KeyboardInterrupt:
|
||||
log.info("Stopped by keyboard interrupt.")
|
||||
sys.exit(0)
|
||||
Reference in New Issue
Block a user