added backlight and fixed outbound
This commit is contained in:
@@ -275,6 +275,13 @@ def api_set_settings():
|
||||
if not isinstance(val, str) or not val.strip():
|
||||
abort(400, "password required")
|
||||
val = val.strip()
|
||||
if k == "webPort":
|
||||
try:
|
||||
val = int(val)
|
||||
except (TypeError, ValueError):
|
||||
abort(400, "invalid web port")
|
||||
if val < 1024 or val > 65535:
|
||||
abort(400, "web port must be 1024–65535")
|
||||
st[k] = val
|
||||
cfg["settings"] = st
|
||||
save_config(cfg)
|
||||
|
||||
+4
-1
@@ -18,7 +18,7 @@ from api import api_bp
|
||||
from auth import auth_bp, load_or_create_secret, login_required
|
||||
from config_store import list_icons, load_config
|
||||
from network import network_bp
|
||||
from system_info import record_request, system_bp
|
||||
from system_info import apply_saved_brightness, record_request, system_bp
|
||||
from updater import current_version, updater_bp
|
||||
|
||||
|
||||
@@ -38,6 +38,9 @@ def create_app():
|
||||
app.register_blueprint(updater_bp)
|
||||
app.register_blueprint(system_bp)
|
||||
|
||||
# Restore LCD brightness from config (best-effort; needs bt-brightness + sudoers).
|
||||
apply_saved_brightness()
|
||||
|
||||
@app.before_request
|
||||
def _request_start():
|
||||
g._req_start = time.time()
|
||||
|
||||
@@ -0,0 +1,83 @@
|
||||
#!/usr/bin/env bash
|
||||
#
|
||||
# bt-brightness - LCD backlight control for ButtonTask.
|
||||
#
|
||||
# Usage:
|
||||
# bt-brightness status
|
||||
# bt-brightness set <0-100>
|
||||
#
|
||||
# Invoked through sudo by the web configurator / Qt UI.
|
||||
# Prefers /sys/class/backlight/backlight; otherwise the first device with
|
||||
# max_brightness > 0.
|
||||
set -euo pipefail
|
||||
|
||||
err() { echo "bt-brightness: $*" >&2; exit 1; }
|
||||
|
||||
BACKLIGHT_ROOT="/sys/class/backlight"
|
||||
|
||||
find_device() {
|
||||
local d preferred="$BACKLIGHT_ROOT/backlight" max
|
||||
if [ -d "$preferred" ] && [ -f "$preferred/max_brightness" ]; then
|
||||
max="$(cat "$preferred/max_brightness" 2>/dev/null || echo 0)"
|
||||
if [ "$max" -gt 0 ] 2>/dev/null; then
|
||||
echo "$preferred"
|
||||
return 0
|
||||
fi
|
||||
fi
|
||||
for d in "$BACKLIGHT_ROOT"/*; do
|
||||
[ -d "$d" ] || continue
|
||||
[ -f "$d/max_brightness" ] || continue
|
||||
max="$(cat "$d/max_brightness" 2>/dev/null || echo 0)"
|
||||
if [ "$max" -gt 0 ] 2>/dev/null; then
|
||||
echo "$d"
|
||||
return 0
|
||||
fi
|
||||
done
|
||||
return 1
|
||||
}
|
||||
|
||||
emit_status() {
|
||||
local dev="$1" max cur percent
|
||||
max="$(cat "$dev/max_brightness")"
|
||||
if [ -f "$dev/actual_brightness" ]; then
|
||||
cur="$(cat "$dev/actual_brightness")"
|
||||
else
|
||||
cur="$(cat "$dev/brightness")"
|
||||
fi
|
||||
if [ "$max" -gt 0 ]; then
|
||||
percent=$(( (cur * 100 + max / 2) / max ))
|
||||
else
|
||||
percent=0
|
||||
fi
|
||||
[ "$percent" -gt 100 ] && percent=100
|
||||
[ "$percent" -lt 0 ] && percent=0
|
||||
printf '{"device":"%s","brightness":%s,"max":%s,"percent":%s}\n' \
|
||||
"$(basename "$dev")" "$cur" "$max" "$percent"
|
||||
}
|
||||
|
||||
set_percent() {
|
||||
local percent="$1" dev max value
|
||||
[[ "$percent" =~ ^[0-9]+$ ]] || err "percent must be an integer 0-100"
|
||||
[ "$percent" -ge 0 ] && [ "$percent" -le 100 ] || err "percent must be 0-100"
|
||||
dev="$(find_device)" || err "no backlight device found"
|
||||
max="$(cat "$dev/max_brightness")"
|
||||
[ "$max" -gt 0 ] || err "max_brightness is 0"
|
||||
value=$(( (percent * max + 50) / 100 ))
|
||||
[ "$value" -lt 0 ] && value=0
|
||||
[ "$value" -gt "$max" ] && value="$max"
|
||||
# Avoid completely blank panel unless explicitly requested as 0.
|
||||
printf '%s\n' "$value" > "$dev/brightness"
|
||||
emit_status "$dev"
|
||||
}
|
||||
|
||||
cmd_status() {
|
||||
local dev
|
||||
dev="$(find_device)" || err "no backlight device found"
|
||||
emit_status "$dev"
|
||||
}
|
||||
|
||||
case "${1:-}" in
|
||||
status) cmd_status ;;
|
||||
set) [ "$#" -eq 2 ] || err "usage: bt-brightness set <0-100>"; set_percent "$2" ;;
|
||||
*) err "usage: bt-brightness {status|set <0-100>}" ;;
|
||||
esac
|
||||
@@ -101,6 +101,37 @@ switch_to() {
|
||||
ln -sfn "$1" "$CURRENT"
|
||||
}
|
||||
|
||||
# Install privileged helpers + sudoers from the staged package so OTA can
|
||||
# deliver new bt-* scripts (e.g. bt-brightness) without a full redeploy.
|
||||
sync_helpers() {
|
||||
local dest="$1"
|
||||
local src="$dest/webconfig/scripts"
|
||||
local s name tmp_sudoers
|
||||
[ -d "$src" ] || { log "sync_helpers: no $src"; return 0; }
|
||||
mkdir -p "$ROOT/scripts"
|
||||
for s in "$src"/bt-*; do
|
||||
[ -f "$s" ] || continue
|
||||
name="$(basename "$s")"
|
||||
install -m 0755 "$s" "$ROOT/scripts/$name"
|
||||
chown root:root "$ROOT/scripts/$name" 2>/dev/null || true
|
||||
sed -i 's/\r$//' "$ROOT/scripts/$name" 2>/dev/null || true
|
||||
log "synced helper $name"
|
||||
done
|
||||
if [ -f "$src/buttontask.sudoers" ]; then
|
||||
tmp_sudoers="$(mktemp)"
|
||||
cp "$src/buttontask.sudoers" "$tmp_sudoers"
|
||||
sed -i 's/\r$//' "$tmp_sudoers" 2>/dev/null || true
|
||||
chmod 0440 "$tmp_sudoers"
|
||||
if visudo -cf "$tmp_sudoers" >/dev/null 2>&1; then
|
||||
install -m 0440 "$tmp_sudoers" /etc/sudoers.d/buttontask
|
||||
log "synced sudoers"
|
||||
else
|
||||
log "sudoers validation failed — kept previous /etc/sudoers.d/buttontask"
|
||||
fi
|
||||
rm -f "$tmp_sudoers"
|
||||
fi
|
||||
}
|
||||
|
||||
cmd_stage() {
|
||||
local pkg="$1"
|
||||
[ -f "$pkg" ] || { write_status error "package not found"; exit 1; }
|
||||
@@ -138,6 +169,9 @@ cmd_stage() {
|
||||
chmod -R u=rwX,go=rX "$dest" 2>/dev/null || true
|
||||
rm -f "$pkg"
|
||||
|
||||
write_status helpers "установка хелперов"
|
||||
sync_helpers "$dest"
|
||||
|
||||
write_status switching "$ver"
|
||||
switch_to "$dest"
|
||||
restart_services
|
||||
@@ -167,6 +201,14 @@ cmd_rollback() {
|
||||
write_status rollback_done "откат выполнен"
|
||||
}
|
||||
|
||||
cmd_sync_helpers() {
|
||||
local dest
|
||||
dest="$(readlink -f "$CURRENT" 2>/dev/null || true)"
|
||||
[ -n "$dest" ] && [ -d "$dest" ] || { echo "no current version" >&2; exit 1; }
|
||||
sync_helpers "$dest"
|
||||
echo "helpers synced from $dest"
|
||||
}
|
||||
|
||||
cmd_remove() {
|
||||
local name="$1"
|
||||
[[ "$name" =~ ^[A-Za-z0-9._-]+$ ]] || { echo "invalid version name" >&2; exit 1; }
|
||||
@@ -184,8 +226,9 @@ cmd_remove() {
|
||||
}
|
||||
|
||||
case "${1:-}" in
|
||||
stage) [ "$#" -ge 2 ] || { echo "usage: bt-update stage <pkg>"; exit 2; }; cmd_stage "$2" ;;
|
||||
rollback) cmd_rollback ;;
|
||||
remove) [ "$#" -ge 2 ] || { echo "usage: bt-update remove <name>"; exit 2; }; cmd_remove "$2" ;;
|
||||
*) echo "usage: bt-update {stage <pkg>|rollback|remove <name>}"; exit 2 ;;
|
||||
stage) [ "$#" -ge 2 ] || { echo "usage: bt-update stage <pkg>"; exit 2; }; cmd_stage "$2" ;;
|
||||
rollback) cmd_rollback ;;
|
||||
sync-helpers) cmd_sync_helpers ;;
|
||||
remove) [ "$#" -ge 2 ] || { echo "usage: bt-update remove <name>"; exit 2; }; cmd_remove "$2" ;;
|
||||
*) echo "usage: bt-update {stage <pkg>|rollback|sync-helpers|remove <name>}"; exit 2 ;;
|
||||
esac
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
# buttontask ALL=(root) NOPASSWD: /opt/buttontask/scripts/bt-settime
|
||||
# buttontask ALL=(root) NOPASSWD: /opt/buttontask/scripts/bt-reboot
|
||||
# buttontask ALL=(root) NOPASSWD: /opt/buttontask/scripts/bt-access
|
||||
# buttontask ALL=(root) NOPASSWD: /opt/buttontask/scripts/bt-brightness
|
||||
# EOF
|
||||
# sudo chmod 0440 /etc/sudoers.d/buttontask
|
||||
# sudo visudo -cf /etc/sudoers.d/buttontask # validate
|
||||
@@ -23,3 +24,4 @@ buttontask ALL=(root) NOPASSWD: /opt/buttontask/scripts/bt-service
|
||||
buttontask ALL=(root) NOPASSWD: /opt/buttontask/scripts/bt-settime
|
||||
buttontask ALL=(root) NOPASSWD: /opt/buttontask/scripts/bt-reboot
|
||||
buttontask ALL=(root) NOPASSWD: /opt/buttontask/scripts/bt-access
|
||||
buttontask ALL=(root) NOPASSWD: /opt/buttontask/scripts/bt-brightness
|
||||
|
||||
+59
-1
@@ -486,6 +486,34 @@ async function loadAccess() {
|
||||
}
|
||||
}
|
||||
|
||||
function renderBrightness(s) {
|
||||
const el = $("#brightness-status");
|
||||
const slider = $("#brightness-slider");
|
||||
const val = $("#brightness-val");
|
||||
if (!el) return;
|
||||
if (!s || s.ok === false) {
|
||||
el.innerHTML = `<p class="warn">${(s && s.error) || "подсветка недоступна"}</p>`;
|
||||
return;
|
||||
}
|
||||
const pct = s.percent != null ? s.percent : 100;
|
||||
el.textContent = `Сейчас: ${pct}%` +
|
||||
(s.device ? ` (${s.device}: ${s.brightness}/${s.max})` : "") +
|
||||
(s.saved != null ? ` · сохранено: ${s.saved}%` : "");
|
||||
if (slider && !slider.dataset.touched) {
|
||||
slider.value = pct;
|
||||
if (val) val.textContent = pct + "%";
|
||||
}
|
||||
}
|
||||
|
||||
async function loadBrightness() {
|
||||
try {
|
||||
const s = await api("GET", "/api/system/brightness");
|
||||
renderBrightness(s);
|
||||
} catch (e) {
|
||||
renderBrightness({ ok: false, error: e.message });
|
||||
}
|
||||
}
|
||||
|
||||
async function setAccess(action, value) {
|
||||
const r = await api("POST", "/api/system/access", { action, value });
|
||||
if (action !== "webPort") renderAccessStatus(r);
|
||||
@@ -505,6 +533,7 @@ async function loadSystem() {
|
||||
dtInput.value = stats.clock.replace(" ", "T").slice(0, 16);
|
||||
}
|
||||
loadAccess();
|
||||
loadBrightness();
|
||||
} catch (e) {
|
||||
const el = $("#sys-stats");
|
||||
if (el) el.textContent = e.message;
|
||||
@@ -526,6 +555,31 @@ $("#web-port")?.addEventListener("input", (e) => {
|
||||
e.target.dataset.touched = "1";
|
||||
});
|
||||
|
||||
$("#brightness-slider")?.addEventListener("input", (e) => {
|
||||
e.target.dataset.touched = "1";
|
||||
const val = $("#brightness-val");
|
||||
if (val) val.textContent = e.target.value + "%";
|
||||
});
|
||||
$("#brightness-refresh")?.addEventListener("click", () => {
|
||||
const slider = $("#brightness-slider");
|
||||
if (slider) delete slider.dataset.touched;
|
||||
loadBrightness();
|
||||
});
|
||||
$("#brightness-save")?.addEventListener("click", async () => {
|
||||
const percent = parseInt($("#brightness-slider")?.value, 10);
|
||||
if (Number.isNaN(percent) || percent < 0 || percent > 100) {
|
||||
flash("Некорректная яркость", true);
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const s = await api("POST", "/api/system/brightness", { percent });
|
||||
const slider = $("#brightness-slider");
|
||||
if (slider) delete slider.dataset.touched;
|
||||
renderBrightness(s);
|
||||
flash(`Яркость: ${s.percent}%`);
|
||||
} catch (e) { flash(e.message, true); }
|
||||
});
|
||||
|
||||
$("#sys-set-time")?.addEventListener("click", async () => {
|
||||
const raw = $("#sys-datetime")?.value;
|
||||
if (!raw) { flash("Укажите дату и время", true); return; }
|
||||
@@ -572,9 +626,13 @@ $("#save-ssh-port")?.addEventListener("click", async () => {
|
||||
flash("Порт SSH обновлён");
|
||||
} catch (e) { flash(e.message, true); }
|
||||
});
|
||||
const MIN_USER_PORT = 1024;
|
||||
$("#save-web-port")?.addEventListener("click", async () => {
|
||||
const port = parseInt($("#web-port").value, 10);
|
||||
if (!port || port < 1 || port > 65535) { flash("Некорректный веб-порт", true); return; }
|
||||
if (!port || port < MIN_USER_PORT || port > 65535) {
|
||||
flash(`Веб-порт должен быть от ${MIN_USER_PORT} до 65535 (1–1023 зарезервированы в Linux)`, true);
|
||||
return;
|
||||
}
|
||||
if (!confirm(`Сменить веб-порт на ${port}? Веб-сервис перезапустится.`)) return;
|
||||
try {
|
||||
await setAccess("webPort", port);
|
||||
|
||||
@@ -200,13 +200,16 @@ def access_status():
|
||||
return status
|
||||
|
||||
|
||||
MIN_USER_PORT = 1024
|
||||
|
||||
|
||||
def set_web_port(port):
|
||||
try:
|
||||
port = int(port)
|
||||
except (TypeError, ValueError):
|
||||
raise ValueError("invalid web port")
|
||||
if port < 1 or port > 65535:
|
||||
raise ValueError("invalid web port")
|
||||
if port < MIN_USER_PORT or port > 65535:
|
||||
raise ValueError(f"web port must be {MIN_USER_PORT}–65535")
|
||||
cfg = load_config()
|
||||
cfg.setdefault("settings", {})["webPort"] = port
|
||||
save_config(cfg)
|
||||
@@ -219,6 +222,72 @@ def set_web_port(port):
|
||||
return port
|
||||
|
||||
|
||||
def _run_brightness(*args, timeout=10):
|
||||
script = str(scripts_dir() / "bt-brightness")
|
||||
proc = subprocess.run(
|
||||
["sudo", "-n", script, *args],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
timeout=timeout,
|
||||
check=False,
|
||||
)
|
||||
for line in reversed((proc.stdout or "").splitlines()):
|
||||
line = line.strip()
|
||||
if line.startswith("{") and line.endswith("}"):
|
||||
return json.loads(line)
|
||||
if proc.returncode != 0:
|
||||
err = (proc.stderr or proc.stdout or "bt-brightness failed").strip()
|
||||
raise RuntimeError(err)
|
||||
raise RuntimeError((proc.stdout or "bt-brightness returned no status").strip())
|
||||
|
||||
|
||||
def brightness_status():
|
||||
live = _run_brightness("status")
|
||||
cfg = load_config()
|
||||
saved = cfg.get("settings", {}).get("brightness")
|
||||
try:
|
||||
saved = int(saved) if saved is not None else None
|
||||
except (TypeError, ValueError):
|
||||
saved = None
|
||||
live["saved"] = saved
|
||||
live["ok"] = True
|
||||
return live
|
||||
|
||||
|
||||
def set_brightness(percent):
|
||||
try:
|
||||
percent = int(percent)
|
||||
except (TypeError, ValueError):
|
||||
raise ValueError("percent must be an integer 0-100")
|
||||
if percent < 0 or percent > 100:
|
||||
raise ValueError("percent must be 0-100")
|
||||
live = _run_brightness("set", str(percent))
|
||||
cfg = load_config()
|
||||
cfg.setdefault("settings", {})["brightness"] = percent
|
||||
save_config(cfg)
|
||||
live["saved"] = percent
|
||||
live["ok"] = True
|
||||
return live
|
||||
|
||||
|
||||
def apply_saved_brightness():
|
||||
"""Restore brightness from config at service start. No-op if unset."""
|
||||
cfg = load_config()
|
||||
saved = cfg.get("settings", {}).get("brightness")
|
||||
if saved is None:
|
||||
return None
|
||||
try:
|
||||
percent = int(saved)
|
||||
except (TypeError, ValueError):
|
||||
return None
|
||||
if percent < 0 or percent > 100:
|
||||
return None
|
||||
try:
|
||||
return _run_brightness("set", str(percent))
|
||||
except Exception:
|
||||
return None
|
||||
|
||||
|
||||
@system_bp.route("/api/system/stats", methods=["GET"])
|
||||
@login_required
|
||||
def api_system_stats():
|
||||
@@ -261,6 +330,27 @@ def api_access_set():
|
||||
return jsonify({"ok": False, "error": str(e)}), 500
|
||||
|
||||
|
||||
@system_bp.route("/api/system/brightness", methods=["GET"])
|
||||
@login_required
|
||||
def api_brightness_get():
|
||||
try:
|
||||
return jsonify(brightness_status())
|
||||
except RuntimeError as e:
|
||||
return jsonify({"ok": False, "error": str(e)}), 500
|
||||
|
||||
|
||||
@system_bp.route("/api/system/brightness", methods=["POST"])
|
||||
@login_required
|
||||
def api_brightness_set():
|
||||
data = request.get_json(force=True, silent=True) or {}
|
||||
try:
|
||||
return jsonify(set_brightness(data.get("percent")))
|
||||
except ValueError as e:
|
||||
abort(400, str(e))
|
||||
except RuntimeError as e:
|
||||
return jsonify({"ok": False, "error": str(e)}), 500
|
||||
|
||||
|
||||
@system_bp.route("/api/system/time", methods=["GET"])
|
||||
@login_required
|
||||
def api_system_time_get():
|
||||
|
||||
@@ -274,6 +274,22 @@
|
||||
<div id="sys-stats" class="sys-stats">Загрузка…</div>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<div class="card-head">
|
||||
<h2>Яркость экрана</h2>
|
||||
<button id="brightness-refresh" class="ghost">↻ Обновить</button>
|
||||
</div>
|
||||
<p class="hint">Подсветка LCD. Снижение яркости уменьшает нагрев. Значение сохраняется и применяется при старте.</p>
|
||||
<div id="brightness-status" class="sys-clock">Загрузка…</div>
|
||||
<div class="row">
|
||||
<label style="flex:1">Яркость
|
||||
<input type="range" id="brightness-slider" min="1" max="100" value="100">
|
||||
</label>
|
||||
<span id="brightness-val">100%</span>
|
||||
<button id="brightness-save" class="primary">Применить</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<h2>Дата и время</h2>
|
||||
<p class="hint">Без сети NTP не работает — задайте время вручную. Сохраняется в системные часы и RTC (если есть).</p>
|
||||
@@ -318,9 +334,10 @@
|
||||
</label>
|
||||
<label>Порт веб-конфигуратора
|
||||
<div class="row">
|
||||
<input type="number" id="web-port" min="1" max="65535" value="8080">
|
||||
<input type="number" id="web-port" min="1024" max="65535" value="8080">
|
||||
<button id="save-web-port">Сохранить и перезапустить веб</button>
|
||||
</div>
|
||||
<span class="hint">Порты 1–1023 зарезервированы в Linux и недоступны без root.</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user