All of lore.kernel.org
 help / color / mirror / Atom feed
From: Leonid Bloch <lb.workbox@gmail.com>
To: "Michael S . Tsirkin" <mst@redhat.com>,
	Igor Mammedov <imammedo@redhat.com>,
	Ani Sinha <anisinha@redhat.com>,
	Paolo Bonzini <pbonzini@redhat.com>,
	Richard Henderson <richard.henderson@linaro.org>,
	Eduardo Habkost <ehabkost@redhat.com>,
	Eric Blake <eblake@redhat.com>,
	Markus Armbruster <armbru@redhat.com>,
	Marcel Apfelbaum <marcel.apfelbaum@gmail.com>,
	Dmitry Fleytman <dmitry.fleytman@gmail.com>
Cc: Leonid Bloch <lb.workbox@gmail.com>, qemu-devel@nongnu.org
Subject: [PATCH v2 4/4] hw/acpi: Introduce the QEMU lid button
Date: Thu, 21 Aug 2025 20:45:52 +0300	[thread overview]
Message-ID: <20250821174554.40607-5-lb.workbox@gmail.com> (raw)
In-Reply-To: <20250821174554.40607-1-lb.workbox@gmail.com>

The lid button device communicates laptop lid state to the guest via ACPI.
It supports two modes of operation:

1. QMP control mode (default): Lid state is controlled programmatically
   via QMP commands for consistent behavior across environments.

