Documentation · Technical reference
Detailed Architecture & Technical Reference
This document explains the hardware topology, protocols, event sources, state machine and edge-case mitigations of TizenPowerSync.
1. System Overview & Hardware Topology#
The suite synchronizes power, input selection and the video link of a
Samsung Tizen Smart TV (UN40KU7000) used as a 4K60 UHD external
monitor for a Linux laptop running KDE Plasma 6 (Wayland).
Hardware & Interface Stack#
- Internal Display (
eDP-1): laptop panel (2880x1800 @ 60Hz, scale1.6, positioned at(3840, 1284)). - External Display (
DP-2): connected through a USB-C dock featuring:- a DisplayPort-to-HDMI 2.0 active converter (Parade
PS186) exposing DRM connector/sys/class/drm/card1-DP-2and HDMI-CEC adapter/dev/cec0(i915driver); - a USB wired Ethernet controller (
enx50a0300cc53a) alongside the laptop’s Wi-Fi interface (wlo1).
- a DisplayPort-to-HDMI 2.0 active converter (Parade
- Samsung Smart TV (
UN40KU7000):- IP
192.168.0.2, MAC40:16:3b:d0:4f:8b - HDMI 1 / DVI, CEC physical address
1.0.0.0, TV logical address0 - Control channels: WoL (UDP 9/7), HDMI-CEC (
/dev/cec0), and the Tizen WebSocket API (port8001).
- IP
All of these are configurable via TV_IP, TV_MAC, CEC_DEVICE, TV_MODE
and TV_LOG_LEVEL (see tvlib.Config).
2. Core Components#
2.1 tv_power_daemon.py (tv-power-sync.service)#
A long-running user-session daemon driving a GLib.MainLoop. The main loop does
only cheap work — sysfs reads, socket reads and scheduling. Every CEC, WoL,
WebSocket and kscreen-doctor operation runs in a worker thread.
Power intent model
Every request to turn the TV on or off creates an Intent:
Intent(seq, kind, cancel_event)
- Creating a new intent increments
seqand sets the previous intent’scancelevent. - Workers check
cancelbefore each side effect: before a CEC query, before each WoL burst, before a WebSocket key, between wake-verification attempts. - The CEC controller re-checks cancellation inside its process lock, so a superseded intent cannot transmit a stale command after waiting for the adapter.
- Duplicate ON requests are ignored while an ON intent is active; duplicate OFF requests are absorbed by the 12-second cooldown and the pre-flight CEC state query.
This replaces the previous abort_wake boolean, which conflated “cancel the
wake” with “sleep now” and could not stop an in-flight sleep retry.
Monitored inputs
-
Kernel Netlink KObject UEvent socket
- Binds multicast group 1 (
NETLINK_KOBJECT_UEVENT), raisesSO_RCVBUFto 1 MiB, and attaches toGLib.io_add_watch. - Only
SUBSYSTEM=drmevents withHOTPLUG=1/add/remove, andSUBSYSTEM=usbadd/removeevents are considered. USB HID/hubchangetraffic and the daemon’s own DPMS changes are ignored. - Events are coalesced by a 400ms one-shot timer, because USB-C/PD
renegotiation and MST setup emit bursts and transient
connectedreads. - If the netlink bind fails,
on_dpms_pollcallscheck_external_display_transition()every 2 seconds. This is the fallback the previous version only logged about.
- Binds multicast group 1 (
-
Laptop lid (
login1.PropertiesChanged,LidClosed)- Read once at startup via D-Bus with a
/proc/acpifallback; afterwards purely event-driven. The handler filters on thelogin1.Managerinterface.
- Read once at startup via D-Bus with a
-
Adaptive hardware DPMS tracking
- Reads
/sys/class/drm/*-eDP-*/dpms(falling back to LVDS/DSI). - 250ms while the session is locked, 2000ms while unlocked. sysfs has no inotify/poll support, so a timer is the correct mechanism; the cost is a few microseconds per read.
- If the node disappears (GPU reset, connector rename), the path is
re-resolved.
Unknownis never treated as “screen off”.
- Reads
-
KDE ScreenSaver (
org.freedesktop.ScreenSaver.ActiveChanged)- Switches the DPMS polling interval and drives unlock-time wake.
- Registration is retried every 5 seconds if the session bus is not ready when the user unit starts.
-
PrepareForSleep- Suspend creates sleep intent.
- Resume waits 2 seconds for KWin to restore DPMS, then reconciles. It never
forces
kscreen-doctor --dpms on, and it does nothing while the lid is closed.
-
Watchdog (5-second GLib timer, worker-thread check)
- Preconditions: state
awake, panel DPMSOn, dockconnected, lid open, no active wake, backoff expired. - Probes TCP
8001first (no subprocess). Only when the port is closed does it query CEC. - A CEC
standby/to-standbyreading wakes immediately;offlinerequires two consecutive checks. The dock state is re-read after the query so an unplug during the check cannot trigger a false wake. - Failed wake attempts apply exponential backoff (60s, 120s, 240s, … up to 900s). Backoff resets on dock connect, DPMS-on, lid-open or unlock.
- Preconditions: state
2.2 boot_wol.py (tv-boot-wol.service)#
A system-level oneshot (RemainAfterExit=yes) running once per boot after
network-online.target and before the display manager. It skips when the panel
is in DPMS Off, checks for a connected DRM connector, a known USB-C dock vendor
(2109 VIA Labs / 0bda Realtek), or the 192.168.0.x home subnet, then sends
three WoL bursts plus CEC IMAGE_VIEW_ON + ACTIVE_SOURCE.
2.3 control_tv.py#
Manual CLI for status, wake, standby and wol. Standby follows the same
single-path rule as the daemon: CEC standby when docked, WebSocket KEY_POWER
only as a fallback. All commands use list-form subprocess calls (no shell).
2.4 parse_drm_info.py#
Formats drm_info -j output: driver/caps, connectors and EDID, bpc/HDR
metadata, CRTC timings, and plane usage. It reads argv[1] when given, otherwise
TV_DRM_INFO (default drm_info.json in the working directory) and falls back
to running drm_info -j when the file is missing.
2.5 pair_samsung_tv.py#
One-off WebSocket pairing helper (ports 8002/8001). Uses TV_IP and writes the
token to TV_TOKEN_FILE (default ~/.local/state/tizenpowersync/tv_token.txt).
2.6 udev/70-cec-uaccess.rules#
KERNEL=="cec*", SUBSYSTEM=="cec", GROUP="video", MODE="0660", TAG+="uaccess"
Grants the active desktop user access to /dev/cec0 without root.
2.7 tvlib.py#
Shared config, logging, WoL, CEC, WebSocket, DRM-sysfs and backoff helpers. The
daemon, CLI and boot service all import it, so send_wol, power-state parsing
and connector discovery exist in exactly one place. TvLink holds the optional
kept-warm remote-control WebSocket; send_key remains the one-shot fallback.
3. Operational Workflows#
3.1 Decisive Wake (_do_decisive_wake)#
Triggered by dock connect, DPMS Off→On, lid open (docked), resume (docked, lid open) and the watchdog.
- Cancellation / connection guard — abort if the intent was superseded or the external display is disconnected.
- Settle window — if the TV was commanded off less than 3s ago, wait out
the remainder. The wait is interruptible by a new intent. A reported
onduring this window is ignored (Tizen can keep port 8001 open while shutting down): the pre-wake CEC query is skipped and the full WoL + CEC sequence runs immediately after the settle. - Clear the off cooldown, then ensure KDE DPMS is on only if the panel is currently off (avoids spawning a Qt process on every wake).
- Fast path — query CEC. If already
on/to-on, re-assert the video mode and CEC active source and return. - Wake — 5 WoL bursts (one
ipenumeration cached for the whole sequence) plus a singlecec-ctl --playback --to 0 --image-view-on --active-sourceinvocation. - Verification loop — up to 10 attempts, polling TCP every 0.75s for the first four attempts and every 1.5s afterwards. WoL is re-sent each round; the blocking CEC wake is repeated only every third round so it does not delay the polls. The loop exits immediately if the intent is cancelled or the dock is unplugged.
- Video mode re-assert —
kscreen-doctor output.<connector>.mode.<TV_MODE>. Using the mode string rather than a positional index survives mode-table changes. - Backoff — a failed, still-current wake doubles the delay (max 15min). Superseded wakes do not apply backoff.
KEY_POWER is never sent during a wake: Samsung treats it as a toggle, so wake
uses only discrete commands.
3.2 Sleep (_power_off_worker)#
Triggered by DPMS off, lid close, dock unplug and suspend.
- Cancel any in-flight wake via the new intent.
- Cooldown — skip if a sleep command was delivered less than 12s ago.
- Pre-flight state — CEC query; if already
standby/to-standby, mark the cooldown and stop. - Single path — when docked, send one CEC
STANDBY. If and only if that fails, fall back to WebSocketKEY_POWER. The previous implementation sent both unconditionally, so the toggle could wake a TV the standby had just turned off. - Warm control channel — while docked and awake, the daemon holds one
WebSocket to the TV (
tvlib.TvLink) and pings it every 10s. Tizen otherwise stops answering on port 8001 after a few idle minutes, which made the undock path spend 6-8s in TCP retries beforeKEY_POWERcould go out. The sleep key reuses this socket withkey_press_delay=0, so only the server round trip remains. The session is sourced from the Wi-Fi address, because a socket bound to the dock’s Ethernet address is destroyed at the moment of undock; the sleep worker tries the warm link before any probe. The link is closed while the TV is expected to be off, and reopened automatically after a wake. - Undock retry — after the dock is removed, CEC is gone and NetworkManager
is failing over from the dock NIC to Wi-Fi. The worker probes reachability
with a 0.25s TCP timeout and retries every 0.25s for up to 20 attempts
(~10s), aborting if the dock returns or the intent is cancelled. Wake-on-LAN
interface discovery uses ioctl rather than a forked
ipprocess, so a dock re-enumeration cannot stall the wake path for seconds.
3.3 Screen Off / Lock#
handle_screen_off sets state to asleep, runs loginctl lock-session with
XDG_SESSION_ID when the trigger was idle DPMS, and creates a sleep intent.
The intent queue handles duplicate OFF requests; there is no longer a
“state is already asleep” early return that could permanently miss a command.
At startup, if the panel is already Off, the daemon creates a sleep intent
after 500ms instead of assuming the TV is already off.
A panel On transition is debounced for 800ms before it wakes the TV.
KDE’s locker re-asserts DPMS around a blank/restore, and waking on that bounce
meant the next sleep request was cancelled by the wake it had just started. The
timer is dropped when the panel goes Off again, so a sleep request is never
superseded by a bounce, while a genuine pointer or keyboard wake at the lock
screen still starts the normal wake path. Startup reconciliation skips the wake
while the session is locked, so a daemon restart cannot toggle the TV.
3.4 Suspend / Resume#
PrepareForSleep(true) creates a sleep intent. systemd does not wait for
D-Bus signal handlers, so the command is best-effort (documented caveat).
PrepareForSleep(false) schedules a reconcile 2s later: if the lid is open and
the panel came back On, it runs the normal wake path; otherwise it leaves the
TV alone and waits for the DPMS poll.
3.5 Watchdog Backoff#
| Consecutive failure | Next attempt |
|---|---|
| 1 | 60s |
| 2 | 120s |
| 3 | 240s |
| 4 | 480s |
| 5+ | 900s (cap) |
Any user-intent transition (dock connect, DPMS-on, lid open, unlock) resets the delay to 60s. This prevents the old behavior of broadcasting WoL forever, once per ~75s, when the TV is unplugged from AC or deliberately off.
4. Event Flow Summary#
| Event | Detection | Action |
|---|---|---|
| Boot (docked) | tv-boot-wol.service after network-online |
3x WoL + CEC wake |
| Dock plugged | netlink uevent, 400ms debounce | reset backoff, decisive wake |
| Dock unplugged | netlink uevent, 400ms debounce | cancel wake, Wi-Fi sleep with route-failover retry |
| Lid closed | login1 PropertiesChanged |
sleep intent |
| Lid opened | login1 PropertiesChanged |
wake if docked |
| Idle DPMS / Esc | panel DPMS On→Off | lock session, sleep intent |
| Mouse/key wake | panel DPMS Off→On | DPMS on (if needed), wake |
| TV off while working | 5s watchdog (thread) | CEC state / 2× offline → wake, with backoff |
| Suspend | PrepareForSleep(true) |
sleep intent |
| Resume | PrepareForSleep(false) + 2s |
reconcile only if lid open and panel on |
5. Configuration#
| Variable | Default | Used by |
|---|---|---|
TV_IP |
192.168.0.2 |
daemon, CLI, boot, pairing |
TV_MAC |
40:16:3b:d0:4f:8b |
WoL |
CEC_DEVICE |
/dev/cec0 |
daemon, CLI, boot |
TV_MODE |
3840x2160@60 |
mode re-assert |
TV_LOG_LEVEL |
INFO |
all scripts |
TV_PYTHON |
$XDG_DATA_HOME/tizenpowersync/venv/bin/python |
deploy.sh interpreter override |
TV_TOKEN_FILE |
~/.local/state/tizenpowersync/tv_token.txt |
pairing helper |
TV_DRM_INFO |
drm_info.json (cwd) |
parse_drm_info snapshot |
deploy.sh keeps the venv outside the clone, at
$XDG_DATA_HOME/tizenpowersync/venv (default
~/.local/share/tizenpowersync/venv), so the checkout can be moved or deleted
after deployment. When the venv is missing the script creates it with
--system-site-packages and installs samsungtvws; TV_PYTHON overrides the
interpreter. If an existing venv cannot see the system bindings it writes a
version-matched 000-system-bindings.pth into the venv (the same effect as the
flag) rather than re-creating it. It substitutes the selected interpreter into
the user and boot units (@PYTHON@, @BIN_DIR@), rewrites the installed script
shebangs to match, fails if no interpreter can import dbus/gi, and warns if
samsungtvws is missing.