public inbox for igt-dev@lists.freedesktop.org
 help / color / mirror / Atom feed
From: Kunal Joshi <kunal1.joshi@intel.com>
To: igt-dev@lists.freedesktop.org
Cc: Kunal Joshi <kunal1.joshi@intel.com>,
	Arun R Murthy <arun.r.murthy@intel.com>
Subject: [PATCH i-g-t 4/6] lib/igt_usb4_switch: add helper library for USB4 Switch 3141
Date: Thu,  9 Apr 2026 10:07:12 +0530	[thread overview]
Message-ID: <20260409043714.284108-5-kunal1.joshi@intel.com> (raw)
In-Reply-To: <20260409043714.284108-1-kunal1.joshi@intel.com>

Add a control library for the Microsft USB4 Switch 3141 test equipment,
used for automated USB4/Thunderbolt dock/undock testing:

 - usb4switch_init()/deinit(): lifecycle with
   .igtrc config or autodiscovery
 - usb4switch_port_enable()/disable(): port control with optional HW delay
 - usb4switch_get_active_port(): query current port state
 - usb4switch_get_voltage_mv()/get_current_ma(): VBUS monitoring
 - usb4switch_get_orientation(): USB-C cable orientation

Signed-off-by: Kunal Joshi <kunal1.joshi@intel.com>
Reviewed-by: Arun R Murthy <arun.r.murthy@intel.com>
---
 lib/igt_usb4_switch.c | 1055 +++++++++++++++++++++++++++++++++++++++++
 lib/igt_usb4_switch.h |  157 ++++++
 lib/meson.build       |    1 +
 3 files changed, 1213 insertions(+)
 create mode 100644 lib/igt_usb4_switch.c
 create mode 100644 lib/igt_usb4_switch.h