2. Host mirroring mode (optional): The device reflects the host's lid
   button state from procfs (/proc/acpi/button/lid/*/state). State
   changes trigger ACPI notifications to the guest.

Properties:
- 'use-qmp': Enable QMP control mode (default: true)
- 'enable-procfs': Enable host lid button mirroring (default: false)
- 'probe_interval': Probe interval in ms for procfs mode (default: 2000)
- 'procfs_path': Override default procfs path /proc/acpi/button

The device implements the ACPI_DEV_AML_IF interface to generate its
own AML code, placing the LID0 device directly under \_SB scope.

QMP commands:
- lid-button-set-state: Set lid open/closed state
- query-lid-button: Query current lid state

Signed-off-by: Leonid Bloch <lb.workbox@gmail.com>
---
 MAINTAINERS                          |   6 +
 docs/specs/button.rst                | 189 ++++++++++++
 docs/specs/index.rst                 |   1 +
 hw/acpi/Kconfig                      |   4 +
 hw/acpi/button.c                     | 438 +++++++++++++++++++++++++++
 hw/acpi/meson.build                  |   1 +
 hw/acpi/trace-events                 |   5 +
 hw/i386/Kconfig                      |   1 +
 include/hw/acpi/acpi_dev_interface.h |   1 +
 include/hw/acpi/button.h             |  25 ++
 qapi/acpi.json                       |  49 +++
 11 files changed, 720 insertions(+)
 create mode 100644 docs/specs/button.rst
 create mode 100644 hw/acpi/button.c
 create mode 100644 include/hw/acpi/button.h

diff --git a/MAINTAINERS b/MAINTAINERS
index 612efcb686..4d54a2ffb5 100644
--- a/MAINTAINERS
+++ b/MAINTAINERS
@@ -2933,6 +2933,12 @@ S: Maintained
 F: hw/acpi/acad.*
 F: docs/specs/acad.rst
 
+Button
+M: Leonid Bloch <lb.workbox@gmail.com>
+S: Maintained
+F: hw/acpi/button.*
+F: docs/specs/button.rst
+
 Subsystems
 ----------
 Overall Audio backends
diff --git a/docs/specs/button.rst b/docs/specs/button.rst
new file mode 100644
index 0000000000..10a940b9f5
--- /dev/null
+++ b/docs/specs/button.rst
@@ -0,0 +1,189 @@
+.. SPDX-License-Identifier: GPL-2.0-or-later
+
+========================
+Laptop Lid Button Device
+========================
+
+The button device provides laptop lid button state information to the guest.
+It supports two operating modes:
+
+1. **QMP Control Mode** (default): Lid state is controlled via QMP commands,
+   providing deterministic control for testing and migration safety.
+2. **Procfs Mode**: Lid state mirrors the host's physical lid button, useful
+   for desktop virtualization where the guest should see the host's lid state.
+
+Configuration
+-------------
+
+The lid button device is created as an ISA device using ``-device button``.
+
+Operating Modes
+~~~~~~~~~~~~~~~
+
+**QMP Control Mode** (``use-qmp=true``, default)
+  Lid state is controlled via QMP commands. This mode is recommended for:
+
+  * Production environments requiring migration support
+  * Testing with predictable lid states
+  * Environments without host lid button access
+  * Security-sensitive deployments
+
+**Procfs Mode** (``enable-procfs=true``)
+  Lid mirrors the host's physical lid button. This mode is useful for:
+
+  * Desktop virtualization on laptops
+  * Development and testing with real lid button behavior
+
+  Note: Procfs mode reads host files and runs timers, which may impact
+  security and migration. Use with caution in production.
+
+Properties
+~~~~~~~~~~
+
+``ioport`` (default: 0x53d)
+  I/O port base address for the lid button device register.
+
+``use-qmp`` (default: true)
+  Enable QMP control mode. When true, lid state is controlled via
+  QMP commands. Cannot be used together with ``enable-procfs=true``.
+
+``enable-procfs`` (default: false)
+  Enable procfs mode to mirror the host's lid button. Cannot be used together
+  with ``use-qmp=true``.
+
+``probe_interval`` (default: 2000)
+  Time interval between periodic probes in milliseconds (procfs mode only).
+  The minimum allowed value is 10ms to prevent excessive polling.
+
+``procfs_path`` (default: /proc/acpi/button)
+  Path to the host's lid button procfs directory (procfs mode only). The device
+  will automatically scan this directory to find the lid state file. Use this
+  property to specify a different path or to provide a custom location for
+  testing purposes.
+
+Host Lid Button Detection
+-------------------------
+
+The host's lid button information is taken from::
+
+    /proc/acpi/button/lid/*/state
+
+This file is expected to be formatted as:
+
+- ``state:      open`` (if the lid is open)
+- ``state:      closed`` (if the lid is closed)
+
+These formats are based on the Linux 'button' driver.
+
+The device automatically scans the ``/proc/acpi/button/lid/`` directory
+for subdirectories containing a readable ``state`` file. If the procfs path
+differs, a different lid button needs to be probed, or even if a "fake" host
+lid button is to be provided, the ``procfs_path`` property allows overriding
+the default detection.
+
+ACPI Interface
+--------------
+
+The lid button device is exposed to the guest as an ACPI device with:
+
+- **HID**: ``PNP0C0D`` (Lid Device)
+- **Device Path**: ``\_SB.LID0``
+- **Notification Values**:
+
+  - ``0x80``: Status change (lid opened/closed)
+
+ACPI Methods
+~~~~~~~~~~~~
+
+``_LID`` (Lid Status)
+  Returns the current lid state (0 = closed, 1 = open).
+
+I/O Interface
+-------------
+
+The device uses a single I/O port register:
+
+- **Port**: ``ioport`` property value (default 0x53d)
+- **Size**: 1 byte
+- **Access**: Read-only
+
+Register Layout
+~~~~~~~~~~~~~~~
+
+**LIDS** (offset 0x00, 1 byte)
+  Current lid state:
+
+  - ``0x00``: Lid closed
+  - ``0x01``: Lid open
+
+QMP Commands
+------------
+
+When using QMP control mode (default), the following commands are available:
+
+``lid-button-set-state``
+  Set the lid button state.
+
+  * ``open``: Whether the lid is open (boolean)
+
+  Example::
+
+    -> { "execute": "lid-button-set-state",
+         "arguments": { "open": true }}
+    <- { "return": {} }
+
+``query-lid-button``
+  Query the current lid button state.
+
+  Example::
+
+    -> { "execute": "query-lid-button" }
+    <- { "return": { "open": true }}
+
+Examples
+--------
+
+QMP control mode (default - recommended)::
+
+  # Start with QMP control
+  qemu-system-x86_64 -device button -qmp tcp:localhost:4444,server,wait=off
+
+  # From another terminal, set lid state via QMP:
+  echo '{"execute":"qmp_capabilities"}
+        {"execute":"lid-button-set-state",
+         "arguments":{"open":false}}' | \
+  nc -N localhost 4444
+
+Procfs mode (mirror host lid button)::
+
+  # Enable procfs mode to mirror host lid button
+  qemu-system-x86_64 -device button,use-qmp=false,enable-procfs=true
+
+  # Custom probe interval (5 seconds)
+  qemu-system-x86_64 -device button,use-qmp=false,enable-procfs=true,probe_interval=5000
+
+  # Custom procfs path
+  qemu-system-x86_64 -device button,use-qmp=false,enable-procfs=true,procfs_path=/custom/path
+
+Testing with fake lid button::
+
+  # Create fake lid button files for testing
+  mkdir -p /tmp/fake_lid/lid/LID0
+  echo "state:      open" > /tmp/fake_lid/lid/LID0/state    # Format: "state:      open" or "state:      closed"
+
+  # Use fake lid button in procfs mode
+  qemu-system-x86_64 -device button,use-qmp=false,enable-procfs=true,procfs_path=/tmp/fake_lid
+
+  # Update lid state while VM is running (from another terminal)
+  echo "state:      closed" > /tmp/fake_lid/lid/LID0/state  # Close lid
+  echo "state:      open" > /tmp/fake_lid/lid/LID0/state    # Open lid
+
+Combined with other laptop devices::
+
+  # QMP mode (recommended)
+  qemu-system-x86_64 -device battery -device acad -device button
+
+  # Procfs/sysfs mode (desktop virtualization)
+  qemu-system-x86_64 -device battery,use-qmp=false,enable-sysfs=true \
+                     -device acad,use-qmp=false,enable-sysfs=true \
+                     -device button,use-qmp=false,enable-procfs=true
diff --git a/docs/specs/index.rst b/docs/specs/index.rst
index e144afcd90..e1c9b91b7b 100644
--- a/docs/specs/index.rst
+++ b/docs/specs/index.rst
@@ -24,6 +24,7 @@ guest hardware that is specific to QEMU.
    acpi_erst
    acad
    battery
+   button
    sev-guest-firmware
    fw_cfg
    fsi
diff --git a/hw/acpi/Kconfig b/hw/acpi/Kconfig
index 9d28c3addf..6600685855 100644
--- a/hw/acpi/Kconfig
+++ b/hw/acpi/Kconfig
@@ -73,6 +73,10 @@ config AC_ADAPTER
     bool
     depends on ACPI
 
+config BUTTON
+    bool
+    depends on ACPI
+
 config ACPI_HW_REDUCED
     bool
     select ACPI
diff --git a/hw/acpi/button.c b/hw/acpi/button.c
new file mode 100644
index 0000000000..dfe86af713
--- /dev/null
+++ b/hw/acpi/button.c
@@ -0,0 +1,438 @@
+/*
+ * QEMU emulated lid button device
+ *
+ * Copyright (c) 2019 Janus Technologies, Inc. (http://janustech.com)
+ *
+ * Authors:
+ *     Leonid Bloch <lb.workbox@gmail.com>
+ *     Marcel Apfelbaum <marcel.apfelbaum@gmail.com>
+ *     Dmitry Fleytman <dmitry.fleytman@gmail.com>
+ *
+ * SPDX-License-Identifier: GPL-2.0-or-later
+ *
+ */
+
+#include "qemu/osdep.h"
+#include "trace.h"
+#include "hw/isa/isa.h"
+#include "hw/acpi/acpi.h"
+#include "hw/nvram/fw_cfg.h"
+#include "qapi/error.h"
+#include "qemu/error-report.h"
+#include "hw/qdev-properties.h"
+#include "migration/vmstate.h"
+#include "hw/acpi/acpi_aml_interface.h"
+#include "qapi/qapi-commands-acpi.h"
+
+#include "hw/acpi/button.h"
+
+#define BUTTON_DEVICE(obj) OBJECT_CHECK(BUTTONState, (obj), \
+                                        TYPE_BUTTON)
+
+#define BUTTON_STA_ADDR            0
+
+#define PROCFS_PATH                "/proc/acpi/button"
+#define LID_DIR                    "lid"
+#define LID_STATE_FILE             "state"
+#define MIN_BUTTON_PROBE_INTERVAL  10  /* ms */
+#define MAX_ALLOWED_LINE_LENGTH    32  /* For convenience when comparing */
+
+enum {
+    LID_CLOSED = 0,
+    LID_OPEN = 1,
+};
+
+static const char *lid_state[] = { "closed", "open" };
+
+typedef struct BUTTONState {
+    ISADevice dev;
+    MemoryRegion io;
+    uint16_t ioport;
+    uint8_t lid_state;
+    bool use_qmp_control;
+    bool qmp_lid_open;
+    bool enable_procfs;
+
+    QEMUTimer *probe_state_timer;
+    uint64_t probe_state_interval;
+
+    char *button_path;
+    char lid_dir[MAX_ALLOWED_LINE_LENGTH];
+} BUTTONState;
+
+static inline bool button_file_accessible(char *path, const char *dir,
+                                          char *subdir, const char *file)
+{
+    char full_path[PATH_MAX];
+    int path_len;
+
+    path_len = snprintf(full_path, PATH_MAX, "%s/%s/%s/%s", path, dir, subdir,
+                        file);
+    if (path_len < 0 || path_len >= PATH_MAX) {
+        return false;
+    }
+
+    if (access(full_path, R_OK) == 0) {
+        return true;
+    }
+    return false;
+}
+
+static void button_get_lid_state(BUTTONState *s)
+{
+    char file_path[PATH_MAX];
+    int path_len;
+    char line[MAX_ALLOWED_LINE_LENGTH];
+    FILE *ff;
+
+    path_len = snprintf(file_path, PATH_MAX, "%s/%s/%s/%s", s->button_path,
+                        LID_DIR, s->lid_dir, LID_STATE_FILE);
+    if (path_len < 0 || path_len >= PATH_MAX) {
+        warn_report("Could not read the lid state.");
+        return;
+    }
+
+    ff = fopen(file_path, "r");
+    if (ff == NULL) {
+        warn_report("Could not read the lid state.");
+        return;
+    }
+
+    if (fgets(line, MAX_ALLOWED_LINE_LENGTH, ff) == NULL) {
+        warn_report("Lid state unreadable.");
+    } else {
+        if (strstr(line, lid_state[LID_OPEN]) != NULL) {
+            s->lid_state = LID_OPEN;
+        } else if (strstr(line, lid_state[LID_CLOSED]) != NULL) {
+            s->lid_state = LID_CLOSED;
+        } else {
+            warn_report("Lid state undetermined.");
+        }
+    }
+
+    fclose(ff);
+}
+
+static void button_get_dynamic_status(BUTTONState *s)
+{
+    trace_button_get_dynamic_status();
+
+    if (s->use_qmp_control) {
+        s->lid_state = s->qmp_lid_open ? LID_OPEN : LID_CLOSED;
+    } else if (s->enable_procfs) {
+        button_get_lid_state(s);
+    } else {
+        s->lid_state = LID_CLOSED;
+    }
+}
+
+static void button_probe_state(void *opaque)
+{
+    BUTTONState *s = opaque;
+
+    uint8_t lid_state_before = s->lid_state;
+
+    button_get_dynamic_status(s);
+
+    if (lid_state_before != s->lid_state) {
+        Object *obj = object_resolve_path_type("", TYPE_ACPI_DEVICE_IF, NULL);
+        acpi_send_event(DEVICE(obj), ACPI_BUTTON_CHANGE_STATUS);
+    }
+    timer_mod(s->probe_state_timer, qemu_clock_get_ms(QEMU_CLOCK_VIRTUAL) +
+              s->probe_state_interval);
+}
+
+static void button_probe_state_timer_init(BUTTONState *s)
+{
+    if (s->enable_procfs && s->probe_state_interval > 0) {
+        s->probe_state_timer = timer_new_ms(QEMU_CLOCK_VIRTUAL,
+                                            button_probe_state, s);
+        timer_mod(s->probe_state_timer, qemu_clock_get_ms(QEMU_CLOCK_VIRTUAL) +
+                  s->probe_state_interval);
+    }
+}
+
+static inline bool button_verify_lid_procfs(char *path, char *lid_subdir)
+{
+    return button_file_accessible(path, LID_DIR, lid_subdir, LID_STATE_FILE);
+}
+
+static bool button_get_lid_dir(BUTTONState *s, char *path)
+{
+    DIR *dir;
+    char lid_path[PATH_MAX];
+    int path_len;
+    struct dirent *ent;
+
+    path_len = snprintf(lid_path, PATH_MAX, "%s/%s", path, LID_DIR);
+    if (path_len < 0 || path_len >= PATH_MAX) {
+        return false;
+    }
+
+    dir = opendir(lid_path);
+    if (dir == NULL) {
+        return false;
+    }
+
+    ent = readdir(dir);
+    while (ent != NULL) {
+        if (ent->d_name[0] != '.') {
+            if (button_verify_lid_procfs(path, ent->d_name)) {
+                path_len = snprintf(s->lid_dir, strlen(ent->d_name) + 1, "%s",
+                                    ent->d_name);
+                if (path_len < 0 || path_len > strlen(ent->d_name)) {
+                    return false;
+                }
+                closedir(dir);
+                return true;
+            }
+        }
+        ent = readdir(dir);
+    }
+    closedir(dir);
+    return false;
+}
+
+static bool get_button_path(DeviceState *dev)
+{
+    BUTTONState *s = BUTTON_DEVICE(dev);
+    char procfs_path[PATH_MAX];
+    int path_len;
+
+    if (s->button_path) {
+        path_len = snprintf(procfs_path, strlen(s->button_path) + 1, "%s",
+                            s->button_path);
+        if (path_len < 0 || path_len > strlen(s->button_path)) {
+            return false;
+        }
+    } else {
+        path_len = snprintf(procfs_path, sizeof(PROCFS_PATH), "%s",
+                            PROCFS_PATH);
+        if (path_len < 0 || path_len >= sizeof(PROCFS_PATH)) {
+            return false;
+        }
+    }
+
+    if (button_get_lid_dir(s, procfs_path)) {
+        qdev_prop_set_string(dev, BUTTON_PATH_PROP, procfs_path);
+        return true;
+    }
+
+    return false;
+}
+
+static void button_realize(DeviceState *dev, Error **errp)
+{
+    ISADevice *d = ISA_DEVICE(dev);
+    BUTTONState *s = BUTTON_DEVICE(dev);
+    FWCfgState *fw_cfg = fw_cfg_find();
+    uint16_t *button_port;
+    char err_details[32] = {};
+
+    trace_button_realize();
+
+    if (s->use_qmp_control && s->enable_procfs) {
+        error_setg(errp, "Cannot enable both QMP control and procfs mode");
+        return;
+    }
+
+    /* Initialize lid to open by default when in QMP mode */
+    if (s->use_qmp_control) {
+        s->qmp_lid_open = true;
+    }
+
+    if (s->probe_state_interval < MIN_BUTTON_PROBE_INTERVAL) {
+        error_setg(errp, "'probe_state_interval' must be greater than %d ms",
+                   MIN_BUTTON_PROBE_INTERVAL);
+        return;
+    }
+
+    if (s->enable_procfs) {
+        if (!s->button_path) {
+            strcpy(err_details, " Try using 'procfs_path='");
+        }
+
+        if (!get_button_path(dev)) {
+            error_setg(errp, "Button procfs path not found or unreadable.%s",
+                       err_details);
+            return;
+        }
+    }
+
+    isa_register_ioport(d, &s->io, s->ioport);
+
+    button_probe_state_timer_init(s);
+
+    if (!fw_cfg) {
+        return;
+    }
+
+    button_port = g_malloc(sizeof(*button_port));
+    *button_port = cpu_to_le16(s->ioport);
+    fw_cfg_add_file(fw_cfg, "etc/button-port", button_port,
+                    sizeof(*button_port));
+}
+
+static const Property button_device_properties[] = {
+    DEFINE_PROP_UINT16(BUTTON_IOPORT_PROP, BUTTONState, ioport, 0x53d),
+    DEFINE_PROP_BOOL("use-qmp", BUTTONState, use_qmp_control, true),
+    DEFINE_PROP_BOOL("enable-procfs", BUTTONState, enable_procfs, false),
+    DEFINE_PROP_UINT64(BUTTON_PROBE_STATE_INTERVAL, BUTTONState,
+                       probe_state_interval, 2000),
+    DEFINE_PROP_STRING(BUTTON_PATH_PROP, BUTTONState, button_path),
+};
+
+static const VMStateDescription button_vmstate = {
+    .name = "button",
+    .version_id = 1,
+    .minimum_version_id = 1,
+    .fields = (VMStateField[]) {
+        VMSTATE_UINT16(ioport, BUTTONState),
+        VMSTATE_UINT64(probe_state_interval, BUTTONState),
+        VMSTATE_END_OF_LIST()
+    }
+};
+
+static void build_button_aml(AcpiDevAmlIf *adev, Aml *scope)
+{
+    Aml *dev, *field, *method;
+    Aml *button_state;
+    Aml *sb_scope;
+    BUTTONState *s = BUTTON_DEVICE(adev);
+
+    button_state = aml_local(0);
+
+    sb_scope = aml_scope("\\_SB");
+    dev = aml_device("LID0");
+    aml_append(dev, aml_name_decl("_HID", aml_string("PNP0C0D")));
+
+    aml_append(dev, aml_operation_region("LSTA", AML_SYSTEM_IO,
+                                         aml_int(s->ioport),
+                                         BUTTON_LEN));
+    field = aml_field("LSTA", AML_BYTE_ACC, AML_NOLOCK, AML_PRESERVE);
+    aml_append(field, aml_named_field("LIDS", 8));
+    aml_append(dev, field);
+
+    method = aml_method("_LID", 0, AML_NOTSERIALIZED);
+    aml_append(method, aml_store(aml_name("LIDS"), button_state));
+    aml_append(method, aml_return(button_state));
+    aml_append(dev, method);
+
+    aml_append(sb_scope, dev);
+    aml_append(scope, sb_scope);
+
+    /* Status Change */
+    method = aml_method("\\_GPE._E0B", 0, AML_NOTSERIALIZED);
+    aml_append(method, aml_notify(aml_name("\\_SB.LID0"), aml_int(0x80)));
+    aml_append(scope, method);
+}
+
+static void button_class_init(ObjectClass *class, const void *data)
+{
+    DeviceClass *dc = DEVICE_CLASS(class);
+    AcpiDevAmlIfClass *adevc = ACPI_DEV_AML_IF_CLASS(class);
+
+    dc->realize = button_realize;
+    device_class_set_props(dc, button_device_properties);
+    dc->vmsd = &button_vmstate;
+    adevc->build_dev_aml = build_button_aml;
+}
+
+static uint64_t button_ioport_read(void *opaque, hwaddr addr, unsigned size)
+{
+    BUTTONState *s = opaque;
+
+    button_get_dynamic_status(s);
+
+    switch (addr) {
+    case BUTTON_STA_ADDR:
+        return s->lid_state;
+    default:
+        warn_report("Button: guest read unknown value.");
+        trace_button_ioport_read_unknown();
+        return 0;
+    }
+}
+
+static const MemoryRegionOps button_ops = {
+    .read = button_ioport_read,
+    .impl = {
+        .min_access_size = 1,
+        .max_access_size = 1,
+    },
+};
+
+static void button_instance_init(Object *obj)
+{
+    BUTTONState *s = BUTTON_DEVICE(obj);
+
+    memory_region_init_io(&s->io, obj, &button_ops, s, "button",
+                          BUTTON_LEN);
+}
+
+static const TypeInfo button_info = {
+    .name          = TYPE_BUTTON,
+    .parent        = TYPE_ISA_DEVICE,
+    .instance_size = sizeof(BUTTONState),
+    .class_init    = button_class_init,
+    .instance_init = button_instance_init,
+    .interfaces = (InterfaceInfo[]) {
+        { TYPE_ACPI_DEV_AML_IF },
+        { },
+    },
+};
+
+static BUTTONState *find_button_device(void)
+{
+    Object *o = object_resolve_path_type("", TYPE_BUTTON, NULL);
+    if (!o) {
+        return NULL;
+    }
+    return BUTTON_DEVICE(o);
+}
+
+void qmp_lid_button_set_state(bool open, Error **errp)
+{
+    BUTTONState *s = find_button_device();
+
+    if (!s) {
+        error_setg(errp, "No lid button device found");
+        return;
+    }
+
+    s->qmp_lid_open = open;
+
+    Object *obj = object_resolve_path_type("", TYPE_ACPI_DEVICE_IF, NULL);
+    if (obj) {
+        acpi_send_event(DEVICE(obj), ACPI_BUTTON_CHANGE_STATUS);
+    }
+}
+
+LidButtonInfo *qmp_query_lid_button(Error **errp)
+{
+    BUTTONState *s = find_button_device();
+    LidButtonInfo *ret;
+
+    if (!s) {
+        error_setg(errp, "No lid button device found");
+        return NULL;
+    }
+
+    ret = g_new0(LidButtonInfo, 1);
+
+    if (s->use_qmp_control) {
+        ret->open = s->qmp_lid_open;
+    } else {
+        button_get_dynamic_status(s);
+        ret->open = (s->lid_state == LID_OPEN);
+    }
+
+    return ret;
+}
+
+static void button_register_types(void)
+{
+    type_register_static(&button_info);
+}
+
+type_init(button_register_types)
diff --git a/hw/acpi/meson.build b/hw/acpi/meson.build
index 2b24951f28..94993ecb9a 100644
--- a/hw/acpi/meson.build
+++ b/hw/acpi/meson.build
@@ -33,6 +33,7 @@ if have_tpm
 endif
 acpi_ss.add(when: 'CONFIG_BATTERY', if_true: files('battery.c'))
 acpi_ss.add(when: 'CONFIG_AC_ADAPTER', if_true: files('acad.c'))
