Skip to content

feat(network): add Wake-on-LAN wake module (type: wol)#217

Open
Ulrond wants to merge 2 commits into
rdkcentral:developfrom
Ulrond:feature/216-wake-on-lan-power-module
Open

feat(network): add Wake-on-LAN wake module (type: wol)#217
Ulrond wants to merge 2 commits into
rdkcentral:developfrom
Ulrond:feature/216-wake-on-lan-power-module

Conversation

@Ulrond

@Ulrond Ulrond commented Jul 14, 2026

Copy link
Copy Markdown
Contributor

Closes #216.

What

Adds a network/wake controller for a device — Wake-on-LAN lives here, not in the power seam.

  • framework/core/networkModules/wol.py (networkWol) — a single wake() verb that sends the WoL magic packet (6×0xFF + target MAC ×16) to broadcast:port.
  • framework/core/networkControl.py (networkControlClass) — dispatches type: "wol", with the same retry behaviour (retryCount / retryDelay) as powerControl.
  • Wired into deviceManager via a device network: block → exposed as dut.networkController; also surfaced on testControl as self.networkController.
  • Standard-library only (socket) — no new dependencies.
network:
  type: "wol"
  mac: "aa:bb:cc:dd:ee:ff"
  broadcast: "255.255.255.255"   # optional - subnet broadcast for directed WoL
  port: 9                        # optional (7 or 9)

Usage in a suite is deliberately dead-simple:

control = dut.networkController
control.wake()

Why network, not power

Wake-on-LAN is a network (layer-2) action, not a power action: it sends a UDP magic packet, cannot power a device off, and supports none of the power-metering contract (getPowerLevel / getVoltageLevel / getCurrentLevel). Filing it under powerModules mislabelled it and left half the "power switch" contract inapplicable.

So it is re-homed onto a dedicated network controller that mirrors the existing device-controller pattern (remoteController / hdmiCECController / avSyncController). The problem it solves is unchanged: targets that aggressively deep-sleep drop their console when idle and can only be brought back by a magic packet; without this, runs against them need an out-of-band wake step before the session opens.

Behavioural note: waking is now an explicit dut.networkController.wake() rather than riding on powerControl.powerOn(). The seam is designed to grow (e.g. isReachable() / waitForOnline() as a natural companion to wake(); deviceClass.pingTest() already offers ICMP reachability).

Testing

  • tests/networkControl_tests.py — builds networkControlClass from config and calls wake().
  • Verified: magic packet is correct (102 bytes, 6×0xFF + MAC×16), wake() sends and was captured on loopback, missing / malformed MAC raise ValueError, unknown type is handled gracefully.
  • End-to-end: a device network: block produces dut.networkController; powerControl(type:"wol") now correctly reports the type as unknown.
  • Woke a real armv7 target on the same broadcast domain.

Docs

  • AGENTS.md — directory tree, controller shortcut attributes, and a new §5.2 Network / wake modules (broadened §5 to "Power & Network Control").
  • docs/modules/device_manager.mdnetworkController added to the deviceClass managed objects.

Note: the branch name (…power-module) predates the re-home; the code is a network module. First commit adds the initial (power-seam) cut; the second re-homes it to the network seam and reverts the power coupling.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds a new Wake-on-LAN (WoL) “power switch” implementation and wires it into the existing powerControlClass dispatch so RAFT can wake devices that are asleep/soft-off via a WoL magic packet.

Changes:

  • Added powerWol module that constructs and sends a WoL magic packet to a configurable broadcast address/UDP port.
  • Added type == "wol" dispatch branch in powerControlClass to instantiate the new module.

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated 4 comments.

File Description
framework/core/powerModules/wol.py Implements WoL magic-packet creation and UDP broadcast send; includes wake-only semantics for powerOff()/reboot().
framework/core/powerControl.py Adds import and type: "wol" dispatch branch to construct powerWol.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

raise ValueError("powerWol requires a 'mac' address")
self.mac = mac
self.broadcast = broadcast or "255.255.255.255"
self.port = int(port) if port else 9
self.log.info("powerWol(mac={}, broadcast={}, port={})".format(
self.mac, self.broadcast, self.port))

def _magicPacket(self):
Comment on lines +72 to +75
hexMac = self.mac.replace(":", "").replace("-", "").replace(".", "")
if len(hexMac) != 12:
raise ValueError("Invalid MAC address: {}".format(self.mac))
return b"\xff" * 6 + bytes.fromhex(hexMac) * 16
bool: True if the packet was sent, False on error.
"""
try:
packet = self._magicPacket()
@Ulrond
Ulrond force-pushed the feature/216-wake-on-lan-power-module branch from c5d9760 to 0a32b41 Compare July 14, 2026 16:37
@Ulrond
Ulrond requested a review from TB-1993 July 15, 2026 15:44
@Ulrond Ulrond self-assigned this Jul 15, 2026
@Ulrond Ulrond added the enhancement New feature or request label Jul 15, 2026
Copilot AI review requested due to automatic review settings July 20, 2026 10:09
@Ulrond Ulrond changed the title feat(power): add Wake-on-LAN power module (type: wol) feat(network): add Wake-on-LAN wake module (type: wol) Jul 20, 2026
powerControlClass had no way to wake a device that is asleep or in a
Wake-on-LAN soft-off state. Add framework/core/powerModules/wol.py
(powerWol) and a `type: "wol"` dispatch branch:

- powerOn() sends the WoL magic packet (6x 0xFF + MAC x16) to broadcast:port.
- Wake-on-LAN is wake-only: powerOff() is a logged no-op, reboot() a
  best-effort wake.
- Config: mac (required), broadcast (default 255.255.255.255), port (default 9).
- Standard-library only (socket); no new dependencies.

Fixes rdkcentral#216

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 7 out of 7 changed files in this pull request and generated 6 comments.

Comment on lines +54 to +60
self.networkModule = None
type = config.get("type")
if type == "wol":
self.networkModule = networkWol(log, config.get("mac"), config.get("broadcast", "255.255.255.255"), config.get("port", 9))
else:
self.log.error("Network controller type [{}] unknown".format(type))
return
Comment on lines +68 to +70
self.log.info("wake ({})".format(self.name))
return self.networkRetry(self.networkModule.wake)

Comment on lines +88 to +91
self.log.info(f"Could not perform {[networkMethod.__name__]}. Retry count: [{x}] of [{self.retryCount}], delay is [{self.retryDelay}]")
if x == self.retryCount:
raise e
self.utils.wait(self.retryDelay)
Comment on lines +71 to +75
"""Build the Wake-on-LAN magic packet: 6x 0xFF followed by the MAC x16."""
hexMac = self.mac.replace(":", "").replace("-", "").replace(".", "")
if len(hexMac) != 12:
raise ValueError("Invalid MAC address: {}".format(self.mac))
return b"\xff" * 6 + bytes.fromhex(hexMac) * 16
Comment on lines +98 to +103
# [ network: optional ] - Network/wake controller for the slot
# Network-level device control (distinct from powerSwitch); wake-only.
# supported types:
# [type: "wol", mac: "aa:bb:cc:dd:ee:ff", broadcast:"optional (default 255.255.255.255)", port:"optional (default 9)"]
# Wake-on-LAN magic packet - wakes a sleeping / soft-off device on the
# same broadcast domain. Call dut.networkController.wake().
Comment on lines +70 to +77
def _magicPacket(self):
"""Build the Wake-on-LAN magic packet: 6x 0xFF followed by the MAC x16."""
hexMac = self.mac.replace(":", "").replace("-", "").replace(".", "")
if len(hexMac) != 12:
raise ValueError("Invalid MAC address: {}".format(self.mac))
return b"\xff" * 6 + bytes.fromhex(hexMac) * 16

def wake(self):
…ake seam

Wake-on-LAN is a network (layer-2) action, not a power action: it sends a
UDP magic packet, cannot power a device off, and supports none of the power
metering contract. Move it out of powerModules onto a dedicated network
controller that mirrors the existing device-controller pattern
(remoteController / hdmiCECController / avSyncController).

- Add framework/core/networkModules/wol.py (networkWol) with a single wake()
  verb (renamed from powerOn; drop the power-only powerOff/reboot shims).
- Add framework/core/networkControl.py (networkControlClass) dispatching
  type: "wol", with retry, mirroring powerControl.
- Wire a device `network:` config block in deviceManager -> dut.networkController;
  expose it in testControl.
- Revert the power-seam coupling: remove the wol import + type:"wol" branch
  from powerControl.py and delete powerModules/wol.py.
- Document the `network:` block in the example rack config.
- Add tests/networkControl_tests.py (packet correctness, loopback capture,
  MAC validation, unknown-type handling).

Config now:

    network:
        type: "wol"
        mac: "aa:bb:cc:dd:ee:ff"
        broadcast: "255.255.255.255"   # optional
        port: 9                        # optional

Waking is now an explicit dut.networkController.wake() rather than riding on
powerControl.powerOn().

Relates to rdkcentral#216
Copilot AI review requested due to automatic review settings July 20, 2026 10:14
@Ulrond
Ulrond force-pushed the feature/216-wake-on-lan-power-module branch from 64dff49 to 1a27ddd Compare July 20, 2026 10:14
@Ulrond

Ulrond commented Jul 20, 2026

Copy link
Copy Markdown
Contributor Author

Updated and ready for review. This revision re-homes Wake-on-LAN from the power seam onto a dedicated network/wake controller (networkControlClass, device network: block, dut.networkController.wake()), reverts the powerModules coupling, and is rebased on latest develop. Docs updated (AGENTS.md §5.2, device_manager.md). Usage is deliberately minimal:

dut.networkController.wake()

@TB-1993 could you take another look and approve when happy? Thanks 🙏

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 9 out of 9 changed files in this pull request and generated 3 comments.

Comments suppressed due to low confidence (1)

framework/core/networkControl.py:59

  • If config['type'] is unknown, this constructor only logs an error and leaves self.networkModule as None. A subsequent wake() call will then raise AttributeError when it tries to access self.networkModule.wake. Also, assigning to a local named type shadows the Python built-in.
        self.networkModule = None
        type = config.get("type")
        if type == "wol":
            self.networkModule = networkWol(log, config.get("mac"), config.get("broadcast", "255.255.255.255"), config.get("port", 9))
        else:
            self.log.error("Network controller type [{}] unknown".format(type))

Comment on lines +83 to +92
for x in range(self.retryCount+1):
try:
result = networkMethod()
break
except Exception as e:
self.log.info(f"Could not perform {[networkMethod.__name__]}. Retry count: [{x}] of [{self.retryCount}], delay is [{self.retryDelay}]")
if x == self.retryCount:
raise e
self.utils.wait(self.retryDelay)
return result
Comment on lines +83 to +92
try:
packet = self._magicPacket()
with socket.socket(socket.AF_INET, socket.SOCK_DGRAM) as sock:
sock.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1)
sock.sendto(packet, (self.broadcast, self.port))
self.log.info("networkWol().wake: magic packet sent to {}".format(self.mac))
return True
except Exception as error:
self.log.error("networkWol().wake failed: {}".format(error))
return False
Comment thread AGENTS.md
Comment on lines +354 to +357
(default 30s). `powerControlClass` (config `powerSwitch:`) owns device power; the
separate `networkControlClass` (config `network:`) owns network-level actions such as
Wake-on-LAN. They are intentionally distinct: Wake-on-LAN is a network (layer-2)
action, not a power action, so it lives in `networkModules/`, not `powerModules/`.
self.log.info("wake ({})".format(self.name))
return self.networkRetry(self.networkModule.wake)

def networkRetry(self, networkMethod):

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should probably get people to use the backoff library for this https://pypi.org/project/backoff/. I think this method becomes a nightmare if we ever add any methods that require arguments.

import socket


class networkWol():

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I was thinking the networkController class would be where we would implement interacting with different network switches. So users could look up MAC addresses, IPs, enable/disable ports firewalls etc. So I was thinking this could just be a generic Host based network controller. With methods implemented to run on the host, providing some of that functionality.

What do you think?

Comment on lines +50 to +52
# If variables are not passed in the config they will be defaulted to retryCount: [1], retryDelay: [30]
self.retryCount = config.get("retryCount", 1)
self.retryDelay = config.get("retryDelay", 30)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should retries really come from the config. Not the test?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Add a Wake-on-LAN network/wake module (type: wol)

3 participants