device-motorola-rtwo: package the dashboard and the 5 GHz WiFi workaround
Both lived only in the rootfs (/usr/local, /etc). Move them into the device package: - rtwo-dashboard: sway always runs with the system dashboard on the panel (DSI-1); the launcher follows the DP-1 HPD and opens a login console on the external monitor while one is connected. The console runs login as a session leader (setsid -c) and reopens the tty on every loop, since login's vhangup() otherwise leaves the loop with dead fds. Files go to /usr/libexec and /usr/share/rtwo-dashboard. - rtwo-wifi-5ghz-scan: unsticks wpa_supplicant after boot (ath12k). The network now comes from NetworkManager (first WiFi connection) or from /etc/default/rtwo-wifi-5ghz-scan instead of a hardcoded SSID. Both services are enabled through multi-user.target.wants, like rtwo-panel-revive. New dependencies: sway, foot, seatd-launch, python3, font-jetbrains-mono-nerd, font-noto-emoji, iw, networkmanager-cli. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DnHwDU5yL8L6qgPEN5CV5m
This commit is contained in:
@@ -2,7 +2,7 @@
|
||||
maintainer="snlavaise <snlavaise@gmail.com>"
|
||||
pkgname=device-motorola-rtwo
|
||||
pkgver=1
|
||||
pkgrel=36
|
||||
pkgrel=37
|
||||
pkgdesc="Motorola Moto Edge 40 Pro"
|
||||
url="https://postmarketos.org"
|
||||
arch="aarch64"
|
||||
@@ -14,12 +14,20 @@ depends="
|
||||
firmware-motorola-rtwo-adsp
|
||||
firmware-motorola-rtwo-cdsp
|
||||
firmware-motorola-rtwo-gpu
|
||||
font-jetbrains-mono-nerd
|
||||
font-noto-emoji
|
||||
foot
|
||||
iw
|
||||
linux-firmware-ath12k
|
||||
linux-firmware-qcom
|
||||
linux-firmware-rtw88
|
||||
linux-postmarketos-qcom-sm8550
|
||||
networkmanager-cli
|
||||
postmarketos-base
|
||||
python3
|
||||
seatd-launch
|
||||
soc-qcom
|
||||
sway
|
||||
"
|
||||
makedepends="devicepkg-dev"
|
||||
source="
|
||||
@@ -32,6 +40,14 @@ source="
|
||||
logind-power-key.conf
|
||||
rtwo-panel-revive.sh
|
||||
rtwo-panel-revive.service
|
||||
rtwo-dashboard-launcher.py
|
||||
rtwo-dashboard.py
|
||||
rtwo-dashboard-sway.config
|
||||
rtwo-dashboard-foot.ini
|
||||
rtwo-dashboard-consola.ini
|
||||
rtwo-dashboard.service
|
||||
rtwo-wifi-5ghz-scan.sh
|
||||
rtwo-wifi-5ghz-scan.service
|
||||
"
|
||||
options="!archcheck !check"
|
||||
|
||||
@@ -114,6 +130,34 @@ package() {
|
||||
install -d "$pkgdir"/usr/lib/systemd/system/multi-user.target.wants
|
||||
ln -s ../rtwo-panel-revive.service \
|
||||
"$pkgdir"/usr/lib/systemd/system/multi-user.target.wants/rtwo-panel-revive.service
|
||||
|
||||
# Dashboard: sway siempre, con el dashboard en el panel (DSI-1) y una
|
||||
# consola de login en el monitor (DP-1) mientras haya uno conectado. El
|
||||
# launcher sigue el HPD de DP-1 y abre/cierra la consola; ver su docstring.
|
||||
install -Dm755 "$srcdir"/rtwo-dashboard-launcher.py \
|
||||
"$pkgdir"/usr/libexec/rtwo-dashboard-launcher
|
||||
install -Dm755 "$srcdir"/rtwo-dashboard.py \
|
||||
"$pkgdir"/usr/libexec/rtwo-dashboard
|
||||
install -Dm644 "$srcdir"/rtwo-dashboard-sway.config \
|
||||
"$pkgdir"/usr/share/rtwo-dashboard/sway.config
|
||||
install -Dm644 "$srcdir"/rtwo-dashboard-foot.ini \
|
||||
"$pkgdir"/usr/share/rtwo-dashboard/foot.ini
|
||||
install -Dm644 "$srcdir"/rtwo-dashboard-consola.ini \
|
||||
"$pkgdir"/usr/share/rtwo-dashboard/consola.ini
|
||||
install -Dm644 "$srcdir"/rtwo-dashboard.service \
|
||||
"$pkgdir"/usr/lib/systemd/system/rtwo-dashboard.service
|
||||
ln -s ../rtwo-dashboard.service \
|
||||
"$pkgdir"/usr/lib/systemd/system/multi-user.target.wants/rtwo-dashboard.service
|
||||
|
||||
# WiFi 5 GHz tras el arranque: destraba el escaneo de wpa_supplicant
|
||||
# (workaround ath12k). La red sale de NetworkManager o de
|
||||
# /etc/default/rtwo-wifi-5ghz-scan; ver el script.
|
||||
install -Dm755 "$srcdir"/rtwo-wifi-5ghz-scan.sh \
|
||||
"$pkgdir"/usr/libexec/rtwo-wifi-5ghz-scan
|
||||
install -Dm644 "$srcdir"/rtwo-wifi-5ghz-scan.service \
|
||||
"$pkgdir"/usr/lib/systemd/system/rtwo-wifi-5ghz-scan.service
|
||||
ln -s ../rtwo-wifi-5ghz-scan.service \
|
||||
"$pkgdir"/usr/lib/systemd/system/multi-user.target.wants/rtwo-wifi-5ghz-scan.service
|
||||
}
|
||||
|
||||
sha512sums="
|
||||
@@ -126,4 +170,12 @@ cb8ea3e0da0219fdf77f6def6d7a77157a4ef152cc434e3d282457647784b05e7152cf1028d00a55
|
||||
bab2fbd6078d9ac342708a1e85b7f5d5cff5e1105b5bd633517bf27f9a61499b9903c9aa17d0142c82f8cd874cd9ac03d1ddb33b8c66e40094a7271dd54c0502 logind-power-key.conf
|
||||
59db456c9ba57352aa8bff0e8ca73bd5b1c5c7f6f9c57e382e5bdd68791c9324d705210e77aa4e945e3869ab11dc1066e1a290c3750a27772c89f83731d28609 rtwo-panel-revive.sh
|
||||
8e374a7304f8648d33d9087149e53efc5b09ffe1f7bc07b8b10521b7b1163e4787b56ecf3d2871629b1cfcc0037c1c15d084b514412e5e0a90ff66fd8e83b088 rtwo-panel-revive.service
|
||||
3dfc80a0399e06c9f9708847bfc3e270202e26ecac48e60c0ef8d98a98c9a92b20febb91a90201b6ebd00141c49ec7bd6ad052acfdba587749b71c1f3dd7798c rtwo-dashboard-launcher.py
|
||||
f9f51c52280e443010d3664bf390e5b9664c0f8b82bec39cd3e36b2fac742b27d6d2bbc32ee4a48d0296c2919eeee7b4728313cb2da69b598e6b593ea7b4a8a9 rtwo-dashboard.py
|
||||
31c20c566be562ff6755cfb24915a94d9b3ad95a45e73fe71eda7e74851dd5fbfe20416052bda8adaa174243be9c63d098de9063c092637ec51061937f18ca3a rtwo-dashboard-sway.config
|
||||
92a3b263af1aed7f8f808ecc1f7815dc0951f21723c1d0edaeb5b57ae78e505371396824901a97d4521c710fc2f670ef71579bb96b35835726a381648adb8e21 rtwo-dashboard-foot.ini
|
||||
c38b7942a7697788ffc62c82f560f6cf5e2f963b1d3ca69de8f9b2d1bfe8143e802cc19e782d01cb12c5ae0347e5a082fad2774e777b4a0696e2070e22004bb6 rtwo-dashboard-consola.ini
|
||||
5fb089bdb6079ec790fb7c1aafad9c3f1a9aa90731c96d42c5bb788b7b44d485bc7a07befa86fba06eafbde9c38d4aaafb56bf752ec289ea5f817465982ffcbd rtwo-dashboard.service
|
||||
259ef3adc35e26f263e572a0937e4e701969eaa0c9c6a85cb13f66f3acc3e0395cf5639f882a0196a9881c3cdd7ac505a33f998296af7d11d34fff6e8dddd747 rtwo-wifi-5ghz-scan.sh
|
||||
0c1bc164f71c10c8b24907d7c9ce91eba8c1983d7d00c5fc4e8c7d3d3b700cb746ad5bece5ba796903ffb3728d8ee108fc08fb95bc8dfe5e2c9311fa829de3d3 rtwo-wifi-5ghz-scan.service
|
||||
"
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
# foot para la consola de login en el monitor (DP-1) mientras sway ocupa las pantallas
|
||||
font=JetBrainsMono Nerd Font Mono:pixelsize=20
|
||||
dpi-aware=no
|
||||
pad=4x4
|
||||
|
||||
[colors-dark]
|
||||
background=000000
|
||||
foreground=c0c0c0
|
||||
@@ -0,0 +1,29 @@
|
||||
# rtwo-dashboard v3 (foot dentro de cage, pantalla completa 1080x2400)
|
||||
[main]
|
||||
font=JetBrainsMono Nerd Font Mono:pixelsize=42
|
||||
dpi-aware=no
|
||||
pad=0x24 center
|
||||
term=foot
|
||||
|
||||
[cursor]
|
||||
blink=no
|
||||
|
||||
[colors-dark]
|
||||
background=11111b
|
||||
foreground=cdd6f4
|
||||
regular0=45475a
|
||||
regular1=f38ba8
|
||||
regular2=a6e3a1
|
||||
regular3=f9e2af
|
||||
regular4=89b4fa
|
||||
regular5=f5c2e7
|
||||
regular6=94e2d5
|
||||
regular7=bac2de
|
||||
bright0=585b70
|
||||
bright1=f38ba8
|
||||
bright2=a6e3a1
|
||||
bright3=f9e2af
|
||||
bright4=89b4fa
|
||||
bright5=f5c2e7
|
||||
bright6=94e2d5
|
||||
bright7=a6adc8
|
||||
@@ -0,0 +1,250 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Launcher del rtwo-dashboard (sway, v9 = v6 + espera de DRM + consola solo
|
||||
con DP-1 activa en sway).
|
||||
|
||||
Un solo sway corriendo siempre, con el dashboard en el panel (DSI-1). Cuando
|
||||
hay un monitor en DP-1 (estable DEBOUNCE_SECS) el launcher abre ademas una
|
||||
consola de login (foot + /bin/login) que sway manda al monitor; si DP-1 se
|
||||
desconecta la cierra en el acto y vuelve el foco al dashboard.
|
||||
|
||||
sway no se reinicia por el HPD: entre un sway y otro fbdev retomaba el DP y
|
||||
lo entrenaba contra un sink ya desconectado (training fallido + vblank
|
||||
timeout, el camino que antes reseteaba el telefono). Algunos conversores y
|
||||
TVs sin alimentacion externa bajan el HPD cada minuto o dos.
|
||||
|
||||
v9: sway.config ya no lanza la consola (en v7 lo hacia siempre y sin monitor
|
||||
caia en el panel con el foco, tapando el dashboard). La consola se abre solo
|
||||
cuando sway ya tiene DP-1 activa; si aparece en el panel se cierra.
|
||||
"""
|
||||
import json
|
||||
import glob
|
||||
import os
|
||||
import signal
|
||||
import socket
|
||||
import subprocess
|
||||
import time
|
||||
|
||||
DEBOUNCE_SECS = 3
|
||||
DP_STATUS = "/sys/class/drm/card1-DP-1/status"
|
||||
# el panel (DSI-1) debe existir antes de lanzar sway, o wlroots no enumera
|
||||
# outputs y sway muere
|
||||
DSI_STATUS = "/sys/class/drm/card1-DSI-1/status"
|
||||
DRM_DEV = "/dev/dri/card1"
|
||||
DRM_WAIT_SECS = 60
|
||||
RUNTIME_DIR = "/run/rtwo-dashboard"
|
||||
# libseat de Alpine no trae el backend "builtin": seatd-launch levanta un
|
||||
# seatd temporal solo para sway.
|
||||
CMD = ["seatd-launch", "--", "sway", "-c", "/usr/share/rtwo-dashboard/sway.config"]
|
||||
# login sale solo tras LOGIN_TIMEOUT (60 s) sin teclas: se relanza dentro de la
|
||||
# misma ventana, como en una tty, para no dejar el monitor en negro. Dos trampas:
|
||||
# - login se pone en su propio grupo de procesos y sh -c no hace control de
|
||||
# trabajos -> SIGTTOU, login detenido (estado T) antes del prompt = monitor
|
||||
# negro con solo el cursor. setsid -c lo hace lider de sesion con la tty.
|
||||
# - login hace vhangup() de la tty: los fds heredados del sh quedan colgados y
|
||||
# el siguiente login moria al instante (bucle a ~2000 procesos/s). Por eso se
|
||||
# reabre la tty por nombre en cada vuelta.
|
||||
CONSOLE = ["foot", "-c", "/usr/share/rtwo-dashboard/consola.ini", "-T", "rtwo-consola",
|
||||
"sh", "-c", 't=$(tty); while :; do setsid -c -w sh -c "clear; exec /bin/login"'
|
||||
' <"$t" >"$t" 2>&1; sleep 0.5; done']
|
||||
ENV = {
|
||||
"XDG_RUNTIME_DIR": RUNTIME_DIR,
|
||||
"WLR_RENDERER": "pixman", # sin depender del firmware de la GPU
|
||||
"WLR_LIBINPUT_NO_DEVICES": "1",
|
||||
"LANG": "C.UTF-8",
|
||||
}
|
||||
# clientes que sway lanza con exec: van en su propia sesion (setsid), asi
|
||||
# que matar el grupo de seatd-launch no los alcanza
|
||||
CLIENT_MARK = b"/usr/share/rtwo-dashboard/"
|
||||
|
||||
|
||||
def dp_connected():
|
||||
try:
|
||||
with open(DP_STATUS) as f:
|
||||
return f.read().strip() == "connected"
|
||||
except OSError:
|
||||
return False
|
||||
|
||||
|
||||
def kill_group(proc, timeout=5):
|
||||
if proc is None or proc.poll() is not None:
|
||||
return
|
||||
try:
|
||||
os.killpg(proc.pid, signal.SIGTERM)
|
||||
except ProcessLookupError:
|
||||
return
|
||||
try:
|
||||
proc.wait(timeout)
|
||||
except subprocess.TimeoutExpired:
|
||||
try:
|
||||
os.killpg(proc.pid, signal.SIGKILL)
|
||||
except ProcessLookupError:
|
||||
pass
|
||||
proc.wait()
|
||||
|
||||
|
||||
def kill_clients():
|
||||
"""Matar bucles sh/foot/login de instancias anteriores de sway."""
|
||||
me = os.getpid()
|
||||
for pid in os.listdir("/proc"):
|
||||
if not pid.isdigit() or int(pid) == me:
|
||||
continue
|
||||
try:
|
||||
with open(f"/proc/{pid}/cmdline", "rb") as f:
|
||||
cmd = f.read()
|
||||
except OSError:
|
||||
continue
|
||||
if CLIENT_MARK in cmd and b"sway.config" not in cmd:
|
||||
try:
|
||||
os.kill(int(pid), signal.SIGTERM)
|
||||
except ProcessLookupError:
|
||||
pass
|
||||
|
||||
|
||||
def clean_stale_seatd():
|
||||
"""Borrar /run/seatd.sock si ningun seatd esta vivo."""
|
||||
sock = "/run/seatd.sock"
|
||||
if not os.path.exists(sock):
|
||||
return
|
||||
for pid in os.listdir("/proc"):
|
||||
if pid.isdigit():
|
||||
try:
|
||||
with open(f"/proc/{pid}/comm") as f:
|
||||
if f.read().strip() == "seatd":
|
||||
return
|
||||
except OSError:
|
||||
pass
|
||||
try:
|
||||
os.unlink(sock)
|
||||
except OSError:
|
||||
pass
|
||||
|
||||
|
||||
def wayland_display():
|
||||
"""Socket wayland de sway que acepta conexiones (None si no hay)."""
|
||||
for path in sorted(glob.glob(RUNTIME_DIR + "/wayland-*")):
|
||||
if path.endswith(".lock"):
|
||||
continue
|
||||
s = socket.socket(socket.AF_UNIX)
|
||||
try:
|
||||
s.connect(path)
|
||||
return os.path.basename(path)
|
||||
except OSError:
|
||||
continue
|
||||
finally:
|
||||
s.close()
|
||||
return None
|
||||
|
||||
|
||||
def wait_for_drm():
|
||||
deadline = time.monotonic() + DRM_WAIT_SECS
|
||||
while time.monotonic() < deadline:
|
||||
try:
|
||||
with open(DSI_STATUS) as f:
|
||||
if f.read().strip() and os.path.exists(DRM_DEV):
|
||||
return
|
||||
except OSError:
|
||||
pass
|
||||
time.sleep(0.5)
|
||||
print(f"timeout esperando DSI-1 ({DRM_WAIT_SECS}s); lanzo igual", flush=True)
|
||||
|
||||
|
||||
def sway_ipc(env, *args):
|
||||
"""swaymsg al sway actual; devuelve stdout (None si falla)."""
|
||||
socks = sorted(glob.glob(RUNTIME_DIR + "/sway-ipc.*.sock"), key=os.path.getmtime)
|
||||
if not socks:
|
||||
return None
|
||||
try:
|
||||
r = subprocess.run(["swaymsg", "-r", "-s", socks[-1], *args], env=env,
|
||||
capture_output=True, timeout=5)
|
||||
except subprocess.TimeoutExpired:
|
||||
return None
|
||||
return r.stdout if r.returncode == 0 else None
|
||||
|
||||
|
||||
def dp_active_in_sway(env):
|
||||
out = sway_ipc(env, "-t", "get_outputs")
|
||||
try:
|
||||
return any(o["name"] == "DP-1" and o["active"] for o in json.loads(out))
|
||||
except (TypeError, ValueError, KeyError):
|
||||
return False
|
||||
|
||||
|
||||
def console_on_panel(env):
|
||||
"""True si la ventana rtwo-consola quedo en DSI-1 (nunca deberia)."""
|
||||
out = sway_ipc(env, "-t", "get_tree")
|
||||
try:
|
||||
tree = json.loads(out)
|
||||
except (TypeError, ValueError):
|
||||
return False
|
||||
|
||||
def walk(n):
|
||||
if n.get("name") == "rtwo-consola":
|
||||
return True
|
||||
return any(walk(c) for c in n.get("nodes", []) + n.get("floating_nodes", []))
|
||||
return any(o.get("name") == "DSI-1" and walk(o) for o in tree.get("nodes", []))
|
||||
|
||||
|
||||
def main():
|
||||
os.makedirs(RUNTIME_DIR, mode=0o700, exist_ok=True)
|
||||
env = dict(os.environ, **ENV)
|
||||
sway = console = None
|
||||
state = dp_connected()
|
||||
since = time.monotonic()
|
||||
last_check = 0.0
|
||||
signal.signal(signal.SIGTERM, lambda *_: (_ for _ in ()).throw(SystemExit))
|
||||
kill_clients()
|
||||
try:
|
||||
while True:
|
||||
now = time.monotonic()
|
||||
cur = dp_connected()
|
||||
if cur != state:
|
||||
state, since = cur, now
|
||||
print("DP-1 " + ("conectado" if cur else "desconectado"), flush=True)
|
||||
if sway is not None and sway.poll() is not None:
|
||||
print(f"sway termino con codigo {sway.returncode}", flush=True)
|
||||
sway = None
|
||||
kill_group(console)
|
||||
console = None
|
||||
kill_clients()
|
||||
if sway is None:
|
||||
clean_stale_seatd()
|
||||
for old in glob.glob(RUNTIME_DIR + "/sway-ipc.*.sock"):
|
||||
os.unlink(old)
|
||||
wait_for_drm()
|
||||
print("arrancando sway (dashboard en DSI-1)", flush=True)
|
||||
sway = subprocess.Popen(CMD, env=env, start_new_session=True)
|
||||
time.sleep(2)
|
||||
continue
|
||||
if console is not None and console.poll() is not None:
|
||||
console = None # logout o foot cerrado: se relanza abajo
|
||||
if (console is not None and now - last_check >= 2
|
||||
and (last_check := now) and console_on_panel(env)):
|
||||
print("consola en el panel: la cierro", flush=True)
|
||||
kill_group(console, timeout=2)
|
||||
console = None
|
||||
sway_ipc(env, "workspace", "1")
|
||||
since = now # reintentar tras otro debounce
|
||||
if not state and console is not None:
|
||||
# cerrar ya (sin debounce) para que no se mude al panel
|
||||
kill_group(console, timeout=2)
|
||||
console = None
|
||||
sway_ipc(env, "workspace", "1")
|
||||
elif (state and console is None and now - since >= DEBOUNCE_SECS
|
||||
and dp_active_in_sway(env)):
|
||||
wl = wayland_display()
|
||||
if wl:
|
||||
print("abriendo consola en DP-1", flush=True)
|
||||
console = subprocess.Popen(
|
||||
CONSOLE, env=dict(env, WAYLAND_DISPLAY=wl),
|
||||
start_new_session=True)
|
||||
time.sleep(0.3)
|
||||
except SystemExit:
|
||||
pass
|
||||
finally:
|
||||
kill_group(console, timeout=2)
|
||||
kill_group(sway)
|
||||
kill_clients()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,30 @@
|
||||
# rtwo dashboard sobre sway (2026-09-26, v9)
|
||||
# sway corre siempre. La consola del monitor la abre/cierra rtwo-dashboard-launcher.py
|
||||
# segun el HPD de DP-1 (si se lanzara aca, sin monitor caeria en el panel).
|
||||
# DSI-1 (panel): dashboard. DP-1 (monitor): consola de login, como la tty1.
|
||||
default_border none
|
||||
default_floating_border none
|
||||
focus_follows_mouse no
|
||||
bar mode invisible
|
||||
output DSI-1 pos 0 0 scale 1
|
||||
# cualquier pantalla externa: su modo preferido
|
||||
output DP-1 pos 1080 0 scale 1
|
||||
# Samsung S19D300: CEA 1280x720@60 (VIC 4, 2o DTD de su EDID).
|
||||
# - 1366x768 nativo: msm parte el plano en 2 mezcladores -> 683 px impar -> imagen
|
||||
# corrida en diagonal.
|
||||
# - modeline VESA 1360x768 (vtotal 795, vsync 6) NO es su timing (nativo: vtotal 798,
|
||||
# vsync 3, vbp 24): el monitor/conversor HDMI engancha y suelta -> imagen que
|
||||
# aparece y desaparece y caidas de HPD cada pocos minutos (2026-09-26).
|
||||
output "Samsung Electric Company S19D300 Unknown" pos 1080 0 scale 1 mode 1280x720@60Hz
|
||||
workspace 1 output DSI-1
|
||||
workspace 2 output DP-1
|
||||
assign [title="^rtwo-dashboard$"] workspace 1
|
||||
assign [title="^rtwo-consola$"] workspace 2
|
||||
for_window [title="^rtwo-(consola|dashboard)$"] fullscreen enable
|
||||
# el teclado va siempre a la consola del monitor; el tactil se apaga para que
|
||||
# un toque en el panel no le robe el foco (el dashboard no lo necesita)
|
||||
for_window [title="^rtwo-consola$"] focus
|
||||
no_focus [title="^rtwo-dashboard$"]
|
||||
input type:touch events disabled
|
||||
# dashboard en el panel (se relanza solo si termina)
|
||||
exec sh -c 'while :; do foot -c /usr/share/rtwo-dashboard/foot.ini -T rtwo-dashboard python3 /usr/libexec/rtwo-dashboard; sleep 1; done'
|
||||
@@ -0,0 +1,503 @@
|
||||
#!/usr/bin/env python3
|
||||
"""rtwo-dashboard v3: full-screen system monitor for the Motorola rtwo panel.
|
||||
|
||||
Runs inside the foot terminal, launched full-screen by cage (Wayland kiosk)
|
||||
from rtwo-dashboard-launcher.py, which also handles showing it after a few
|
||||
idle seconds and hiding it on the first key press. This script only draws:
|
||||
square-cornered cards, 24-bit color, gradient bars, history sparklines and color
|
||||
emoji (Noto Color Emoji through foot/fontconfig). Pure stdlib.
|
||||
|
||||
Only emoji with East Asian Width "W" are used (always 2 cells, no variation
|
||||
selector), so the box borders line up.
|
||||
"""
|
||||
import datetime as _dt
|
||||
import os
|
||||
import re
|
||||
import shutil
|
||||
import signal
|
||||
import subprocess
|
||||
import sys
|
||||
import time
|
||||
import unicodedata
|
||||
|
||||
REFRESH_SECS = 1.0
|
||||
HIST = 60
|
||||
|
||||
# Palette (Catppuccin Mocha)
|
||||
BASE = (17, 17, 27)
|
||||
SURF = (49, 50, 68)
|
||||
OVER = (108, 112, 134)
|
||||
SUB = (166, 173, 200)
|
||||
TEXT = (205, 214, 244)
|
||||
BLUE = (137, 180, 250)
|
||||
LAV = (180, 190, 254)
|
||||
SAPH = (116, 199, 236)
|
||||
TEAL = (148, 226, 213)
|
||||
GREEN = (166, 227, 161)
|
||||
YEL = (249, 226, 175)
|
||||
PEACH = (250, 179, 135)
|
||||
RED = (243, 139, 168)
|
||||
MAUVE = (203, 166, 247)
|
||||
PINK = (245, 194, 231)
|
||||
|
||||
ESC = "\033["
|
||||
RST = ESC + "0m"
|
||||
BOLD = ESC + "1m"
|
||||
|
||||
|
||||
def fg(c):
|
||||
return f"{ESC}38;2;{c[0]};{c[1]};{c[2]}m"
|
||||
|
||||
|
||||
def bg(c):
|
||||
return f"{ESC}48;2;{c[0]};{c[1]};{c[2]}m"
|
||||
|
||||
|
||||
ANSI = re.compile(r"\x1b\[[0-9;?]*[A-Za-z]")
|
||||
|
||||
|
||||
def cwidth(ch):
|
||||
o = ord(ch)
|
||||
if o in (0x200D, 0xFE0F, 0xFE0E) or unicodedata.combining(ch):
|
||||
return 0
|
||||
return 2 if unicodedata.east_asian_width(ch) in ("W", "F") else 1
|
||||
|
||||
|
||||
def vlen(s):
|
||||
return sum(cwidth(ch) for ch in ANSI.sub("", s))
|
||||
|
||||
|
||||
def pad(s, width):
|
||||
"""Pad (or trim) visible content to exactly `width` cells."""
|
||||
n = vlen(s)
|
||||
if n <= width:
|
||||
return s + " " * (width - n)
|
||||
out, seen = [], 0
|
||||
for part in re.split(r"(\x1b\[[0-9;?]*[A-Za-z])", s):
|
||||
if part.startswith("\x1b"):
|
||||
out.append(part)
|
||||
continue
|
||||
for ch in part:
|
||||
w = cwidth(ch)
|
||||
if seen + w > width - 1:
|
||||
return "".join(out) + "…" + " " * (width - 1 - seen)
|
||||
out.append(ch)
|
||||
seen += w
|
||||
return "".join(out)
|
||||
|
||||
|
||||
def mix(a, b, t):
|
||||
t = max(0.0, min(1.0, t))
|
||||
return tuple(int(a[i] + (b[i] - a[i]) * t) for i in range(3))
|
||||
|
||||
|
||||
def heat(t):
|
||||
"""0 -> green, 0.6 -> yellow, 1 -> red."""
|
||||
if t < 0.6:
|
||||
return mix(GREEN, YEL, t / 0.6)
|
||||
return mix(YEL, RED, (t - 0.6) / 0.4)
|
||||
|
||||
|
||||
EIGHTHS = " ▏▎▍▌▋▊▉█"
|
||||
|
||||
|
||||
def gbar(pct, width, level=None):
|
||||
"""Gradient bar with 1/8 cell resolution. Cell color follows position."""
|
||||
pct = max(0.0, min(100.0, pct))
|
||||
cells = pct * width / 100.0
|
||||
full = int(cells)
|
||||
part = int((cells - full) * 8)
|
||||
out = []
|
||||
for i in range(width):
|
||||
col = heat(level if level is not None else i / max(1, width - 1))
|
||||
if i < full:
|
||||
out.append(fg(col) + "█")
|
||||
elif i == full and part:
|
||||
out.append(fg(col) + bg(SURF) + EIGHTHS[part] + RST)
|
||||
else:
|
||||
out.append(fg(SURF) + "█")
|
||||
return "".join(out) + RST
|
||||
|
||||
|
||||
SPARK = "▁▂▃▄▅▆▇█"
|
||||
|
||||
|
||||
def spark(values, width, color, top=None):
|
||||
vals = list(values)[-width:]
|
||||
vals = [0.0] * (width - len(vals)) + vals
|
||||
hi = top if top else (max(vals) or 1.0)
|
||||
out = [fg(color)]
|
||||
for v in vals:
|
||||
idx = int(round(max(0.0, min(1.0, v / hi)) * (len(SPARK) - 1)))
|
||||
out.append(SPARK[idx] if v > 0 else fg(SURF) + "▁" + fg(color))
|
||||
return "".join(out) + RST
|
||||
|
||||
|
||||
def card(icon, title, accent, body, width):
|
||||
"""Card with square corners (foot draws stray strokes next to the
|
||||
rounded arc corners at this font size). `body` lines are pre-colored."""
|
||||
inner = width - 4
|
||||
head = f" {icon} {BOLD}{fg(accent)}{title}{RST} "
|
||||
top = (fg(accent) + "┌─" + RST + head + fg(accent)
|
||||
+ "─" * max(0, width - 3 - vlen(head)) + "┐" + RST)
|
||||
lines = [top]
|
||||
for b in body:
|
||||
lines.append(fg(accent) + "│ " + RST + pad(b, inner) + fg(accent) + " │" + RST)
|
||||
lines.append(fg(accent) + "└" + "─" * (width - 2) + "┘" + RST)
|
||||
return lines
|
||||
|
||||
|
||||
def human(n, digits=1):
|
||||
for unit, div in (("T", 1 << 40), ("G", 1 << 30), ("M", 1 << 20), ("K", 1 << 10)):
|
||||
if n >= div:
|
||||
return f"{n / div:.{digits}f}{unit}"
|
||||
return f"{n:.0f}B"
|
||||
|
||||
|
||||
def rate(bps):
|
||||
return human(bps, 1) + "/s" if bps >= 1024 else f"{bps:.0f}B/s"
|
||||
|
||||
|
||||
def read(path, default=None):
|
||||
try:
|
||||
with open(path) as f:
|
||||
return f.read().strip()
|
||||
except OSError:
|
||||
return default
|
||||
|
||||
|
||||
def readint(path, default=None):
|
||||
v = read(path)
|
||||
try:
|
||||
return int(v)
|
||||
except (TypeError, ValueError):
|
||||
return default
|
||||
|
||||
|
||||
def sh(cmd):
|
||||
try:
|
||||
return subprocess.check_output(cmd, stderr=subprocess.DEVNULL, text=True).strip()
|
||||
except (OSError, subprocess.CalledProcessError):
|
||||
return ""
|
||||
|
||||
|
||||
# ---------------------------------------------------------------- data
|
||||
|
||||
|
||||
class Cpu:
|
||||
def __init__(self):
|
||||
self.prev = self._read()
|
||||
self.hist = []
|
||||
|
||||
@staticmethod
|
||||
def _read():
|
||||
out = {}
|
||||
try:
|
||||
with open("/proc/stat") as f:
|
||||
for line in f:
|
||||
if not line.startswith("cpu"):
|
||||
break
|
||||
p = line.split()
|
||||
vals = list(map(int, p[1:8]))
|
||||
idle = vals[3] + vals[4]
|
||||
out[p[0]] = (sum(vals), idle)
|
||||
except OSError:
|
||||
pass
|
||||
return out
|
||||
|
||||
def sample(self):
|
||||
cur = self._read()
|
||||
usage = {}
|
||||
for k, (tot, idle) in cur.items():
|
||||
ptot, pidle = self.prev.get(k, (tot, idle))
|
||||
dt = tot - ptot
|
||||
usage[k] = 0.0 if dt <= 0 else 100.0 * (1 - (idle - pidle) / dt)
|
||||
self.prev = cur
|
||||
total = usage.get("cpu", 0.0)
|
||||
self.hist = (self.hist + [total])[-HIST:]
|
||||
cores = [usage.get(f"cpu{i}", 0.0) for i in range(os.cpu_count() or 8)]
|
||||
return total, cores
|
||||
|
||||
|
||||
def thermal():
|
||||
temps = {}
|
||||
base = "/sys/class/thermal"
|
||||
try:
|
||||
zones = os.listdir(base)
|
||||
except OSError:
|
||||
return temps
|
||||
for z in zones:
|
||||
if not z.startswith("thermal_zone"):
|
||||
continue
|
||||
t = read(f"{base}/{z}/type", "")
|
||||
v = readint(f"{base}/{z}/temp")
|
||||
if v is not None:
|
||||
temps[t] = v / 1000.0
|
||||
return temps
|
||||
|
||||
|
||||
def freqs():
|
||||
out = []
|
||||
for i in range(os.cpu_count() or 8):
|
||||
v = readint(f"/sys/devices/system/cpu/cpu{i}/cpufreq/scaling_cur_freq")
|
||||
out.append((v or 0) / 1e6)
|
||||
return out
|
||||
|
||||
|
||||
def meminfo():
|
||||
m = {}
|
||||
try:
|
||||
with open("/proc/meminfo") as f:
|
||||
for line in f:
|
||||
k, v = line.split(":", 1)
|
||||
m[k] = int(v.split()[0]) * 1024
|
||||
except (OSError, ValueError):
|
||||
pass
|
||||
return m
|
||||
|
||||
|
||||
class Net:
|
||||
def __init__(self, dev="wlan0"):
|
||||
self.dev = dev
|
||||
self.prev = self._read()
|
||||
self.t = time.monotonic()
|
||||
self.rx_hist, self.tx_hist = [], []
|
||||
|
||||
def _read(self):
|
||||
base = f"/sys/class/net/{self.dev}/statistics"
|
||||
return readint(f"{base}/rx_bytes", 0), readint(f"{base}/tx_bytes", 0)
|
||||
|
||||
def sample(self):
|
||||
now = time.monotonic()
|
||||
rx, tx = self._read()
|
||||
dt = max(1e-3, now - self.t)
|
||||
r = max(0.0, (rx - self.prev[0]) / dt)
|
||||
t = max(0.0, (tx - self.prev[1]) / dt)
|
||||
self.prev, self.t = (rx, tx), now
|
||||
self.rx_hist = (self.rx_hist + [r])[-HIST:]
|
||||
self.tx_hist = (self.tx_hist + [t])[-HIST:]
|
||||
return r, t
|
||||
|
||||
|
||||
def wifi():
|
||||
info = {"ssid": "", "dbm": None, "freq": None, "ip": ""}
|
||||
for line in sh(["iw", "dev", "wlan0", "link"]).splitlines():
|
||||
line = line.strip()
|
||||
if line.startswith("SSID:"):
|
||||
info["ssid"] = line.split(":", 1)[1].strip()
|
||||
elif line.startswith("signal:"):
|
||||
try:
|
||||
info["dbm"] = int(line.split()[1])
|
||||
except (IndexError, ValueError):
|
||||
pass
|
||||
elif line.startswith("freq:"):
|
||||
try:
|
||||
info["freq"] = float(line.split()[1])
|
||||
except (IndexError, ValueError):
|
||||
pass
|
||||
for part in sh(["ip", "-o", "-4", "addr", "show", "dev", "wlan0"]).split():
|
||||
if "/" in part and "." in part:
|
||||
info["ip"] = part.split("/")[0]
|
||||
break
|
||||
return info
|
||||
|
||||
|
||||
def battery():
|
||||
ps = "/sys/class/power_supply/qcom-battmgr-bat"
|
||||
b = {
|
||||
"cap": readint(f"{ps}/capacity"),
|
||||
"status": read(f"{ps}/status", "?"),
|
||||
"volt": (readint(f"{ps}/voltage_now") or 0) / 1e6,
|
||||
"cur": (readint(f"{ps}/current_now") or 0) / 1e6,
|
||||
"power": (readint(f"{ps}/power_now") or 0) / 1e6,
|
||||
"temp": (readint(f"{ps}/temp") or 0) / 10.0,
|
||||
"charge": readint(f"{ps}/charge_counter"),
|
||||
"cycles": readint(f"{ps}/cycle_count"),
|
||||
"health": readint(f"{ps}/state_of_health"),
|
||||
}
|
||||
usb = "/sys/class/power_supply/qcom-battmgr-usb"
|
||||
b["usb_v"] = (readint(f"{usb}/voltage_now") or 0) / 1e6
|
||||
b["usb_online"] = readint(f"{usb}/online", 0)
|
||||
return b
|
||||
|
||||
|
||||
def hms(secs):
|
||||
if not secs or secs <= 0:
|
||||
return "—"
|
||||
h, m = divmod(int(secs) // 60, 60)
|
||||
return f"{h}h {m:02d}m" if h else f"{m}m"
|
||||
|
||||
|
||||
def uptime():
|
||||
try:
|
||||
secs = float(read("/proc/uptime", "0").split()[0])
|
||||
except ValueError:
|
||||
return "—"
|
||||
d, rem = divmod(int(secs), 86400)
|
||||
h, rem = divmod(rem, 3600)
|
||||
m = rem // 60
|
||||
return f"{d}d {h}h {m:02d}m" if d else f"{h}h {m:02d}m"
|
||||
|
||||
|
||||
DIAS = ["lun", "mar", "mié", "jue", "vie", "sáb", "dom"]
|
||||
MESES = ["ene", "feb", "mar", "abr", "may", "jun", "jul", "ago", "sep", "oct", "nov", "dic"]
|
||||
|
||||
|
||||
# ---------------------------------------------------------------- render
|
||||
|
||||
|
||||
def split_lr(left, right, width):
|
||||
gap = width - vlen(left) - vlen(right)
|
||||
return left + " " * max(1, gap) + right
|
||||
|
||||
|
||||
def render(cols, rows, cpu, net):
|
||||
width = min(cols - 2, 60)
|
||||
inner = width - 4
|
||||
total, cores = cpu.sample()
|
||||
rx, tx = net.sample()
|
||||
temps = thermal()
|
||||
fr = freqs()
|
||||
mem = meminfo()
|
||||
w = wifi()
|
||||
b = battery()
|
||||
now = _dt.datetime.now()
|
||||
|
||||
out = []
|
||||
|
||||
# header
|
||||
host = read("/proc/sys/kernel/hostname", "rtwo")
|
||||
clock = f"{BOLD}{fg(LAV)}{now:%H:%M:%S}{RST}"
|
||||
out.append(split_lr(f"📱 {BOLD}{fg(TEXT)}{host}{RST}", f"🕒 {clock}", width))
|
||||
fecha = f"{DIAS[now.weekday()]} {now.day} {MESES[now.month - 1]} {now.year}"
|
||||
out.append(split_lr(f"{fg(SUB)} {fecha}{RST}", f"⏳ {fg(SUB)}{uptime()}{RST}", width))
|
||||
out.append("")
|
||||
|
||||
# CPU
|
||||
cpu_t = max([v for k, v in temps.items() if k.startswith("cpu")] or [0])
|
||||
load = read("/proc/loadavg", "0 0 0").split()[:3]
|
||||
body = [
|
||||
split_lr(gbar(total, inner - 6, total / 100), f"{BOLD}{fg(heat(total / 100))}{total:4.0f}%{RST}", inner),
|
||||
spark(cpu.hist, inner, BLUE, top=100),
|
||||
]
|
||||
core_cells = []
|
||||
for i, u in enumerate(cores):
|
||||
core_cells.append(f"{fg(OVER)}{i}{fg(heat(u / 100))}{SPARK[min(7, int(u / 12.5))]}{RST}")
|
||||
body.append(" ".join(core_cells) + f" {fg(SUB)}{min(fr):.1f}–{max(fr):.1f} GHz{RST}")
|
||||
body.append(split_lr(f"🔥 {fg(heat((cpu_t - 30) / 50))}{cpu_t:.0f}°C{RST}",
|
||||
f"{fg(SUB)}load {' '.join(load)}{RST}", inner))
|
||||
out += card("🧠", "CPU", BLUE, body, width)
|
||||
|
||||
# memory
|
||||
mt, ma = mem.get("MemTotal", 0), mem.get("MemAvailable", 0)
|
||||
mu = mt - ma
|
||||
mp = 100.0 * mu / mt if mt else 0
|
||||
st, sf = mem.get("SwapTotal", 0), mem.get("SwapFree", 0)
|
||||
body = [
|
||||
split_lr(gbar(mp, inner - 6, mp / 100), f"{BOLD}{fg(heat(mp / 100))}{mp:4.0f}%{RST}", inner),
|
||||
split_lr(f"{fg(TEXT)}{human(mu)}{fg(OVER)} / {human(mt)}{RST}",
|
||||
f"{fg(SUB)}zram {human(st - sf)} / {human(st, 0)}{RST}", inner),
|
||||
]
|
||||
out += card("🐏", "Memoria", MAUVE, body, width)
|
||||
|
||||
# storage
|
||||
try:
|
||||
s = os.statvfs("/")
|
||||
dt_, du = s.f_blocks * s.f_frsize, (s.f_blocks - s.f_bfree) * s.f_frsize
|
||||
except OSError:
|
||||
dt_ = du = 0
|
||||
dp = 100.0 * du / dt_ if dt_ else 0
|
||||
body = [
|
||||
split_lr(gbar(dp, inner - 6, dp / 100), f"{BOLD}{fg(heat(dp / 100))}{dp:4.0f}%{RST}", inner),
|
||||
f"{fg(TEXT)}{human(du)}{fg(OVER)} usados de {human(dt_)}{RST}",
|
||||
]
|
||||
out += card("💾", "Almacenamiento", SAPH, body, width)
|
||||
|
||||
# pantalla apaisada (monitor por DP): red y bateria en una segunda columna
|
||||
two_cols = cols >= 2 * width + 4
|
||||
if two_cols:
|
||||
col1, out = out, [""] * 3
|
||||
|
||||
# network
|
||||
if w["ssid"]:
|
||||
dbm = w["dbm"] if w["dbm"] is not None else -100
|
||||
q = max(0.0, min(1.0, (dbm + 90) / 50))
|
||||
bars = "".join(
|
||||
(fg(heat(1 - q)) if q >= (i + 1) / 4 - 0.01 else fg(SURF)) + c
|
||||
for i, c in enumerate("▂▄▆█")
|
||||
) + RST
|
||||
band = f"{w['freq'] / 1000:.1f} GHz" if w["freq"] else ""
|
||||
body = [
|
||||
split_lr(f"{BOLD}{fg(TEXT)}{w['ssid']}{RST}", f"{bars} {fg(SUB)}{dbm} dBm{RST}", inner),
|
||||
split_lr(f"🌐 {fg(TEAL)}{w['ip'] or 'sin IP'}{RST}", f"{fg(SUB)}{band}{RST}", inner),
|
||||
f"📥 {fg(GREEN)}{rate(rx):>9}{RST} " + spark(net.rx_hist, inner - 13, GREEN),
|
||||
f"📤 {fg(PEACH)}{rate(tx):>9}{RST} " + spark(net.tx_hist, inner - 13, PEACH),
|
||||
]
|
||||
else:
|
||||
body = [f"{fg(RED)}sin conexión{RST}", f"{fg(OVER)}esperando WiFi…{RST}"]
|
||||
out += card("📶", "Red", TEAL, body, width)
|
||||
|
||||
# battery
|
||||
cap = b["cap"] if b["cap"] is not None else 0
|
||||
st_txt = {"Charging": "⚡ Cargando", "Discharging": "🔋 Descargando",
|
||||
"Full": "✅ Completa", "Not charging": "🔌 Conectada"}.get(b["status"], b["status"])
|
||||
cap_col = heat(1 - cap / 100)
|
||||
cur = b["cur"]
|
||||
sign = "+" if cur > 0 else ""
|
||||
# time_to_*_avg del firmware viene en otra escala (daba ~441 h):
|
||||
# estimar con la carga restante (charge_counter, uAh) y la corriente.
|
||||
left = "—"
|
||||
if b["charge"] and abs(cur) > 0.02 and cap:
|
||||
rem_ah = b["charge"] / 1e6
|
||||
full_ah = rem_ah * 100.0 / cap
|
||||
hours = (full_ah - rem_ah) / abs(cur) if cur > 0 else rem_ah / abs(cur)
|
||||
left = hms(hours * 3600)
|
||||
body = [
|
||||
split_lr(gbar(cap, inner - 6, 1 - cap / 100), f"{BOLD}{fg(cap_col)}{cap:4d}%{RST}", inner),
|
||||
split_lr(f"{fg(TEXT)}{st_txt}{RST}", f"⏳ {fg(SUB)}{left}{RST}", inner),
|
||||
split_lr(f"{fg(YEL)}{abs(b['power']):.2f} W{RST} {fg(OVER)}·{RST} "
|
||||
f"{fg(TEXT)}{b['volt']:.2f} V{RST} {fg(OVER)}·{RST} {fg(TEXT)}{sign}{cur:.2f} A{RST}",
|
||||
f"🔥 {fg(heat((b['temp'] - 25) / 20))}{b['temp']:.0f}°C{RST}", inner),
|
||||
]
|
||||
extra = f"{fg(SUB)}{b['cycles'] or '?'} ciclos · salud {b['health'] or '?'}%{RST}"
|
||||
if b["usb_online"] and b["usb_v"] > 1:
|
||||
extra = split_lr(extra, f"🔌 {fg(SUB)}USB {b['usb_v']:.1f} V{RST}", inner)
|
||||
body.append(extra)
|
||||
out += card("🔋", "Batería", GREEN, body, width)
|
||||
|
||||
if two_cols:
|
||||
n = max(len(col1), len(out))
|
||||
col1 += [""] * (n - len(col1))
|
||||
out += [""] * (n - len(out))
|
||||
out = [pad(a, width) + " " + b for a, b in zip(col1, out)]
|
||||
width = 2 * width + 4
|
||||
|
||||
# center
|
||||
top = max(0, (rows - len(out)) // 2)
|
||||
left_pad = " " * max(0, (cols - width) // 2)
|
||||
frame = [""] * top + [left_pad + ln for ln in out]
|
||||
frame += [""] * max(0, rows - len(frame))
|
||||
return frame[:rows]
|
||||
|
||||
|
||||
def main():
|
||||
sys.stdout.write(ESC + "?25l" + ESC + "2J")
|
||||
signal.signal(signal.SIGTERM, lambda *_: sys.exit(0))
|
||||
cpu, net = Cpu(), Net()
|
||||
time.sleep(0.3)
|
||||
try:
|
||||
while True:
|
||||
cols, rows = shutil.get_terminal_size((45, 45))
|
||||
frame = render(cols, rows, cpu, net)
|
||||
sys.stdout.write(ESC + "H" + "\r\n".join(ESC + "2K" + ln for ln in frame))
|
||||
sys.stdout.flush()
|
||||
time.sleep(REFRESH_SECS)
|
||||
except (KeyboardInterrupt, SystemExit):
|
||||
pass
|
||||
finally:
|
||||
sys.stdout.write(RST + ESC + "?25h")
|
||||
sys.stdout.flush()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,13 @@
|
||||
[Unit]
|
||||
Description=rtwo dashboard (sway: dashboard en el panel; consola en el monitor DP-1 si hay)
|
||||
After=systemd-user-sessions.service getty@tty1.service plymouth-quit-wait.service
|
||||
Wants=getty@tty1.service
|
||||
|
||||
[Service]
|
||||
Type=simple
|
||||
ExecStart=/usr/libexec/rtwo-dashboard-launcher
|
||||
Restart=always
|
||||
RestartSec=2
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
@@ -0,0 +1,11 @@
|
||||
[Unit]
|
||||
Description=rtwo: destrabar wpa_supplicant y conectar a 5 GHz tras el arranque (workaround ath12k)
|
||||
After=NetworkManager.service wpa_supplicant.service
|
||||
Wants=NetworkManager.service
|
||||
|
||||
[Service]
|
||||
Type=simple
|
||||
ExecStart=/usr/libexec/rtwo-wifi-5ghz-scan
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
@@ -0,0 +1,66 @@
|
||||
#!/bin/sh
|
||||
# rtwo (ath12k / WCN7850), 2026-09-25. Tras el arranque, wpa_supplicant queda
|
||||
# con un escaneo trabado ("Reject scan trigger since one is already pending")
|
||||
# y su interfaz no sale de 'disconnected': NetworkManager no intenta conectar
|
||||
# nunca. Ademas, los escaneos normales no recorren los canales de 5 GHz, y la red
|
||||
# (en 5 GHz) no aparece.
|
||||
# Hasta que wlan0 conecte: escaneo dirigido a 5 GHz (con 'scan abort' si da
|
||||
# EBUSY), conexion explicita con nmcli si la red aparece, y reinicio de
|
||||
# wpa_supplicant (NetworkManager se reengancha solo) si sigue sin conectar.
|
||||
FREQS="5180 5200 5220 5240 5260 5280 5300 5320 5500 5520 5540 5560 5580 5600 5620 5640 5660 5680 5700 5720 5745 5765 5785 5805 5825"
|
||||
# Red a destrabar: la primera conexion WiFi de NetworkManager, o la que se fije
|
||||
# en /etc/default/rtwo-wifi-5ghz-scan (CON=<nombre de la conexion>).
|
||||
[ -r /etc/default/rtwo-wifi-5ghz-scan ] && . /etc/default/rtwo-wifi-5ghz-scan
|
||||
: "${CON:=$(nmcli -t -f NAME,TYPE connection show | awk -F: '$2 == "802-11-wireless" { print $1; exit }')}"
|
||||
[ -n "$CON" ] || { echo "sin conexion WiFi en NetworkManager"; exit 0; }
|
||||
: "${SSID:=$(nmcli -g 802-11-wireless.ssid connection show "$CON")}"
|
||||
|
||||
connected() {
|
||||
nmcli -t -f DEVICE,STATE device | grep -q '^wlan0:connected$'
|
||||
}
|
||||
|
||||
scan() {
|
||||
iw dev wlan0 scan freq $FREQS ssid "$SSID" 2>&1
|
||||
}
|
||||
|
||||
i=0
|
||||
while [ ! -e /sys/class/net/wlan0 ] && [ $i -lt 60 ]; do
|
||||
sleep 1; i=$((i + 1))
|
||||
done
|
||||
[ -e /sys/class/net/wlan0 ] || { echo "wlan0 no aparecio"; exit 1; }
|
||||
sleep 10
|
||||
|
||||
n=0
|
||||
while [ $n -lt 30 ]; do
|
||||
if connected; then
|
||||
echo "wlan0 conectado tras $n intentos"
|
||||
exit 0
|
||||
fi
|
||||
n=$((n + 1))
|
||||
|
||||
if [ $n -eq 2 ] || [ $((n % 5)) -eq 0 ]; then
|
||||
echo "#$n: reiniciando wpa_supplicant (escaneo trabado)"
|
||||
systemctl restart wpa_supplicant
|
||||
sleep 5
|
||||
connected && continue
|
||||
fi
|
||||
|
||||
out=$(scan)
|
||||
if echo "$out" | grep -q 'Resource busy'; then
|
||||
echo "#$n: EBUSY, scan abort y reintento"
|
||||
iw dev wlan0 scan abort >/dev/null 2>&1
|
||||
sleep 1
|
||||
out=$(scan)
|
||||
fi
|
||||
if echo "$out" | grep -q "SSID: $SSID\$"; then
|
||||
echo "#$n: $SSID encontrada, conectando con nmcli"
|
||||
nmcli --wait 30 connection up "$CON" 2>&1 | sed 's/^/ nmcli: /'
|
||||
else
|
||||
nbss=$(echo "$out" | grep -c '^BSS ')
|
||||
err=$(echo "$out" | grep -m1 'command failed')
|
||||
echo "#$n: $SSID NO encontrada ($nbss BSS en 5 GHz) $err"
|
||||
fi
|
||||
sleep 10
|
||||
done
|
||||
echo "wlan0 sigue sin conectar tras $n intentos"
|
||||
exit 1
|
||||
Reference in New Issue
Block a user