+acpi_ss.add(when: 'CONFIG_BUTTON', if_true: files('button.c'))
 system_ss.add(when: 'CONFIG_ACPI', if_false: files('acpi-stub.c', 'aml-build-stub.c', 'ghes-stub.c', 'acpi_interface.c'))
 system_ss.add(when: 'CONFIG_ACPI_PCI_BRIDGE', if_false: files('pci-bridge-stub.c'))
 system_ss.add_all(when: 'CONFIG_ACPI', if_true: acpi_ss)
diff --git a/hw/acpi/trace-events b/hw/acpi/trace-events
index 68ac6e9701..816f027bb3 100644
--- a/hw/acpi/trace-events
+++ b/hw/acpi/trace-events
@@ -97,3 +97,8 @@ battery_ioport_read_unknown(void) "Battery read unknown"
 acad_realize(void) "AC adapter device realize entry"
 acad_get_dynamic_status(uint8_t state) "AC adapter read state: %"PRIu8
 acad_ioport_read_unknown(void) "AC adapter read unknown"
+
+# button.c
+button_realize(void) "Button device realize entry"
+button_get_dynamic_status(void) "Button read dynamic status entry"
+button_ioport_read_unknown(void) "Button read unknown"
diff --git a/hw/i386/Kconfig b/hw/i386/Kconfig
index baab382a2e..1d2d809028 100644
--- a/hw/i386/Kconfig
+++ b/hw/i386/Kconfig
@@ -41,6 +41,7 @@ config PC
     imply FDC_ISA
     imply BATTERY
     imply AC_ADAPTER
+    imply BUTTON
     select I8259
     select I8254
     select PCKBD
diff --git a/include/hw/acpi/acpi_dev_interface.h b/include/hw/acpi/acpi_dev_interface.h
index 588fbbd05f..4f5bcc15da 100644
--- a/include/hw/acpi/acpi_dev_interface.h
+++ b/include/hw/acpi/acpi_dev_interface.h
@@ -15,6 +15,7 @@ typedef enum {
     ACPI_POWER_DOWN_STATUS = 64,
     ACPI_BATTERY_CHANGE_STATUS = 128,
     ACPI_AC_ADAPTER_CHANGE_STATUS = 1024,
+    ACPI_BUTTON_CHANGE_STATUS = 2048,
 } AcpiEventStatusBits;
 
 #define TYPE_ACPI_DEVICE_IF "acpi-device-interface"
diff --git a/include/hw/acpi/button.h b/include/hw/acpi/button.h
new file mode 100644
index 0000000000..fa5f7001b2
--- /dev/null
+++ b/include/hw/acpi/button.h
@@ -0,0 +1,25 @@
+/*
+ * QEMU emulated button device.
+ *
+ * Copyright (c) 2019 Janus Technologies, Inc. (http://janustech.com)
+ *
+ * Authors:
+ *     Leonid Bloch <lb.workbox@gmail.com>
+ *     Marcel Apfelbaum <marcel.apfelbaum@gmail.com>
+ *     Dmitry Fleytman <dmitry.fleytman@gmail.com>
+ *
+ * SPDX-License-Identifier: GPL-2.0-or-later
+ *
+ */
+
+#ifndef HW_ACPI_BUTTON_H
+#define HW_ACPI_BUTTON_H
+
+#define TYPE_BUTTON                  "button"
+#define BUTTON_IOPORT_PROP           "ioport"
+#define BUTTON_PATH_PROP             "procfs_path"
+#define BUTTON_PROBE_STATE_INTERVAL  "probe_interval"
+
+#define BUTTON_LEN                   1
+
+#endif
diff --git a/qapi/acpi.json b/qapi/acpi.json
index 52e151f0e6..bcbddbfbd5 100644
--- a/qapi/acpi.json
+++ b/qapi/acpi.json
@@ -264,3 +264,52 @@
 ##
 { 'command': 'query-ac-adapter',
   'returns': 'AcAdapterInfo' }
+
+##
+# @lid-button-set-state:
+#
+# Set the state of the emulated laptop lid button device
+#
+# @open: whether the lid is open
+#
+
+#
+# Since: 10.2
+#
+# .. qmp-example::
+#
+#     -> { "execute": "lid-button-set-state",
+#          "arguments": { "open": true } }
+#     <- { "return": {} }
+##
+{ 'command': 'lid-button-set-state',
+  'data': { 'open': 'bool' } }
+
+##
+# @LidButtonInfo:
+#
+# Lid button state information
+#
+# @open: whether the lid is open
+#
+# Since: 10.2
+##
+{ 'struct': 'LidButtonInfo',
+  'data': { 'open': 'bool' } }
+
+##
+# @query-lid-button:
+#
+# Query the current state of the emulated laptop lid button device
+#
+# Returns: lid button state
+#
+# Since: 10.2
+#
+# .. qmp-example::
+#
+#     -> { "execute": "query-lid-button" }
+#     <- { "return": { "open": true } }
+##
+{ 'command': 'query-lid-button',
+  'returns': 'LidButtonInfo' }
-- 
2.50.1



  parent reply	other threads:[~2025-08-21 17:48 UTC|newest]

Thread overview: 7+ messages / expand[flat|nested]  mbox.gz  Atom feed  top
2025-08-21 17:45 [PATCH v2 0/4] Introduce a battery, AC adapter, and lid button Leonid Bloch
2025-08-21 17:45 ` [PATCH v2 1/4] hw/acpi: Increase the number of possible ACPI interrupts Leonid Bloch
2025-08-26 12:47   ` Igor Mammedov
2025-08-21 17:45 ` [PATCH v2 2/4] hw/acpi: Introduce the QEMU Battery Leonid Bloch
2025-08-21 17:45 ` [PATCH v2 3/4] hw/acpi: Introduce the QEMU AC adapter Leonid Bloch
2025-08-21 17:45 ` Leonid Bloch [this message]
2025-08-21 18:03 ` [PATCH v2 0/4] Introduce a battery, AC adapter, and lid button Leonid Bloch

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=20250821174554.40607-5-lb.workbox@gmail.com \
    --to=lb.workbox@gmail.com \
    --cc=anisinha@redhat.com \
    --cc=armbru@redhat.com \
    --cc=dmitry.fleytman@gmail.com \
    --cc=eblake@redhat.com \
    --cc=ehabkost@redhat.com \
    --cc=imammedo@redhat.com \
    --cc=marcel.apfelbaum@gmail.com \
    --cc=mst@redhat.com \
    --cc=pbonzini@redhat.com \
    --cc=qemu-devel@nongnu.org \
    --cc=richard.henderson@linaro.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 an external index of several public inboxes,
see mirroring instructions on how to clone and mirror
all data and code used by this external index.