From: "Murthy, Arun R" <arun.r.murthy@intel.com>
To: Kunal Joshi <kunal1.joshi@intel.com>, <igt-dev@lists.freedesktop.org>
Subject: Re: [i-g-t,4/6] lib/igt_usb4_switch: add helper library for USB4 Switch 3141
Date: Mon, 16 Mar 2026 14:15:41 +0530 [thread overview]
Message-ID: <d2ff02fe-b75d-4783-9f2e-c7adb4d571bf@intel.com> (raw)
In-Reply-To: <20260225212859.876713-5-kunal1.joshi@intel.com>
Looks Good.
Reviewed-by: Arun R Murthy <arun.r.murthy@intel.com>
Thanks and Regards,
Arun R Murthy
-------------------
On 26-02-2026 02:58, Kunal Joshi wrote:
> 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>
> ---
> 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", ¤t) == 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 67b57533c..d03bef209 100644
> --- a/lib/meson.build
> +++ b/lib/meson.build
> @@ -124,6 +124,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',
next prev parent reply other threads:[~2026-03-16 8:45 UTC|newest]
Thread overview: 23+ messages / expand[flat|nested] mbox.gz Atom feed top
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 1/6] lib/igt_edid: add EDID serial extraction helpers Kunal Joshi
2026-03-09 9:30 ` [i-g-t,1/6] " Murthy, Arun R
2026-03-09 11:02 ` Joshi, Kunal1
2026-03-16 3:03 ` Murthy, Arun R
2026-02-25 21:28 ` [PATCH i-g-t 2/6] lib/igt_connector_helper: Add generic connector helpers Kunal Joshi
2026-03-09 9:38 ` [i-g-t,2/6] " Murthy, Arun R
2026-03-09 11:06 ` Joshi, Kunal1
2026-03-16 3:17 ` Murthy, Arun R
2026-02-25 21:28 ` [PATCH i-g-t 3/6] lib/igt_serial: add generic serial communication helper Kunal Joshi
2026-03-16 5:40 ` [i-g-t,3/6] " Murthy, Arun R
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-03-16 8:45 ` Murthy, Arun R [this message]
2026-02-25 21:28 ` [PATCH i-g-t 5/6] tests/kms_feature_discovery: add basic usb4 switch discovery Kunal Joshi
2026-03-09 10:05 ` [i-g-t,5/6] " Murthy, Arun R
2026-02-25 21:28 ` [PATCH i-g-t 6/6] tests/intel/kms_usb4_switch: Add USB4 switch test suite Kunal Joshi
2026-03-09 10:03 ` [i-g-t,6/6] " Murthy, Arun R
2026-03-09 11:14 ` Joshi, Kunal1
2026-03-16 3:38 ` Murthy, Arun R
2026-02-26 2:20 ` ✗ Xe.CI.BAT: failure for add test to validate dock/undock and switch (rev3) Patchwork
2026-02-26 3:04 ` ✓ i915.CI.BAT: success " Patchwork
2026-02-26 6:52 ` ✗ Xe.CI.FULL: failure " Patchwork
2026-02-26 7:45 ` ✗ i915.CI.Full: " Patchwork
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=d2ff02fe-b75d-4783-9f2e-c7adb4d571bf@intel.com \
--to=arun.r.murthy@intel.com \
--cc=igt-dev@lists.freedesktop.org \
--cc=kunal1.joshi@intel.com \
/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