diff --git a/lib/igt_usb4_switch.c b/lib/igt_usb4_switch.c
new file mode 100644
index 000000000..5cede4d60
--- /dev/null
+++ b/lib/igt_usb4_switch.c
@@ -0,0 +1,1055 @@
+// SPDX-License-Identifier: MIT
+/*
+ * Copyright © 2026 Intel Corporation
+ */
+
+/**
+ * SECTION:igt_usb4_switch
+ * @short_description: USB4 Switch 3141 control library
+ * @title: USB4 Switch
+ * @include: igt_usb4_switch.h
+ *
+ * This library provides control and status functions for the Microsoft
+ * USB4 Switch 3141 test equipment, used for automated USB4/Thunderbolt
+ * dock/undock testing.
+ *
+ * The library handles serial communication, port control, and
+ * configuration parsing.
+ */
+
+#include <stdlib.h>
+#include <string.h>
+#include <strings.h>
+#include <time.h>
+#include <unistd.h>
+
+#include "igt_core.h"
+#include "igt_rc.h"
+#include "igt_serial.h"
+#include "igt_usb4_switch.h"
+#include "igt_connector_helper.h"
+
+struct usb4switch {
+	struct igt_serial *serial;
+
+	/* Configuration */
+	char *device_path;
+	int hotplug_timeout_s;
+	int iterations;
+	int min_switch_interval_ms;
+
+	/* Timestamp of last port change for minimum interval enforcement */
+	struct timespec last_port_change;
+
+	/* Port configuration */
+	struct usb4switch_port ports[USB4_SWITCH_MAX_PORTS];
+	int port_count;
+};
+
+/* .igtrc section and key names */
+#define USB4_SWITCH_SECTION "USB4Switch"
+#define USB4_SWITCH_KEY_DEVICE "Device"
+#define USB4_SWITCH_KEY_HOTPLUG_TIMEOUT "HotplugTimeout"
+#define USB4_SWITCH_KEY_ITERATIONS "DockUndockIterations"
+#define USB4_SWITCH_KEY_MIN_INTERVAL "MinSwitchInterval"
+
+/*
+ * response_is_ok - Check that a firmware response does not indicate an error.
+ *
+ * The 3141 prefixes error replies with "ERR" (e.g. "ERR: unknown command").
+ * An empty response after a successful transport exchange is also treated as
+ * failure because no meaningful control command produces an empty reply.
+ *
+ * This is a transport vs. command-level distinction: send_command() only
+ * guarantees that bytes were exchanged; callers that need to know whether
+ * the firmware actually accepted the command must call response_is_ok().
+ */
+static bool response_is_ok(const char *response)
+{
+	if (!response || response[0] == '\0')
+		return false;
+
+	if (strncasecmp(response, "ERR", 3) == 0)
+		return false;
+
+	return true;
+}
+
+/*
+ * Minimum-interval helpers - enforce a floor between successive port
+ * operations to protect hardware from rapid switching.
+ */
+static void enforce_min_interval(struct usb4switch *sw)
+{
+	struct timespec now;
+	int elapsed_ms;
+
+	if (sw->last_port_change.tv_sec == 0 &&
+	    sw->last_port_change.tv_nsec == 0)
+		return;
+
+	clock_gettime(CLOCK_MONOTONIC, &now);
+	elapsed_ms = (now.tv_sec - sw->last_port_change.tv_sec) * 1000 +
+		     (now.tv_nsec - sw->last_port_change.tv_nsec) / 1000000;
+
+	if (elapsed_ms < sw->min_switch_interval_ms) {
+		int wait = sw->min_switch_interval_ms - elapsed_ms;
+
+		igt_debug("USB4Switch: enforcing %d ms minimum interval "
+			  "(sleeping %d ms)\n",
+			  sw->min_switch_interval_ms, wait);
+		usleep(wait * 1000);
+	}
+}
+
+/**
+ * send_command:
+ * @sw: switch handle
+ * @cmd: command string (without line ending)
+ * @response: buffer for response
+ * @resp_size: size of response buffer
+ *
+ * Sends a command to the USB4 Switch 3141.
+ * Appends CRLF as required by the switch protocol.
+ *
+ * Note: success means the serial exchange completed and at least one byte
+ * was received. It does not validate the firmware response payload — callers
+ * that care about command-level success must check response_is_ok().
+ *
+ * Returns: true if the exchange succeeded, false on transport error.
+ */
+static bool send_command(struct usb4switch *sw, const char *cmd,
+			 char *response, size_t resp_size)
+{
+	char cmd_with_crlf[256];
+	int len;
+
+	if (!sw || !sw->serial || !cmd)
+		return false;
+
+	len = snprintf(cmd_with_crlf, sizeof(cmd_with_crlf), "%s\r\n", cmd);
+	if (len < 0 || (size_t)len >= sizeof(cmd_with_crlf))
+		return false;
+
+	return igt_serial_command(sw->serial, cmd_with_crlf,
+				  response, resp_size);
+}
+
+static bool verify_communication(struct usb4switch *sw)
+{
+	char response[256];
+
+	if (!send_command(sw, "version", response, sizeof(response))) {
+		igt_debug("USB4Switch: Failed to query version\n");
+		return false;
+	}
+
+	if (!response_is_ok(response)) {
+		igt_debug("USB4Switch: Version command rejected: '%s'\n",
+			  response);
+		return false;
+	}
+
+	/*
+	 * The device was already identified by VID:PID in
+	 * discover_usb4_switch_device(). Here we just confirm the serial
+	 * link is alive.  The firmware returns only its firmware version
+	 * (e.g. "0201"), not the model number.
+	 */
+	igt_debug("USB4Switch: Version response: %s\n", response);
+	return true;
+}
+
+static char *get_config_string(const char *key)
+{
+	char *value = NULL;
+	GError *error = NULL;
+
+	value = g_key_file_get_string(igt_key_file, USB4_SWITCH_SECTION,
+				      key, &error);
+	if (error) {
+		g_error_free(error);
+		return NULL;
+	}
+	return value;
+}
+
+static int get_config_int(const char *key, int default_value)
+{
+	GError *error = NULL;
+	int value;
+
+	value = g_key_file_get_integer(igt_key_file, USB4_SWITCH_SECTION,
+				       key, &error);
+	if (error) {
+		g_error_free(error);
+		return default_value;
+	}
+	return value;
+}
+
+static void free_display_config(struct usb4switch_display *disp)
+{
+	free(disp->connector_path);
+	disp->connector_path = NULL;
+	free(disp->connector_name);
+	disp->connector_name = NULL;
+	free(disp->edid_serial);
+	disp->edid_serial = NULL;
+}
+
+static void free_port_config(struct usb4switch_port *port)
+{
+	int i;
+
+	free(port->name);
+	for (i = 0; i < USB4_SWITCH_MAX_DISPLAYS_PER_PORT; i++)
+		free_display_config(&port->displays[i]);
+}
+
+static void free_config(struct usb4switch *sw)
+{
+	int i;
+
+	free(sw->device_path);
+	for (i = 0; i < USB4_SWITCH_MAX_PORTS; i++)
+		free_port_config(&sw->ports[i]);
+}
+
+static void parse_display_config(struct usb4switch *sw, int port_idx,
+				 int disp_idx)
+{
+	struct usb4switch_display *disp;
+	char key[64];
+
+	disp = &sw->ports[port_idx].displays[disp_idx];
+
+	/* Path takes precedence for MST */
+	snprintf(key, sizeof(key), "Port%d.Display%d.Path",
+		 port_idx + 1, disp_idx + 1);
+	disp->connector_path = get_config_string(key);
+
+	/* Connector name as fallback */
+	snprintf(key, sizeof(key), "Port%d.Display%d.Connector",
+		 port_idx + 1, disp_idx + 1);
+	disp->connector_name = get_config_string(key);
+
+	/* EDID serial for verification */
+	snprintf(key, sizeof(key), "Port%d.Display%d.EDIDSerial",
+		 port_idx + 1, disp_idx + 1);
+	disp->edid_serial = get_config_string(key);
+}
+
+static void parse_port_config(struct usb4switch *sw, int port_idx)
+{
+	struct usb4switch_port *port;
+	char key[64];
+	int i;
+
+	port = &sw->ports[port_idx];
+	port->port_num = port_idx + 1;
+
+	snprintf(key, sizeof(key), "Port%d.Name", port_idx + 1);
+	port->name = get_config_string(key);
+	if (!port->name) {
+		port->name = malloc(16);
+		if (port->name)
+			snprintf(port->name, 16, "Port %d", port_idx + 1);
+	}
+
+	/*
+	 * Parse all display slots, then count the leading contiguous
+	 * prefix.  display_count is the exact number of valid entries
+	 * accessible via displays[0..display_count-1].
+	 */
+	for (i = 0; i < USB4_SWITCH_MAX_DISPLAYS_PER_PORT; i++)
+		parse_display_config(sw, port_idx, i);
+
+	/* Count leading contiguous displays (Display1, Display2, ...) */
+	port->display_count = 0;
+	for (i = 0; i < USB4_SWITCH_MAX_DISPLAYS_PER_PORT; i++) {
+		if (sw->ports[port_idx].displays[i].connector_path ||
+		    sw->ports[port_idx].displays[i].connector_name)
+			port->display_count++;
+		else
+			break;
+	}
+
+	/* Warn about and free any sparse entries beyond the contiguous prefix */
+	for (; i < USB4_SWITCH_MAX_DISPLAYS_PER_PORT; i++) {
+		if (sw->ports[port_idx].displays[i].connector_path ||
+		    sw->ports[port_idx].displays[i].connector_name) {
+			igt_warn("USB4Switch: Port%d.Display%d configured "
+				 "but Display%d is missing; displays must "
+				 "be contiguous from 1 — ignoring\n",
+				 port_idx + 1, i + 1,
+				 port->display_count + 1);
+			free_display_config(&sw->ports[port_idx].displays[i]);
+		}
+	}
+}
+
+static bool parse_config(struct usb4switch *sw)
+{
+	int i;
+
+	sw->device_path = get_config_string(USB4_SWITCH_KEY_DEVICE);
+	if (!sw->device_path) {
+		igt_debug("USB4Switch: No Device configured\n");
+		return false;
+	}
+
+	sw->hotplug_timeout_s =
+		get_config_int(USB4_SWITCH_KEY_HOTPLUG_TIMEOUT,
+			       USB4_SWITCH_DEFAULT_HOTPLUG_TIMEOUT_S);
+	sw->iterations =
+		get_config_int(USB4_SWITCH_KEY_ITERATIONS,
+			       USB4_SWITCH_DEFAULT_ITERATIONS);
+	sw->min_switch_interval_ms =
+		get_config_int(USB4_SWITCH_KEY_MIN_INTERVAL,
+			       USB4_SWITCH_DEFAULT_MIN_INTERVAL_MS);
+
+	/*
+	 * Count only leading contiguous ports that have at least one display.
+	 * Stop at the first port with no displays — this enforces contiguous
+	 * port numbering (Port1, Port2, ...) and means port_count is the exact
+	 * number of valid ports accessible via ports[0..port_count-1].
+	 */
+	sw->port_count = 0;
+	for (i = 0; i < USB4_SWITCH_MAX_PORTS; i++) {
+		parse_port_config(sw, i);
+		if (sw->ports[i].display_count > 0)
+			sw->port_count++;
+		else
+			break;
+	}
+
+	if (sw->port_count == 0)
+		igt_warn("USB4Switch: Device configured but no display keys found\n");
+
+	igt_debug("USB4Switch: Device=%s, Timeout=%ds, Iterations=%d, Ports=%d\n",
+		  sw->device_path, sw->hotplug_timeout_s, sw->iterations,
+		  sw->port_count);
+
+	return true;
+}
+
+static void stamp_port_change(struct usb4switch *sw)
+{
+	clock_gettime(CLOCK_MONOTONIC, &sw->last_port_change);
+}
+
+static bool usb4switch_queue_port_change_delayed(struct usb4switch *sw,
+						 int port,
+						 int delay_seconds,
+						 const char *action)
+{
+	char cmd[32];
+	char response[256];
+
+	if (!sw)
+		return false;
+
+	if (port < 0 || port > USB4_SWITCH_MAX_PORTS) {
+		igt_warn("USB4Switch: Invalid port %d\n", port);
+		return false;
+	}
+
+	if (delay_seconds < 0 || delay_seconds > 3600) {
+		igt_warn("USB4Switch: Invalid delay %d (must be 0-3600)\n",
+			 delay_seconds);
+		return false;
+	}
+
+	enforce_min_interval(sw);
+
+	snprintf(cmd, sizeof(cmd), "delay %d", delay_seconds);
+	if (!send_command(sw, cmd, response, sizeof(response))) {
+		igt_warn("USB4Switch: Failed to set delay %d\n", delay_seconds);
+		return false;
+	}
+
+	if (!response_is_ok(response)) {
+		igt_warn("USB4Switch: Failed to set delay %d (response: '%s')\n",
+			 delay_seconds, response);
+		return false;
+	}
+
+	snprintf(cmd, sizeof(cmd), "port %d", port);
+	if (!send_command(sw, cmd, response, sizeof(response))) {
+		igt_warn("USB4Switch: Failed to queue %s command\n", action);
+		return false;
+	}
+
+	if (!response_is_ok(response)) {
+		igt_warn("USB4Switch: Failed to queue %s command (response: '%s')\n",
+			 action, response);
+		return false;
+	}
+
+	stamp_port_change(sw);
+	igt_debug("USB4Switch: Queued %s with %ds delay\n",
+		  action, delay_seconds);
+
+	return true;
+}
+
+/*
+ * USB VID:PID for Microsoft USB4 Switch 3141
+ */
+#define USB4_SWITCH_VID "045e"
+#define USB4_SWITCH_PID "0646"
+
+/*
+ * Linux USB uevent PRODUCT= format uses lowercase hex without leading
+ * zeros: VID 0x045e → "45e", PID 0x0646 → "646".
+ */
+#define USB4_SWITCH_UEVENT_MATCH "PRODUCT=45e/646/"
+
+/*
+ * When no .igtrc configuration is present, autodiscover:
+ * - USB4 switch device by scanning /sys/class/tty for matching VID:PID
+ * - Displays behind each port by comparing connectors before/after docking
+ */
+static char *discover_usb4_switch_device(void)
+{
+	char path[256];
+	char uevent[512];
+	char *device_path = NULL;
+	FILE *fp;
+	int i;
+
+	/*
+	 * Scan ttyACM0..63. The 3141 almost always lands in the low
+	 * range, but the upper bound is generous to handle busy systems.
+	 */
+	for (i = 0; i < 64; i++) {
+		snprintf(path, sizeof(path),
+			 "/sys/class/tty/ttyACM%d/device/uevent", i);
+
+		fp = fopen(path, "r");
+		if (!fp)
+			continue;
+
+		while (fgets(uevent, sizeof(uevent), fp)) {
+			if (strstr(uevent, USB4_SWITCH_UEVENT_MATCH)) {
+				device_path = malloc(32);
+				if (device_path)
+					snprintf(device_path, 32,
+						 "/dev/ttyACM%d", i);
+				break;
+			}
+		}
+
+		fclose(fp);
+		if (device_path)
+			break;
+	}
+
+	if (device_path)
+		igt_debug("USB4Switch: Autodiscovered device at %s\n",
+			  device_path);
+	else
+		igt_debug("USB4Switch: No device found during autodiscovery\n");
+
+	return device_path;
+}
+
+/*
+ * discover_port_displays - Dock a port and compare connectors to find
+ * which displays appear. drm_fd is passed as parameter, NOT stored.
+ */
+static bool discover_port_displays(struct usb4switch *sw, int drm_fd,
+				   int port_num, uint32_t *before,
+				   int before_count)
+{
+	struct usb4switch_port *port = &sw->ports[port_num - 1];
+	uint32_t after[32];
+	int after_count;
+	char name[32];
+	char serial[64];
+	int i, j, wait;
+	bool found;
+
+	if (!usb4switch_port_enable(sw, port_num)) {
+		igt_debug("USB4Switch: Failed to enable port %d\n", port_num);
+		return false;
+	}
+
+	igt_debug("USB4Switch: Waiting for port %d displays...\n", port_num);
+	/* Poll for new connectors (max 10s) instead of fixed sleep */
+	after_count = 0;
+	for (wait = 0; wait < 10; wait++) {
+		sleep(1);
+		igt_connector_reprobe_all(drm_fd);
+		after_count = igt_connector_get_connected(drm_fd, after, 32);
+		if (after_count > before_count)
+			break;
+	}
+
+	if (after_count <= before_count) {
+		igt_debug("USB4Switch: No new connectors detected on port %d\n",
+			  port_num);
+		usb4switch_port_disable_and_wait(sw);
+		return false;
+	}
+
+	port->port_num = port_num;
+	port->name = malloc(16);
+	if (port->name)
+		snprintf(port->name, 16, "port-%d", port_num);
+	port->display_count = 0;
+
+	for (i = 0; i < after_count; i++) {
+		found = false;
+		for (j = 0; j < before_count; j++) {
+			if (after[i] == before[j]) {
+				found = true;
+				break;
+			}
+		}
+
+		if (!found &&
+		    port->display_count < USB4_SWITCH_MAX_DISPLAYS_PER_PORT) {
+			char path[256];
+
+			if (igt_connector_get_info(drm_fd, after[i],
+						   name, sizeof(name),
+						   serial, sizeof(serial),
+						   path, sizeof(path))) {
+				struct usb4switch_display *disp;
+
+				disp = &port->displays[port->display_count];
+				disp->connector_name = strdup(name);
+				if (!disp->connector_name)
+					continue;
+				disp->edid_serial = serial[0] ?
+						    strdup(serial) : NULL;
+				disp->connector_path = path[0] ?
+						       strdup(path) : NULL;
+
+				igt_info("USB4Switch: Port %d Display %d: %s (EDID: %s)\n",
+					 port_num, port->display_count + 1,
+					 name,
+					 serial[0] ? serial : "none");
+
+				port->display_count++;
+			}
+		}
+	}
+
+	usb4switch_port_disable_and_wait(sw);
+	igt_connector_reprobe_all(drm_fd);
+
+	if (port->display_count > 0)
+		sw->port_count++;
+
+	return port->display_count > 0;
+}
+
+/*
+ * autodiscover_config - Autodiscover switch device and port displays.
+ */
+static bool autodiscover_config(struct usb4switch *sw, int drm_fd)
+{
+	uint32_t before[32];
+	int before_count;
+	int p;
+
+	sw->device_path = discover_usb4_switch_device();
+	if (!sw->device_path)
+		return false;
+
+	sw->hotplug_timeout_s = USB4_SWITCH_DEFAULT_HOTPLUG_TIMEOUT_S;
+	sw->iterations = USB4_SWITCH_DEFAULT_ITERATIONS;
+	sw->min_switch_interval_ms = USB4_SWITCH_DEFAULT_MIN_INTERVAL_MS;
+	sw->port_count = 0;
+
+	sw->serial = igt_serial_open(sw->device_path, 115200);
+	if (!sw->serial) {
+		igt_debug("USB4Switch: Failed to open autodiscovered device %s\n",
+			  sw->device_path);
+		free(sw->device_path);
+		sw->device_path = NULL;
+		return false;
+	}
+
+	if (!verify_communication(sw)) {
+		igt_serial_close(sw->serial);
+		sw->serial = NULL;
+		free(sw->device_path);
+		sw->device_path = NULL;
+		return false;
+	}
+
+	igt_info("USB4Switch: Autodiscovering displays...\n");
+
+	usb4switch_port_disable_and_wait(sw);
+
+	igt_connector_reprobe_all(drm_fd);
+
+	before_count = igt_connector_get_connected(drm_fd, before, 32);
+	if (before_count >= 32)
+		igt_warn("USB4Switch: Connector count %d may exceed array size\n",
+			 before_count);
+	igt_debug("USB4Switch: %d connectors before autodiscovery\n",
+		  before_count);
+
+	/*
+	 * Probe ports sequentially. Stop at the first port that yields no
+	 * displays — same contiguity rule as config-based port counting.
+	 */
+	for (p = 1; p <= USB4_SWITCH_MAX_PORTS; p++) {
+		if (!discover_port_displays(sw, drm_fd, p, before, before_count))
+			break;
+	}
+
+	usb4switch_port_disable(sw);
+
+	igt_info("USB4Switch: Autodiscovery complete - %d ports with displays\n",
+		 sw->port_count);
+
+	return sw->port_count > 0;
+}
+
+/**
+ * usb4switch_init:
+ * @drm_fd: DRM file descriptor (used only for autodiscovery, not stored)
+ *
+ * Initializes USB4 switch from .igtrc configuration.
+ * If no configuration is present, attempts
+ * autodiscovery of the switch device and displays behind each port.
+ *
+ * Returns: Switch handle on success, NULL on failure.
+ */
+struct usb4switch *usb4switch_init(int drm_fd)
+{
+	struct usb4switch *sw;
+	bool config_ok;
+
+	sw = calloc(1, sizeof(*sw));
+	if (!sw)
+		return NULL;
+
+	config_ok = parse_config(sw);
+
+	if (!config_ok) {
+		if (drm_fd < 0) {
+			igt_debug("USB4Switch: No config and no DRM fd for autodiscovery\n");
+			free_config(sw);
+			free(sw);
+			return NULL;
+		}
+
+		igt_info("USB4Switch: No configuration found, attempting autodiscovery...\n");
+		if (!autodiscover_config(sw, drm_fd)) {
+			igt_debug("USB4Switch: Autodiscovery failed\n");
+			if (sw->serial)
+				igt_serial_close(sw->serial);
+			free_config(sw);
+			free(sw);
+			return NULL;
+		}
+		igt_info("USB4Switch: Initialized via autodiscovery (Model 3141)\n");
+		return sw;
+	}
+
+	sw->serial = igt_serial_open(sw->device_path, 115200);
+	if (!sw->serial) {
+		igt_debug("USB4Switch: Failed to open serial port %s\n",
+			  sw->device_path);
+		free_config(sw);
+		free(sw);
+		return NULL;
+	}
+
+	if (!verify_communication(sw)) {
+		igt_serial_close(sw->serial);
+		free_config(sw);
+		free(sw);
+		return NULL;
+	}
+
+	igt_info("USB4Switch: Initialized (Model 3141)\n");
+	return sw;
+}
+
+/**
+ * usb4switch_deinit:
+ * @sw: switch handle
+ *
+ * Closes the switch and frees resources.
+ * Disables all ports before closing.
+ */
+void usb4switch_deinit(struct usb4switch *sw)
+{
+	if (!sw)
+		return;
+
+	usb4switch_port_disable(sw);
+
+	if (sw->serial)
+		igt_serial_close(sw->serial);
+	free_config(sw);
+	free(sw);
+
+	igt_debug("USB4Switch: Deinitialized\n");
+}
+
+/**
+ * usb4switch_port_enable:
+ * @sw: switch handle
+ * @port: port number (1 or 2)
+ *
+ * Enables the specified port.
+ *
+ * Returns: true on success, false on failure.
+ */
+bool usb4switch_port_enable(struct usb4switch *sw, int port)
+{
+	char cmd[32];
+	char response[256];
+
+	if (!sw || port < 1 || port > USB4_SWITCH_MAX_PORTS)
+		return false;
+
+	enforce_min_interval(sw);
+
+	snprintf(cmd, sizeof(cmd), "port %d", port);
+
+	if (!send_command(sw, cmd, response, sizeof(response))) {
+		igt_warn("USB4Switch: Failed to enable port %d\n", port);
+		return false;
+	}
+	if (!response_is_ok(response)) {
+		igt_warn("USB4Switch: Failed to enable port %d (response: '%s')\n",
+			 port, response);
+		return false;
+	}
+
+	stamp_port_change(sw);
+	igt_debug("USB4Switch: Enabled port %d\n", port);
+	return true;
+}
+
+/**
+ * usb4switch_port_enable_delayed:
+ * @sw: switch handle
+ * @port: port number (1 or 2)
+ * @delay_seconds: delay in seconds before port change executes
+ *
+ * Schedules a port enable after the specified delay using the hardware
+ * delay command on Model 3141. Useful for hotplug-during-suspend tests.
+ *
+ * Returns: true on success, false on failure.
+ */
+bool usb4switch_port_enable_delayed(struct usb4switch *sw, int port,
+				    int delay_seconds)
+{
+	return usb4switch_queue_port_change_delayed(sw, port, delay_seconds,
+						    "port enable");
+}
+
+/**
+ * usb4switch_port_disable_delayed:
+ * @sw: switch handle
+ * @delay_seconds: delay in seconds before port disable executes
+ *
+ * Schedules a port disable (undock) after the specified delay.
+ * Useful for undock-during-suspend tests.
+ *
+ * Returns: true on success, false on failure.
+ */
+bool usb4switch_port_disable_delayed(struct usb4switch *sw, int delay_seconds)
+{
+	return usb4switch_queue_port_change_delayed(sw, 0, delay_seconds,
+						    "port disable");
+}
+
+/**
+ * usb4switch_port_disable:
+ * @sw: switch handle
+ *
+ * Disables all ports.
+ *
+ * Returns: true on success, false on failure.
+ */
+bool usb4switch_port_disable(struct usb4switch *sw)
+{
+	char response[256];
+
+	if (!sw)
+		return false;
+
+	enforce_min_interval(sw);
+
+	if (!send_command(sw, "port 0", response, sizeof(response))) {
+		igt_warn("USB4Switch: Failed to disable ports\n");
+		return false;
+	}
+	if (!response_is_ok(response)) {
+		igt_warn("USB4Switch: Failed to disable ports (response: '%s')\n",
+			 response);
+		return false;
+	}
+
+	stamp_port_change(sw);
+	igt_debug("USB4Switch: Disabled all ports\n");
+	return true;
+}
+
+/**
+ * usb4switch_get_active_port:
+ * @sw: switch handle
+ *
+ * Gets currently active port.
+ *
+ * Returns: Port number (1 or 2), 0 if disabled, -1 on error.
+ */
+int usb4switch_get_active_port(struct usb4switch *sw)
+{
+	char response[256];
+	int port;
+
+	if (!sw)
+		return -1;
+
+	if (!send_command(sw, "port ?", response, sizeof(response)))
+		return -1;
+
+	if (!response_is_ok(response)) {
+		igt_warn("USB4Switch: Port query rejected: '%s'\n", response);
+		return -1;
+	}
+
+	if (sscanf(response, "port: %d", &port) == 1 ||
+	    sscanf(response, "port %d", &port) == 1 ||
+	    sscanf(response, "%d", &port) == 1)
+		return port;
+
+	igt_warn("USB4Switch: Could not parse port response: '%s'\n",
+		 response);
+	return -1;
+}
+
+/**
+ * usb4switch_get_status:
+ * @sw: switch handle
+ * @buf: buffer for status
+ * @size: buffer size
+ *
+ * Gets full status from switch.
+ *
+ * Returns: true on success.
+ */
+bool usb4switch_get_status(struct usb4switch *sw, char *buf, size_t size)
+{
+	if (!sw || !buf || size == 0)
+		return false;
+
+	if (!send_command(sw, "status", buf, size))
+		return false;
+
+	return response_is_ok(buf);
+}
+
+/**
+ * usb4switch_get_voltage_mv:
+ * @sw: switch handle
+ *
+ * Gets VBUS voltage by sending the "volts" command.
+ * The 3141 firmware responds with "X.XX V" (e.g. "4.85 V").
+ *
+ * Note: The 3141 hardware may not have accurate VBUS sense circuitry.
+ * Voltage readback may not be meaningful on all units.
+ *
+ * Returns: Voltage in mV, -1 on error.
+ */
+int usb4switch_get_voltage_mv(struct usb4switch *sw)
+{
+	char response[256];
+	float volts;
+
+	if (!sw)
+		return -1;
+
+	if (!send_command(sw, "volts", response, sizeof(response)))
+		return -1;
+
+	if (sscanf(response, "%f", &volts) == 1)
+		return (int)(volts * 1000);
+
+	return -1;
+}
+
+/**
+ * usb4switch_get_current_ma:
+ * @sw: switch handle
+ *
+ * Gets VBUS current by sending the "amps" command.
+ * The 3141 firmware responds with "XXX mA" (e.g. "125 mA").
+ *
+ * Note: The 3141 hardware may not have accurate current sense circuitry.
+ *
+ * Returns: Current in mA, -1 on error.
+ */
+int usb4switch_get_current_ma(struct usb4switch *sw)
+{
+	char response[256];
+	int current;
+
+	if (!sw)
+		return -1;
+
+	if (!send_command(sw, "amps", response, sizeof(response)))
+		return -1;
+
+	if (sscanf(response, "%d", &current) == 1)
+		return current;
+
+	return -1;
+}
+
+/**
+ * usb4switch_wait_vbus_safe:
+ * @sw: switch handle
+ * @timeout_ms: maximum time to wait in milliseconds
+ *
+ * Waits for VBUS voltage to drop below the Vsafe0V threshold (800 mV)
+ * after a port disable. Polls using the "volts" command. If voltage
+ * readback is not supported or returns errors, falls back to a fixed
+ * 1-second delay.
+ *
+ * Returns: true when VBUS is safe (or after fallback delay).
+ */
+bool usb4switch_wait_vbus_safe(struct usb4switch *sw, int timeout_ms)
+{
+	int elapsed = 0;
+	int mv = -1;
+
+	if (!sw)
+		return false;
+
+	while (elapsed < timeout_ms) {
+		mv = usb4switch_get_voltage_mv(sw);
+		if (mv < 0) {
+			/* Voltage readback not supported; use fixed delay */
+			igt_debug("USB4Switch: voltage readback failed, "
+				  "using 1s fallback delay\n");
+			sleep(1);
+			return true;
+		}
+		if (mv < USB4_SWITCH_VBUS_SAFE_MV) {
+			igt_debug("USB4Switch: VBUS safe at %d mV\n", mv);
+			return true;
+		}
+		usleep(USB4_SWITCH_VBUS_SAFE_POLL_MS * 1000);
+		elapsed += USB4_SWITCH_VBUS_SAFE_POLL_MS;
+	}
+
+	igt_warn("USB4Switch: VBUS still at %d mV after %d ms\n",
+		 mv, timeout_ms);
+	return false;
+}
+
+/**
+ * usb4switch_port_disable_and_wait:
+ * @sw: switch handle
+ *
+ * Disables all ports and waits for VBUS to reach safe levels.
+ *
+ * Returns: true on success.
+ */
+bool usb4switch_port_disable_and_wait(struct usb4switch *sw)
+{
+	if (!usb4switch_port_disable(sw))
+		return false;
+
+	return usb4switch_wait_vbus_safe(sw, USB4_SWITCH_VBUS_SAFE_TIMEOUT_MS);
+}
+
+/**
+ * usb4switch_port_switch:
+ * @sw: switch handle
+ * @new_port: target port number (1..MAX_PORTS)
+ *
+ * Safely switches from the currently active port to @new_port.
+ * If another port is active, disables it first and waits for VBUS
+ * discharge before enabling the new port. If @new_port is already
+ * active, this is a no-op.
+ *
+ * Returns: true on success.
+ */
+bool usb4switch_port_switch(struct usb4switch *sw, int new_port)
+{
+	int active;
+
+	if (!sw || new_port < 1 || new_port > USB4_SWITCH_MAX_PORTS)
+		return false;
+
+	active = usb4switch_get_active_port(sw);
+
+	if (active == new_port) {
+		igt_debug("USB4Switch: Port %d already active\n", new_port);
+		return true;
+	}
+
+	if (active > 0) {
+		igt_debug("USB4Switch: Disabling port %d before switching "
+			  "to port %d\n", active, new_port);
+		if (!usb4switch_port_disable_and_wait(sw))
+			return false;
+	}
+
+	return usb4switch_port_enable(sw, new_port);
+}
+
+/**
+ * usb4switch_get_port_config:
+ * @sw: switch handle
+ * @port_index: port index (0-based)
+ *
+ * Returns: Pointer to port config, NULL if invalid.
+ */
+const struct usb4switch_port *usb4switch_get_port_config(struct usb4switch *sw,
+							 int port_index)
+{
+	if (!sw || port_index < 0 || port_index >= sw->port_count)
+		return NULL;
+
+	return &sw->ports[port_index];
+}
+
+/**
+ * usb4switch_get_port_count:
+ * @sw: switch handle
+ *
+ * Returns: Number of configured ports.
+ */
+int usb4switch_get_port_count(struct usb4switch *sw)
+{
+	return sw ? sw->port_count : 0;
+}
+
+/**
+ * usb4switch_get_hotplug_timeout:
+ * @sw: switch handle
+ *
+ * Returns: Hotplug timeout in seconds.
+ */
+int usb4switch_get_hotplug_timeout(struct usb4switch *sw)
+{
+	return sw ? sw->hotplug_timeout_s :
+		    USB4_SWITCH_DEFAULT_HOTPLUG_TIMEOUT_S;
+}
+
+/**
+ * usb4switch_get_iterations:
+ * @sw: switch handle
+ *
+ * Returns: Configured iterations.
+ */
+int usb4switch_get_iterations(struct usb4switch *sw)
+{
+	return sw ? sw->iterations : USB4_SWITCH_DEFAULT_ITERATIONS;
+}
diff --git a/lib/igt_usb4_switch.h b/lib/igt_usb4_switch.h
new file mode 100644
index 000000000..5f1a0d334
--- /dev/null
+++ b/lib/igt_usb4_switch.h
@@ -0,0 +1,157 @@
+/* SPDX-License-Identifier: MIT */
+/*
+ * Copyright © 2026 Intel Corporation
+ */
+
+#ifndef IGT_USB4_SWITCH_H
+#define IGT_USB4_SWITCH_H
+
+#include <stdbool.h>
+#include <stddef.h>
+#include <stdint.h>
+
+/**
+ * USB4_SWITCH_MODEL_3141:
+ *
+ * Model number for Switch 3141.
+ */
+#define USB4_SWITCH_MODEL_3141 3141
+
+/**
+ * USB4_SWITCH_MAX_PORTS:
+ *
+ * Maximum number of ports on the switch.
+ */
+#define USB4_SWITCH_MAX_PORTS 2
+
+/**
+ * USB4_SWITCH_MAX_DISPLAYS_PER_PORT:
+ *
+ * Maximum displays per port (for MST hubs).
+ */
+#define USB4_SWITCH_MAX_DISPLAYS_PER_PORT 4
+
+/**
+ * USB4_SWITCH_RESPONSE_MAX_LEN:
+ *
+ * Recommended buffer size for usb4switch_get_status() callers.
+ * Internal commands use smaller buffers since responses are short.
+ */
+#define USB4_SWITCH_RESPONSE_MAX_LEN 1024
+
+/**
+ * USB4_SWITCH_DEFAULT_TIMEOUT_MS:
+ *
+ * Default command timeout in milliseconds.
+ */
+#define USB4_SWITCH_DEFAULT_TIMEOUT_MS 2000
+
+/**
+ * USB4_SWITCH_DEFAULT_HOTPLUG_TIMEOUT_S:
+ *
+ * Default hotplug wait timeout in seconds.
+ */
+#define USB4_SWITCH_DEFAULT_HOTPLUG_TIMEOUT_S 10
+
+/**
+ * USB4_SWITCH_DEFAULT_ITERATIONS:
+ *
+ * Default dock/undock iterations.
+ */
+#define USB4_SWITCH_DEFAULT_ITERATIONS 3
+
+/**
+ * USB4_SWITCH_VBUS_SAFE_MV:
+ *
+ * VBUS voltage threshold in mV below which VBUS is considered safe
+ * (Vsafe0V per USB PD specification).
+ */
+#define USB4_SWITCH_VBUS_SAFE_MV 800
+
+/**
+ * USB4_SWITCH_VBUS_SAFE_TIMEOUT_MS:
+ *
+ * Maximum time in milliseconds to wait for VBUS to discharge to safe levels.
+ */
+#define USB4_SWITCH_VBUS_SAFE_TIMEOUT_MS 3000
+
+/**
+ * USB4_SWITCH_VBUS_SAFE_POLL_MS:
+ *
+ * Polling interval in milliseconds when waiting for VBUS discharge.
+ */
+#define USB4_SWITCH_VBUS_SAFE_POLL_MS 100
+
+/**
+ * USB4_SWITCH_DEFAULT_MIN_INTERVAL_MS:
+ *
+ * Default minimum interval in milliseconds between successive port
+ * operations.  Matches Cricket UI's 1-second safety guard.
+ */
+#define USB4_SWITCH_DEFAULT_MIN_INTERVAL_MS 1000
+
+/**
+ * struct usb4switch_display:
+ * @connector_path: MST PATH property (e.g., "mst:5-2-8"), preferred for MST
+ * @connector_name: Fallback connector name (e.g., "DP-6") for non-MST
+ * @edid_serial: Expected EDID serial string for verification
+ *
+ * Configuration for an expected display on a switch port.
+ * For MST displays, use connector_path which is stable across hotplug.
+ * For non-MST displays, connector_name can be used.
+ */
+struct usb4switch_display {
+	char *connector_path;
+	char *connector_name;
+	char *edid_serial;
+};
+
+/**
+ * struct usb4switch_port:
+ * @port_num: Port number (1 or 2)
+ * @name: Human-readable port name
+ * @displays: Array of expected displays on this port
+ * @display_count: Number of displays configured
+ *
+ * Configuration for a switch port.
+ */
+struct usb4switch_port {
+	int port_num;
+	char *name;
+	struct usb4switch_display displays[USB4_SWITCH_MAX_DISPLAYS_PER_PORT];
+	int display_count;
+};
+
+struct usb4switch;
+
+/* Lifecycle */
+struct usb4switch *usb4switch_init(int drm_fd);
+void usb4switch_deinit(struct usb4switch *sw);
+
+/* Port control */
+bool usb4switch_port_enable(struct usb4switch *sw, int port);
+bool usb4switch_port_enable_delayed(struct usb4switch *sw, int port,
+				    int delay_seconds);
+bool usb4switch_port_disable(struct usb4switch *sw);
+bool usb4switch_port_disable_delayed(struct usb4switch *sw,
+				     int delay_seconds);
+
+/* VBUS safety */
+bool usb4switch_wait_vbus_safe(struct usb4switch *sw, int timeout_ms);
+bool usb4switch_port_disable_and_wait(struct usb4switch *sw);
+bool usb4switch_port_switch(struct usb4switch *sw, int new_port);
+
+/* Status queries */
+int usb4switch_get_active_port(struct usb4switch *sw);
+bool usb4switch_get_status(struct usb4switch *sw, char *buf, size_t size);
+int usb4switch_get_voltage_mv(struct usb4switch *sw);
+int usb4switch_get_current_ma(struct usb4switch *sw);
+
+/* Configuration getters */
+const struct usb4switch_port *usb4switch_get_port_config(struct usb4switch *sw,
+							 int port_index);
+int usb4switch_get_port_count(struct usb4switch *sw);
+int usb4switch_get_hotplug_timeout(struct usb4switch *sw);
+int usb4switch_get_iterations(struct usb4switch *sw);
+
+#endif /* IGT_USB4_SWITCH_H */
diff --git a/lib/meson.build b/lib/meson.build
index 26e7015d2..f18354f83 100644
--- a/lib/meson.build
+++ b/lib/meson.build
@@ -123,6 +123,7 @@ lib_sources = [
 	'igt_dsc.c',
 	'igt_hook.c',
 	'igt_serial.c',
+	'igt_usb4_switch.c',
 	'xe/xe_gt.c',
 	'xe/xe_ioctl.c',
 	'xe/xe_legacy.c',
-- 
2.25.1


  parent reply	other threads:[~2026-04-09  4:16 UTC|newest]

Thread overview: 13+ messages / expand[flat|nested]  mbox.gz  Atom feed  top
2026-04-09  4:37 [PATCH i-g-t 0/6] add test to validate dock/undock and switch Kunal Joshi
2026-04-09  4:37 ` [PATCH i-g-t 1/6] lib/igt_edid: add EDID serial extraction helpers Kunal Joshi
2026-04-09  4:37 ` [PATCH i-g-t 2/6] lib/igt_connector_helper: Add generic connector helpers Kunal Joshi
2026-04-09  4:37 ` [PATCH i-g-t 3/6] lib/igt_serial: add generic serial communication helper Kunal Joshi
2026-04-09  4:37 ` Kunal Joshi [this message]
2026-04-09  4:37 ` [PATCH i-g-t 5/6] tests/kms_feature_discovery: add basic usb4 switch discovery Kunal Joshi
2026-04-09  4:37 ` [PATCH i-g-t 6/6] tests/intel/kms_usb4_switch: Add USB4 switch test suite Kunal Joshi
2026-04-10  0:18 ` ✓ i915.CI.BAT: success for add test to validate dock/undock and switch (rev4) Patchwork
2026-04-10  0:19 ` ✓ Xe.CI.BAT: " Patchwork
2026-04-10  2:45 ` ✗ Xe.CI.FULL: failure " Patchwork
2026-04-10 17:16 ` ✗ i915.CI.Full: " Patchwork
  -- strict thread matches above, loose matches on Subject: below --
2026-02-25 21:28 [PATCH i-g-t 0/6] add test to validate dock/undock and switch Kunal Joshi
2026-02-25 21:28 ` [PATCH i-g-t 4/6] lib/igt_usb4_switch: add helper library for USB4 Switch 3141 Kunal Joshi
2026-02-25 19:42 [PATCH i-g-t 0/6] add test to validate dock/undock and switch Kunal Joshi
2026-02-25 19:42 ` [PATCH i-g-t 4/6] lib/igt_usb4_switch: add helper library for USB4 Switch 3141 Kunal Joshi

Reply instructions:

You may reply publicly to this message via plain-text email
using any one of the following methods:

* Save the following mbox file, import it into your mail client,
  and reply-to-all from there: mbox

  Avoid top-posting and favor interleaved quoting:
  https://en.wikipedia.org/wiki/Posting_style#Interleaved_style

* Reply using the --to, --cc, and --in-reply-to
  switches of git-send-email(1):

  git send-email \
    --in-reply-to=20260409043714.284108-5-kunal1.joshi@intel.com \
    --to=kunal1.joshi@intel.com \
    --cc=arun.r.murthy@intel.com \
    --cc=igt-dev@lists.freedesktop.org \
    /path/to/YOUR_REPLY

  https://kernel.org/pub/software/scm/git/docs/git-send-email.html

* If your mail client supports setting the In-Reply-To header
  via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line before the message body.
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox