* [PATCH v4 0/8] Introduce a battery, AC adapter, and lid button
@ 2026-05-26 4:29 Leonid Bloch
2026-05-26 4:29 ` [PATCH v4 1/8] hw/acpi: Support extended GPE handling for additional ACPI devices Leonid Bloch
` (7 more replies)
0 siblings, 8 replies; 11+ messages in thread
From: Leonid Bloch @ 2026-05-26 4:29 UTC (permalink / raw)
To: Michael S . Tsirkin, Igor Mammedov, Ani Sinha, Paolo Bonzini,
Richard Henderson, Eduardo Habkost, Eric Blake, Markus Armbruster,
Marcel Apfelbaum, Dmitry Fleytman
Cc: Leonid Bloch, qemu-devel
This series introduces three ACPI devices that are particularly useful
for laptop/mobile virtualization:
* Battery
* AC adapter
* Laptop lid button
Link to v3: https://lists.nongnu.org/archive/html/qemu-devel/2025-08/msg04138.html
Link to GitHub PR, for ease of review: https://github.com/blochl/qemu/pull/4
Changes in v4:
--------------
* Rebased on latest master
Based on the feedback on v3:
* Documentation patch split: each device is now introduced by a
separate documentation patch followed by the code patch.
* sysfs/procfs host mirroring removed from the C devices: QEMU no
longer reads /sys or /proc directly. The devices are now purely
QMP-controlled. A reference Python script is added in
scripts/laptop-mirror.py that demonstrates how a management layer
can translate host sysfs/procfs state into the QMP commands.
* Battery I/O region cleanup: the battery registers are now native
DWORD-access only, matching the AML AML_DWORD_ACC field layout.
* find_*_device() now reports ambiguity: when more than one matching
device is present, QMP returns "More than one X device present".
* QAPI cleanups:
- Stray blank documentation lines in *-set-state removed.
- All the new types and commands carry "Since: 11.1".
- The C devices no longer #include "qemu/error-report.h" now that
they don't call warn_report().
- battery-set-state now rejects impossible combinations
(charging && discharging, charge/discharge without present,
out-of-range charge-percent and rate).
* I/O port unknown-address handling: the default branches in the
ioport_read paths now g_assert_not_reached() instead of logging and
returning 0.
* Set the devices not hotpluggable.
* ISA vs. PCI base: kept ISA. ACPI defines PNP0C0A (battery),
ACPI0003 (AC adapter) and PNP0C0D (lid) as namespace devices under
\_SB with control methods (_BIF/_BST, _PSR, _LID) - see ACPI 6.5
sections 10.2, 10.3 and 9.4
(https://uefi.org/sites/default/files/resources/ACPI_Spec_6_5_Aug29.pdf).
There is no PCI class code defined for any of these in the first place,
and no actual laptop puts them on PCI - the signals come out of the
embedded controller and surface as fixed-hardware ACPI devices.
Guest power-management code matches the same way: Linux uses
drivers/acpi/{battery,ac,button}, Windows acpi.sys, macOS and the
BSDs follow the same pattern. Making the devices PCI would mean
inventing a device class no guest has a driver for.
Changes in v3:
--------------
* Rebased on latest master
* Addressed the v2 review by Igor Mammedov
Changes in v2:
--------------
Based on the feedback from Philippe Mathieu-Daudé and Michael S. Tsirkin:
* Complete redesign with dual-mode operation:
- QMP control mode (default): Devices are controlled via QMP commands,
providing deterministic behavior essential for migration and CI/testing
- Host mirroring mode (opt-in): Original sysfs/procfs monitoring behavior,
now disabled by default
* Migrated to modern QEMU ACPI architecture:
- Devices now implement ACPI_DEV_AML_IF interface
- AML generation moved from centralized acpi-build.c to device files
* Added a QMP interface:
- battery-set-state/query-battery
- ac-adapter-set-state/query-ac-adapter
- lid-button-set-state/query-lid-button
* Documentation improvements:
- Converted to .rst format
- Added examples for both QMP and "fake" sysfs/procfs testing
The dual-mode design ensures these devices are migration-safe and
deterministic by default, while still allowing host state mirroring
when explicitly requested for desktop use cases.
Use cases:
----------
1. Testing: CI systems can programmatically control power states.
2. Cloud: Expose a virtual battery for usage-based resource limiting.
3. Desktop virtualization: Mirror host laptop state to the guest via
the laptop-mirror.py reference script (or a libvirt equivalent).
4. Development: Test power management without physical hardware.
Example usage:
--------------
# QMP-controlled battery
qemu-system-x86_64 -device battery \
-qmp unix:/tmp/qmp.sock,server=on,wait=off
# Control via QMP
{"execute": "battery-set-state",
"arguments": {"state": {"present": true, "charging": false,
"discharging": true, "charge-percent": 42,
"rate": 500}}}
# Or, to mirror host laptop state through QMP:
$builddir/run scripts/laptop-mirror.py -s /tmp/qmp.sock
The series has been tested with Windows and Linux guests, correctly
showing battery status, AC adapter state, and lid button events in
guest UIs and triggering appropriate power management actions.
Thanks again for the reviews.
Leonid.
Leonid Bloch (8):
hw/acpi: Support extended GPE handling for additional ACPI devices
docs/specs: Introduce the QEMU Battery documentation
hw/acpi: Introduce the QEMU Battery
docs/specs: Introduce the QEMU AC adapter documentation
hw/acpi: Introduce the QEMU AC adapter
docs/specs: Introduce the QEMU lid button documentation
hw/acpi: Introduce the QEMU lid button
scripts: Add laptop-mirror reference script
MAINTAINERS | 27 ++
docs/specs/acad.rst | 126 ++++++++++
docs/specs/battery.rst | 154 ++++++++++++
docs/specs/button.rst | 131 ++++++++++
docs/specs/index.rst | 3 +
docs/tools/index.rst | 1 +
docs/tools/laptop-mirror.rst | 82 ++++++
hw/acpi/Kconfig | 12 +
hw/acpi/acad-stub.c | 20 ++
hw/acpi/acad.c | 251 ++++++++++++++++++
hw/acpi/battery-stub.c | 20 ++
hw/acpi/battery.c | 364 +++++++++++++++++++++++++++
hw/acpi/button-stub.c | 20 ++
hw/acpi/button.c | 227 +++++++++++++++++
hw/acpi/core.c | 17 +-
hw/acpi/meson.build | 6 +
hw/acpi/trace-events | 12 +
hw/i386/Kconfig | 3 +
include/hw/acpi/acad.h | 25 ++
include/hw/acpi/acpi_dev_interface.h | 3 +
include/hw/acpi/battery.h | 32 +++
include/hw/acpi/button.h | 23 ++
qapi/acpi.json | 165 ++++++++++++
scripts/laptop-mirror.py | 219 ++++++++++++++++
24 files changed, 1941 insertions(+), 2 deletions(-)
create mode 100644 docs/specs/acad.rst
create mode 100644 docs/specs/battery.rst
create mode 100644 docs/specs/button.rst
create mode 100644 docs/tools/laptop-mirror.rst
create mode 100644 hw/acpi/acad-stub.c
create mode 100644 hw/acpi/acad.c
create mode 100644 hw/acpi/battery-stub.c
create mode 100644 hw/acpi/battery.c
create mode 100644 hw/acpi/button-stub.c
create mode 100644 hw/acpi/button.c
create mode 100644 include/hw/acpi/acad.h
create mode 100644 include/hw/acpi/battery.h
create mode 100644 include/hw/acpi/button.h
create mode 100755 scripts/laptop-mirror.py
--
2.54.0
^ permalink raw reply [flat|nested] 11+ messages in thread
* [PATCH v4 1/8] hw/acpi: Support extended GPE handling for additional ACPI devices
2026-05-26 4:29 [PATCH v4 0/8] Introduce a battery, AC adapter, and lid button Leonid Bloch
@ 2026-05-26 4:29 ` Leonid Bloch
2026-05-26 4:29 ` [PATCH v4 2/8] docs/specs: Introduce the QEMU Battery documentation Leonid Bloch
` (6 subsequent siblings)
7 siblings, 0 replies; 11+ messages in thread
From: Leonid Bloch @ 2026-05-26 4:29 UTC (permalink / raw)
To: Michael S . Tsirkin, Igor Mammedov, Ani Sinha, Paolo Bonzini,
Richard Henderson, Eduardo Habkost, Eric Blake, Markus Armbruster,
Marcel Apfelbaum, Dmitry Fleytman
Cc: Leonid Bloch, qemu-devel
This patch extends the GPE (General Purpose Event) handling to support
the maximum number of interrupts available based on the machine's GPE
register length, rather than being limited to the first 8 bits.
This change is needed to support additional ACPI devices that will be
introduced in subsequent patches (Battery, AC adapter, and button devices).
These new devices require GPE event bits beyond the first 8, which were
previously not being properly handled by the event sending and SCI
(System Control Interrupt) update mechanisms.
The actual number of available GPE interrupts varies by machine type:
- PIIX4: GPE_LEN = 4 (32 bits total across status and enable registers)
- ICH9: ICH9_PMIO_GPE0_LEN = 16 (128 bits total)
The patch modifies:
- acpi_send_gpe_event(): Now properly propagates status bits across all
available GPE registers based on the machine's gpe.len value
- acpi_update_sci(): Checks all GPE registers for pending interrupts,
not just the first byte
Note: A future enhancement could refactor the GPE handling to use the
bitmap API from bitops.h instead of the current manual bit manipulation,
which would provide a cleaner interface for these operations.
Signed-off-by: Leonid Bloch <lb.workbox@gmail.com>
---
hw/acpi/core.c | 17 +++++++++++++++--
1 file changed, 15 insertions(+), 2 deletions(-)
diff --git a/hw/acpi/core.c b/hw/acpi/core.c
index a6a62a742d..ad7abe568d 100644
--- a/hw/acpi/core.c
+++ b/hw/acpi/core.c
@@ -733,19 +733,32 @@ uint32_t acpi_gpe_ioport_readb(ACPIREGS *ar, uint32_t addr)
void acpi_send_gpe_event(ACPIREGS *ar, qemu_irq irq,
AcpiEventStatusBits status)
{
- ar->gpe.sts[0] |= status;
+ int i;
+ AcpiEventStatusBits st = status;
+
+ for (i = 0; i < ar->gpe.len / 2; i++) {
+ ar->gpe.sts[i] |= st;
+ st >>= TYPE_WIDTH(ar->gpe.sts[0]);
+ }
+
acpi_update_sci(ar, irq);
}
void acpi_update_sci(ACPIREGS *regs, qemu_irq irq)
{
int sci_level, pm1a_sts;
+ bool gpe_sci = false;
+ int i;
pm1a_sts = acpi_pm1_evt_get_sts(regs);
+ for (i = 0; i < regs->gpe.len / 2; i++) {
+ gpe_sci = gpe_sci || !!(regs->gpe.sts[i] & regs->gpe.en[i]);
+ }
+
sci_level = ((pm1a_sts &
regs->pm1.evt.en & ACPI_BITMASK_PM1_COMMON_ENABLED) != 0) ||
- ((regs->gpe.sts[0] & regs->gpe.en[0]) != 0);
+ gpe_sci;
qemu_set_irq(irq, sci_level);
--
2.54.0
^ permalink raw reply related [flat|nested] 11+ messages in thread
* [PATCH v4 2/8] docs/specs: Introduce the QEMU Battery documentation
2026-05-26 4:29 [PATCH v4 0/8] Introduce a battery, AC adapter, and lid button Leonid Bloch
2026-05-26 4:29 ` [PATCH v4 1/8] hw/acpi: Support extended GPE handling for additional ACPI devices Leonid Bloch
@ 2026-05-26 4:29 ` Leonid Bloch
2026-05-26 4:29 ` [PATCH v4 3/8] hw/acpi: Introduce the QEMU Battery Leonid Bloch
` (5 subsequent siblings)
7 siblings, 0 replies; 11+ messages in thread
From: Leonid Bloch @ 2026-05-26 4:29 UTC (permalink / raw)
To: Michael S . Tsirkin, Igor Mammedov, Ani Sinha, Paolo Bonzini,
Richard Henderson, Eduardo Habkost, Eric Blake, Markus Armbruster,
Marcel Apfelbaum, Dmitry Fleytman
Cc: Leonid Bloch, qemu-devel
Signed-off-by: Leonid Bloch <lb.workbox@gmail.com>
---
MAINTAINERS | 5 ++
docs/specs/battery.rst | 154 +++++++++++++++++++++++++++++++++++++++++
docs/specs/index.rst | 1 +
3 files changed, 160 insertions(+)
create mode 100644 docs/specs/battery.rst
diff --git a/MAINTAINERS b/MAINTAINERS
index cd5c4831e2..e356f46a58 100644
--- a/MAINTAINERS
+++ b/MAINTAINERS
@@ -3036,6 +3036,11 @@ F: hw/nitro/
F: include/hw/nitro/
F: docs/system/nitro.rst
+Battery
+M: Leonid Bloch <lb.workbox@gmail.com>
+S: Maintained
+F: docs/specs/battery.rst
+
Subsystems
----------
Overall Audio backends
diff --git a/docs/specs/battery.rst b/docs/specs/battery.rst
new file mode 100644
index 0000000000..2ce2b90da4
--- /dev/null
+++ b/docs/specs/battery.rst
@@ -0,0 +1,154 @@
+.. SPDX-License-Identifier: GPL-2.0-or-later
+
+==============
+Battery Device
+==============
+
+The battery device provides battery state information to the guest via ACPI.
+Battery state is controlled via QMP commands, providing deterministic control
+for testing and migration safety.
+
+Configuration
+-------------
+
+The battery device is created as an ISA device using ``-device battery``.
+
+Properties
+~~~~~~~~~~
+
+``ioport`` (default: 0x530)
+ I/O port base address for the battery device registers.
+
+ACPI Interface
+--------------
+
+The battery device exposes itself as an ACPI battery device with:
+
+* **_HID**: ``PNP0C0A`` (Control Method Battery)
+* **Device path**: ``\_SB.BAT0``
+
+The device implements standard ACPI battery methods:
+
+``_STA`` (Status)
+ Returns the battery presence status.
+
+``_BIF`` (Battery Information)
+ Returns static battery information including design capacity,
+ technology, and model information.
+
+``_BST`` (Battery Status)
+ Returns dynamic battery status including current state
+ (charging/discharging), present rate, and remaining capacity.
+
+I/O Interface
+-------------
+
+The battery device exposes 12 bytes of I/O space at the configured
+I/O port address with the following layout:
+
+* **Bytes 0-3**: Battery state (DWORD)
+
+ Bits 0-3 hold the ACPI ``_BST`` state (1 = discharging,
+ 2 = charging); bit 4 carries the battery presence flag exposed by
+ ``_STA`` (set when the battery is present).
+
+* **Bytes 4-7**: Battery rate (DWORD)
+
+ Current charge/discharge rate normalized to design capacity.
+
+* **Bytes 8-11**: Battery charge (DWORD)
+
+ Current battery charge level normalized to design capacity.
+
+All values are normalized where the design capacity equals 10000 units.
+
+ACPI Notifications
+------------------
+
+The battery device generates ACPI notifications through GPE events:
+
+* **GPE._E08**: Status Change (0x80) - Battery state change
+
+QMP Commands
+------------
+
+The following QMP commands control the battery state:
+
+``battery-set-state``
+ Set the battery state. Parameters:
+
+ * ``present``: Whether the battery is present (boolean)
+ * ``charging``: Whether the battery is charging (boolean)
+ * ``discharging``: Whether the battery is discharging (boolean)
+ * ``charge-percent``: Battery charge percentage 0-100 (integer)
+ * ``rate``: Charge/discharge rate in mW (optional integer)
+
+ Example::
+
+ -> { "execute": "battery-set-state",
+ "arguments": { "state": {
+ "present": true,
+ "charging": true,
+ "discharging": false,
+ "charge-percent": 85,
+ "rate": 500
+ }}}
+ <- { "return": {} }
+
+``query-battery``
+ Query the current battery state. Returns the same fields as above.
+
+ Example::
+
+ -> { "execute": "query-battery" }
+ <- { "return": {
+ "present": true,
+ "charging": true,
+ "discharging": false,
+ "charge-percent": 85,
+ "rate": 500,
+ "design-capacity": 10000
+ }}
+
+Examples
+--------
+
+Basic usage::
+
+ # Start VM with battery device
+ qemu-system-x86_64 -device battery -qmp tcp:localhost:4444,server,wait=off
+
+ # From another terminal, set battery state via QMP:
+ echo '{"execute":"qmp_capabilities"}
+ {"execute":"battery-set-state",
+ "arguments":{"state":{"present":true,"charging":false,
+ "discharging":true,"charge-percent":42,
+ "rate":500}}}' | \
+ nc -N localhost 4444
+
+Simulate battery discharge::
+
+ # Start with 100% charged battery
+ echo '{"execute":"battery-set-state",
+ "arguments":{"state":{"present":true,"charging":false,
+ "discharging":true,"charge-percent":100,
+ "rate":1000}}}' | nc -N localhost 4444
+
+ # Later, update to 50% charged
+ echo '{"execute":"battery-set-state",
+ "arguments":{"state":{"present":true,"charging":false,
+ "discharging":true,"charge-percent":50,
+ "rate":1000}}}' | nc -N localhost 4444
+
+Simulate charging::
+
+ # Start with 25% battery, begin charging
+ echo '{"execute":"battery-set-state",
+ "arguments":{"state":{"present":true,"charging":true,
+ "discharging":false,"charge-percent":25,
+ "rate":500}}}' | nc -N localhost 4444
+
+Combined with other laptop devices::
+
+ # Create a complete laptop environment
+ qemu-system-x86_64 -device battery -device acad -device button
diff --git a/docs/specs/index.rst b/docs/specs/index.rst
index b7909a108a..3442b76da6 100644
--- a/docs/specs/index.rst
+++ b/docs/specs/index.rst
@@ -22,6 +22,7 @@ guest hardware that is specific to QEMU.
acpi_pci_hotplug
acpi_nvdimm
acpi_erst
+ battery
sev-guest-firmware
fw_cfg
fsi
--
2.54.0
^ permalink raw reply related [flat|nested] 11+ messages in thread
* [PATCH v4 3/8] hw/acpi: Introduce the QEMU Battery
2026-05-26 4:29 [PATCH v4 0/8] Introduce a battery, AC adapter, and lid button Leonid Bloch
2026-05-26 4:29 ` [PATCH v4 1/8] hw/acpi: Support extended GPE handling for additional ACPI devices Leonid Bloch
2026-05-26 4:29 ` [PATCH v4 2/8] docs/specs: Introduce the QEMU Battery documentation Leonid Bloch
@ 2026-05-26 4:29 ` Leonid Bloch
2026-06-02 5:49 ` Markus Armbruster
2026-05-26 4:29 ` [PATCH v4 4/8] docs/specs: Introduce the QEMU AC adapter documentation Leonid Bloch
` (4 subsequent siblings)
7 siblings, 1 reply; 11+ messages in thread
From: Leonid Bloch @ 2026-05-26 4:29 UTC (permalink / raw)
To: Michael S . Tsirkin, Igor Mammedov, Ani Sinha, Paolo Bonzini,
Richard Henderson, Eduardo Habkost, Eric Blake, Markus Armbruster,
Marcel Apfelbaum, Dmitry Fleytman
Cc: Leonid Bloch, qemu-devel
The battery device communicates battery state to the guest via ACPI.
Battery state is controlled programmatically via QMP commands, making
the device deterministic and migration-safe.
Properties:
- 'ioport': I/O port base address (default: 0x530)
The device implements the ACPI_DEV_AML_IF interface to generate its
own AML code, placing the BAT0 device directly under \_SB scope as
per ACPI specification.
QMP commands:
- battery-set-state: Set battery state (present, charging, capacity, rate)
- query-battery: Query current battery state
This provides a stable interface for virtualization management systems.
Signed-off-by: Leonid Bloch <lb.workbox@gmail.com>
Signed-off-by: Marcel Apfelbaum <marcel.apfelbaum@gmail.com>
---
MAINTAINERS | 2 +
hw/acpi/Kconfig | 4 +
hw/acpi/battery-stub.c | 20 ++
hw/acpi/battery.c | 364 +++++++++++++++++++++++++++
hw/acpi/meson.build | 2 +
hw/acpi/trace-events | 4 +
hw/i386/Kconfig | 1 +
include/hw/acpi/acpi_dev_interface.h | 1 +
include/hw/acpi/battery.h | 32 +++
qapi/acpi.json | 71 ++++++
10 files changed, 501 insertions(+)
create mode 100644 hw/acpi/battery-stub.c
create mode 100644 hw/acpi/battery.c
create mode 100644 include/hw/acpi/battery.h
diff --git a/MAINTAINERS b/MAINTAINERS
index e356f46a58..ce50329f48 100644
--- a/MAINTAINERS
+++ b/MAINTAINERS
@@ -3040,6 +3040,8 @@ Battery
M: Leonid Bloch <lb.workbox@gmail.com>
S: Maintained
F: docs/specs/battery.rst
+F: hw/acpi/battery*
+F: include/hw/acpi/battery.h
Subsystems
----------
diff --git a/hw/acpi/Kconfig b/hw/acpi/Kconfig
index daabbe6cd1..6b2c46d37a 100644
--- a/hw/acpi/Kconfig
+++ b/hw/acpi/Kconfig
@@ -88,3 +88,7 @@ config ACPI_ERST
config ACPI_CXL
bool
depends on ACPI
+
+config BATTERY
+ bool
+ depends on ACPI
diff --git a/hw/acpi/battery-stub.c b/hw/acpi/battery-stub.c
new file mode 100644
index 0000000000..d2f13b51c1
--- /dev/null
+++ b/hw/acpi/battery-stub.c
@@ -0,0 +1,20 @@
+/*
+ * QEMU emulated battery device - QMP stubs.
+ *
+ * SPDX-License-Identifier: GPL-2.0-or-later
+ */
+
+#include "qemu/osdep.h"
+#include "qapi/error.h"
+#include "qapi/qapi-commands-acpi.h"
+
+void qmp_battery_set_state(BatteryInfo *state, Error **errp)
+{
+ error_setg(errp, "No battery device found");
+}
+
+BatteryInfo *qmp_query_battery(Error **errp)
+{
+ error_setg(errp, "No battery device found");
+ return NULL;
+}
diff --git a/hw/acpi/battery.c b/hw/acpi/battery.c
new file mode 100644
index 0000000000..35b81ad486
--- /dev/null
+++ b/hw/acpi/battery.c
@@ -0,0 +1,364 @@
+/*
+ * QEMU emulated battery device.
+ *
+ * Copyright (c) 2019-2026 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 "qapi/error.h"
+#include "hw/core/qdev-properties.h"
+#include "migration/vmstate.h"
+#include "hw/acpi/acpi_aml_interface.h"
+#include "qapi/qapi-commands-acpi.h"
+
+#include "hw/acpi/battery.h"
+
+#define BATTERY_DEVICE(obj) OBJECT_CHECK(BatteryState, (obj), TYPE_BATTERY)
+
+#define BATTERY_DISCHARGING 0x01 /* ACPI _BST bit 0 */
+#define BATTERY_CHARGING 0x02 /* ACPI _BST bit 1 */
+#define BATTERY_CRITICAL 0x04 /* ACPI _BST bit 2 */
+#define BATTERY_PRESENT 0x10 /* ACPI _STA bit 4 */
+
+typedef struct BatteryState {
+ ISADevice dev;
+ MemoryRegion io;
+ uint16_t ioport;
+ uint32_t state;
+ uint32_t rate;
+ uint32_t charge;
+ bool qmp_present;
+ bool qmp_charging;
+ bool qmp_discharging;
+ int32_t qmp_charge_percent;
+ int32_t qmp_rate;
+} BatteryState;
+
+enum {
+ BSTA_ADDR = 0,
+ BRTE_ADDR = 4,
+ BCRG_ADDR = 8,
+};
+
+static void battery_get_dynamic_status(BatteryState *s)
+{
+ s->state = 0;
+ if (s->qmp_present) {
+ s->state |= BATTERY_PRESENT;
+ if (s->qmp_charging) {
+ s->state |= BATTERY_CHARGING;
+ }
+ if (s->qmp_discharging) {
+ s->state |= BATTERY_DISCHARGING;
+ }
+ }
+ s->rate = s->qmp_rate;
+ s->charge = (s->qmp_charge_percent * BATTERY_FULL_CAP) / 100;
+
+ trace_battery_get_dynamic_status(s->state, s->rate, s->charge);
+}
+
+static void battery_realize(DeviceState *dev, Error **errp)
+{
+ ISADevice *d = ISA_DEVICE(dev);
+ BatteryState *s = BATTERY_DEVICE(dev);
+ bool ambiguous;
+
+ trace_battery_realize();
+
+ object_resolve_path_type("", TYPE_BATTERY, &ambiguous);
+ if (ambiguous) {
+ error_setg(errp, "at most one %s device is permitted", TYPE_BATTERY);
+ return;
+ }
+
+ /* Initialize QMP state to sensible defaults */
+ s->qmp_present = true;
+ s->qmp_charging = false;
+ s->qmp_discharging = true;
+ s->qmp_charge_percent = 50;
+ s->qmp_rate = 1000; /* 1000 mW discharge rate */
+
+ isa_register_ioport(d, &s->io, s->ioport);
+}
+
+static const Property battery_device_properties[] = {
+ DEFINE_PROP_UINT16(BATTERY_IOPORT_PROP, BatteryState, ioport, 0x530),
+};
+
+static const VMStateDescription battery_vmstate = {
+ .name = "battery",
+ .version_id = 1,
+ .minimum_version_id = 1,
+ .fields = (VMStateField[]) {
+ VMSTATE_BOOL(qmp_present, BatteryState),
+ VMSTATE_BOOL(qmp_charging, BatteryState),
+ VMSTATE_BOOL(qmp_discharging, BatteryState),
+ VMSTATE_INT32(qmp_charge_percent, BatteryState),
+ VMSTATE_INT32(qmp_rate, BatteryState),
+ VMSTATE_END_OF_LIST()
+ }
+};
+
+static void build_battery_aml(AcpiDevAmlIf *adev, Aml *scope)
+{
+ Aml *dev, *field, *method, *pkg;
+ Aml *bat_state, *bat_rate, *bat_charge;
+ Aml *sb_scope;
+ BatteryState *s = BATTERY_DEVICE(adev);
+
+ bat_state = aml_local(0);
+ bat_rate = aml_local(1);
+ bat_charge = aml_local(2);
+
+ sb_scope = aml_scope("\\_SB");
+ dev = aml_device("BAT0");
+ aml_append(dev, aml_name_decl("_HID", aml_eisaid("PNP0C0A")));
+
+ aml_append(dev, aml_operation_region("DBST", AML_SYSTEM_IO,
+ aml_int(s->ioport),
+ BATTERY_LEN));
+ field = aml_field("DBST", AML_DWORD_ACC, AML_NOLOCK, AML_PRESERVE);
+ aml_append(field, aml_named_field("BSTA", 32));
+ aml_append(field, aml_named_field("BRTE", 32));
+ aml_append(field, aml_named_field("BCRG", 32));
+ aml_append(dev, field);
+
+ method = aml_method("_STA", 0, AML_NOTSERIALIZED);
+ aml_append(method, aml_return(aml_or(aml_int(0x0F),
+ aml_and(aml_name("BSTA"),
+ aml_int(0x10), NULL),
+ NULL)));
+ aml_append(dev, method);
+
+ method = aml_method("_BIF", 0, AML_NOTSERIALIZED);
+ pkg = aml_package(13);
+ /* Power Unit */
+ aml_append(pkg, aml_int(0)); /* mW */
+ /* Design Capacity */
+ aml_append(pkg, aml_int(BATTERY_FULL_CAP));
+ /* Last Full Charge Capacity */
+ aml_append(pkg, aml_int(BATTERY_FULL_CAP));
+ /* Battery Technology */
+ aml_append(pkg, aml_int(1)); /* Secondary */
+ /* Design Voltage */
+ aml_append(pkg, aml_int(BATTERY_DESIGN_VOLTAGE));
+ /* Design Capacity of Warning */
+ aml_append(pkg, aml_int(BATTERY_CAPACITY_OF_WARNING));
+ /* Design Capacity of Low */
+ aml_append(pkg, aml_int(BATTERY_CAPACITY_OF_LOW));
+ /* Battery Capacity Granularity 1 */
+ aml_append(pkg, aml_int(BATTERY_CAPACITY_GRANULARITY));
+ /* Battery Capacity Granularity 2 */
+ aml_append(pkg, aml_int(BATTERY_CAPACITY_GRANULARITY));
+ /* Model Number */
+ aml_append(pkg, aml_string("QBAT001")); /* Model Number */
+ /* Serial Number */
+ aml_append(pkg, aml_string("SN00000")); /* Serial Number */
+ /* Battery Type */
+ aml_append(pkg, aml_string("Virtual")); /* Battery Type */
+ /* OEM Information */
+ aml_append(pkg, aml_string("QEMU")); /* OEM Information */
+ aml_append(method, aml_return(pkg));
+ aml_append(dev, method);
+
+ pkg = aml_package(4);
+ /* Battery State */
+ aml_append(pkg, aml_int(0));
+ /* Battery Present Rate */
+ aml_append(pkg, aml_int(BATTERY_VAL_UNKNOWN));
+ /* Battery Remaining Capacity */
+ aml_append(pkg, aml_int(BATTERY_VAL_UNKNOWN));
+ /* Battery Present Voltage */
+ aml_append(pkg, aml_int(BATTERY_DESIGN_VOLTAGE));
+ aml_append(dev, aml_name_decl("DBPR", pkg));
+
+ method = aml_method("_BST", 0, AML_NOTSERIALIZED);
+ aml_append(method, aml_store(aml_and(aml_name("BSTA"), aml_int(0x0F),
+ NULL),
+ bat_state));
+ aml_append(method, aml_store(aml_name("BRTE"), bat_rate));
+ aml_append(method, aml_store(aml_name("BCRG"), bat_charge));
+ aml_append(method, aml_store(bat_state,
+ aml_index(aml_name("DBPR"), aml_int(0))));
+ aml_append(method, aml_store(bat_rate,
+ aml_index(aml_name("DBPR"), aml_int(1))));
+ aml_append(method, aml_store(bat_charge,
+ aml_index(aml_name("DBPR"), aml_int(2))));
+ aml_append(method, aml_return(aml_name("DBPR")));
+ aml_append(dev, method);
+
+ aml_append(sb_scope, dev);
+ aml_append(scope, sb_scope);
+
+ /* Status Change */
+ method = aml_method("\\_GPE._E08", 0, AML_NOTSERIALIZED);
+ aml_append(method, aml_notify(aml_name("\\_SB.BAT0"), aml_int(0x80)));
+ aml_append(scope, method);
+}
+
+static void battery_class_init(ObjectClass *class, const void *data)
+{
+ DeviceClass *dc = DEVICE_CLASS(class);
+ AcpiDevAmlIfClass *adevc = ACPI_DEV_AML_IF_CLASS(class);
+
+ dc->realize = battery_realize;
+ dc->hotpluggable = false;
+ device_class_set_props(dc, battery_device_properties);
+ dc->vmsd = &battery_vmstate;
+ adevc->build_dev_aml = build_battery_aml;
+}
+
+static uint64_t battery_ioport_read(void *opaque, hwaddr addr, unsigned size)
+{
+ BatteryState *s = opaque;
+
+ battery_get_dynamic_status(s);
+
+ switch (addr) {
+ case BSTA_ADDR:
+ return s->state;
+ case BRTE_ADDR:
+ return s->rate;
+ case BCRG_ADDR:
+ return s->charge;
+ default:
+ g_assert_not_reached();
+ }
+}
+
+static const MemoryRegionOps battery_ops = {
+ .read = battery_ioport_read,
+ .endianness = DEVICE_LITTLE_ENDIAN,
+ .valid = {
+ .min_access_size = 4,
+ .max_access_size = 4,
+ },
+ .impl = {
+ .min_access_size = 4,
+ .max_access_size = 4,
+ },
+};
+
+static void battery_instance_init(Object *obj)
+{
+ BatteryState *s = BATTERY_DEVICE(obj);
+
+ memory_region_init_io(&s->io, obj, &battery_ops, s, "battery",
+ BATTERY_LEN);
+}
+
+static const TypeInfo battery_info = {
+ .name = TYPE_BATTERY,
+ .parent = TYPE_ISA_DEVICE,
+ .instance_size = sizeof(BatteryState),
+ .class_init = battery_class_init,
+ .instance_init = battery_instance_init,
+ .interfaces = (InterfaceInfo[]) {
+ { TYPE_ACPI_DEV_AML_IF },
+ { },
+ },
+};
+
+static BatteryState *find_battery_device(Error **errp)
+{
+ bool ambiguous;
+ Object *o = object_resolve_path_type("", TYPE_BATTERY, &ambiguous);
+
+ if (!o) {
+ error_setg(errp, "No battery device found");
+ return NULL;
+ }
+ if (ambiguous) {
+ error_setg(errp, "More than one battery device present");
+ return NULL;
+ }
+ return BATTERY_DEVICE(o);
+}
+
+void qmp_battery_set_state(BatteryInfo *state, Error **errp)
+{
+ BatteryState *s = find_battery_device(errp);
+ Object *obj;
+
+ if (!s) {
+ return;
+ }
+
+ if (state->charging && state->discharging) {
+ error_setg(errp,
+ "'charging' and 'discharging' are mutually exclusive");
+ return;
+ }
+ if (!state->present && (state->charging || state->discharging)) {
+ error_setg(errp,
+ "'charging'/'discharging' require 'present' to be true");
+ return;
+ }
+ if (state->charge_percent < 0 || state->charge_percent > 100) {
+ error_setg(errp, "'charge-percent' must be in the range 0..100");
+ return;
+ }
+ if (state->has_rate && (state->rate < 0 || state->rate > INT32_MAX)) {
+ error_setg(errp, "'rate' must be in the range 0..0x%" PRIX32,
+ (uint32_t)INT32_MAX);
+ return;
+ }
+
+ s->qmp_present = state->present;
+ s->qmp_charging = state->charging;
+ s->qmp_discharging = state->discharging;
+ s->qmp_charge_percent = state->charge_percent;
+
+ if (state->has_rate) {
+ s->qmp_rate = state->rate;
+ }
+
+ obj = object_resolve_path_type("", TYPE_ACPI_DEVICE_IF, NULL);
+ if (obj) {
+ acpi_send_event(DEVICE(obj), ACPI_BATTERY_CHANGE_STATUS);
+ }
+}
+
+BatteryInfo *qmp_query_battery(Error **errp)
+{
+ BatteryState *s = find_battery_device(errp);
+ BatteryInfo *ret;
+
+ if (!s) {
+ return NULL;
+ }
+
+ ret = g_new0(BatteryInfo, 1);
+
+ ret->present = s->qmp_present;
+ ret->charging = s->qmp_charging;
+ ret->discharging = s->qmp_discharging;
+ ret->charge_percent = s->qmp_charge_percent;
+ ret->has_rate = true;
+ ret->rate = s->qmp_rate;
+
+ ret->has_remaining_capacity = false;
+ ret->has_design_capacity = true;
+ ret->design_capacity = BATTERY_FULL_CAP;
+
+ return ret;
+}
+
+static void battery_register_types(void)
+{
+ type_register_static(&battery_info);
+}
+
+type_init(battery_register_types)
diff --git a/hw/acpi/meson.build b/hw/acpi/meson.build
index 1c5251909b..e6bc78274e 100644
--- a/hw/acpi/meson.build
+++ b/hw/acpi/meson.build
@@ -35,6 +35,8 @@ if have_tpm
endif
stub_ss.add(files('acpi-stub.c', 'aml-build-stub.c', 'ghes-stub.c'))
stub_ss.add(files('pci-bridge-stub.c'))
+acpi_ss.add(when: 'CONFIG_BATTERY', if_true: files('battery.c'))
+stub_ss.add(files('battery-stub.c'))
system_ss.add_all(when: 'CONFIG_ACPI', if_true: acpi_ss)
system_ss.add(when: 'CONFIG_GHES_CPER', if_true: files('ghes_cper.c'))
stub_ss.add(files('ghes_cper_stub.c'))
diff --git a/hw/acpi/trace-events b/hw/acpi/trace-events
index edc93e703c..8a6ab91a13 100644
--- a/hw/acpi/trace-events
+++ b/hw/acpi/trace-events
@@ -87,3 +87,7 @@ acpi_nvdimm_read_io_port(void) "Alert: we never read _DSM IO Port"
acpi_nvdimm_dsm_mem_addr(uint64_t dsm_mem_addr) "dsm memory address 0x%" PRIx64
acpi_nvdimm_dsm_info(uint32_t revision, uint32_t handle, uint32_t function) "Revision 0x%" PRIx32 " Handle 0x%" PRIx32 " Function 0x%" PRIx32
acpi_nvdimm_invalid_revision(uint32_t revision) "Revision 0x%" PRIx32 " is not supported, expect 0x1"
+
+# battery.c
+battery_realize(void) "Battery device realize entry"
+battery_get_dynamic_status(uint32_t state, uint32_t rate, uint32_t charge) "Battery read state: 0x%"PRIx32", rate: %"PRIu32", charge: %"PRIu32
diff --git a/hw/i386/Kconfig b/hw/i386/Kconfig
index 12473acaa7..94004ffeb2 100644
--- a/hw/i386/Kconfig
+++ b/hw/i386/Kconfig
@@ -39,6 +39,7 @@ config PC
imply VIRTIO_VGA
imply NVDIMM
imply FDC_ISA
+ imply BATTERY
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 65debb90a8..a6f9022c0b 100644
--- a/include/hw/acpi/acpi_dev_interface.h
+++ b/include/hw/acpi/acpi_dev_interface.h
@@ -14,6 +14,7 @@ typedef enum {
ACPI_VMGENID_CHANGE_STATUS = 32,
ACPI_POWER_DOWN_STATUS = 64,
ACPI_GENERIC_ERROR = 128,
+ ACPI_BATTERY_CHANGE_STATUS = 256,
} AcpiEventStatusBits;
#define TYPE_ACPI_DEVICE_IF "acpi-device-interface"
diff --git a/include/hw/acpi/battery.h b/include/hw/acpi/battery.h
new file mode 100644
index 0000000000..eaff760db9
--- /dev/null
+++ b/include/hw/acpi/battery.h
@@ -0,0 +1,32 @@
+/*
+ * QEMU emulated battery device.
+ *
+ * Copyright (c) 2019-2026 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_BATTERY_H
+#define HW_ACPI_BATTERY_H
+
+#define TYPE_BATTERY "battery"
+#define BATTERY_IOPORT_PROP "ioport"
+
+#define BATTERY_FULL_CAP 10000 /* mWh */
+#define BATTERY_DESIGN_VOLTAGE 12000 /* mV */
+
+#define BATTERY_CAPACITY_OF_WARNING (BATTERY_FULL_CAP / 10) /* 10% */
+#define BATTERY_CAPACITY_OF_LOW (BATTERY_FULL_CAP / 25) /* 4% */
+#define BATTERY_CAPACITY_GRANULARITY (BATTERY_FULL_CAP / 100) /* 1% */
+
+#define BATTERY_VAL_UNKNOWN 0xFFFFFFFF
+
+#define BATTERY_LEN 0x0C
+
+#endif
diff --git a/qapi/acpi.json b/qapi/acpi.json
index 906b3687a5..4711a05614 100644
--- a/qapi/acpi.json
+++ b/qapi/acpi.json
@@ -142,3 +142,74 @@
##
{ 'event': 'ACPI_DEVICE_OST',
'data': { 'info': 'ACPIOSTInfo' } }
+
+##
+# @BatteryInfo:
+#
+# Battery state information
+#
+# @present: whether the battery is present
+#
+# @charging: whether the battery is charging
+#
+# @discharging: whether the battery is discharging
+#
+# @charge-percent: battery charge percentage (0-100)
+#
+# @rate: charge/discharge rate in mW (optional)
+#
+# @remaining-capacity: remaining capacity in mWh (optional)
+#
+# @design-capacity: design capacity in mWh (optional)
+#
+# Since: 11.1
+##
+{ 'struct': 'BatteryInfo',
+ 'data': { 'present': 'bool',
+ 'charging': 'bool',
+ 'discharging': 'bool',
+ 'charge-percent': 'int',
+ '*rate': 'int',
+ '*remaining-capacity': 'int',
+ '*design-capacity': 'int' } }
+
+##
+# @battery-set-state:
+#
+# Set the state of the emulated battery device
+#
+# @state: new battery state
+#
+# Since: 11.1
+#
+# .. qmp-example::
+#
+# -> { "execute": "battery-set-state",
+# "arguments": { "state": { "present": true,
+# "charging": true,
+# "discharging": false,
+# "charge-percent": 85 } } }
+# <- { "return": {} }
+##
+{ 'command': 'battery-set-state',
+ 'data': { 'state': 'BatteryInfo' } }
+
+##
+# @query-battery:
+#
+# Query the current state of the emulated battery device
+#
+# Returns: current battery state
+#
+# Since: 11.1
+#
+# .. qmp-example::
+#
+# -> { "execute": "query-battery" }
+# <- { "return": { "present": true,
+# "charging": true,
+# "discharging": false,
+# "charge-percent": 85 } }
+##
+{ 'command': 'query-battery',
+ 'returns': 'BatteryInfo' }
--
2.54.0
^ permalink raw reply related [flat|nested] 11+ messages in thread
* [PATCH v4 4/8] docs/specs: Introduce the QEMU AC adapter documentation
2026-05-26 4:29 [PATCH v4 0/8] Introduce a battery, AC adapter, and lid button Leonid Bloch
` (2 preceding siblings ...)
2026-05-26 4:29 ` [PATCH v4 3/8] hw/acpi: Introduce the QEMU Battery Leonid Bloch
@ 2026-05-26 4:29 ` Leonid Bloch
2026-05-26 4:29 ` [PATCH v4 5/8] hw/acpi: Introduce the QEMU AC adapter Leonid Bloch
` (3 subsequent siblings)
7 siblings, 0 replies; 11+ messages in thread
From: Leonid Bloch @ 2026-05-26 4:29 UTC (permalink / raw)
To: Michael S . Tsirkin, Igor Mammedov, Ani Sinha, Paolo Bonzini,
Richard Henderson, Eduardo Habkost, Eric Blake, Markus Armbruster,
Marcel Apfelbaum, Dmitry Fleytman
Cc: Leonid Bloch, qemu-devel
Signed-off-by: Leonid Bloch <lb.workbox@gmail.com>
---
MAINTAINERS | 5 ++
docs/specs/acad.rst | 126 +++++++++++++++++++++++++++++++++++++++++++
docs/specs/index.rst | 1 +
3 files changed, 132 insertions(+)
create mode 100644 docs/specs/acad.rst
diff --git a/MAINTAINERS b/MAINTAINERS
index ce50329f48..47436ec878 100644
--- a/MAINTAINERS
+++ b/MAINTAINERS
@@ -3043,6 +3043,11 @@ F: docs/specs/battery.rst
F: hw/acpi/battery*
F: include/hw/acpi/battery.h
+AC Adapter
+M: Leonid Bloch <lb.workbox@gmail.com>
+S: Maintained
+F: docs/specs/acad.rst
+
Subsystems
----------
Overall Audio backends
diff --git a/docs/specs/acad.rst b/docs/specs/acad.rst
new file mode 100644
index 0000000000..af79b4b8d7
--- /dev/null
+++ b/docs/specs/acad.rst
@@ -0,0 +1,126 @@
+.. SPDX-License-Identifier: GPL-2.0-or-later
+
+=================
+AC Adapter Device
+=================
+
+The AC adapter device provides AC power state information to the guest via ACPI.
+AC adapter state is controlled via QMP commands, providing deterministic control
+for testing and migration safety.
+
+Configuration
+-------------
+
+The AC adapter device is created as an ISA device using ``-device acad``.
+
+Properties
+~~~~~~~~~~
+
+``ioport`` (default: 0x53c)
+ I/O port base address for the AC adapter device register.
+
+ACPI Interface
+--------------
+
+The AC adapter device is exposed to the guest as an ACPI device with:
+
+* **HID**: ``ACPI0003`` (AC Adapter)
+* **Device Path**: ``\_SB.ADP0``
+* **Notification Values**:
+
+ * ``0x80``: Status change (connected/disconnected)
+
+ACPI Methods
+~~~~~~~~~~~~
+
+``_PSR`` (Power Source)
+ Returns the current AC adapter state (0 = offline, 1 = online).
+
+``_PCL`` (Power Consumer List)
+ Returns the list of devices powered by this adapter.
+
+``_PIF`` (Power Source Information)
+ Returns static information about the power source including model number,
+ serial number, and OEM information.
+
+I/O Interface
+-------------
+
+The device uses a single I/O port register:
+
+* **Port**: ``ioport`` property value (default 0x53c)
+* **Size**: 1 byte
+* **Access**: Read-only
+
+Register Layout
+~~~~~~~~~~~~~~~
+
+**PWRS** (offset 0x00, 1 byte)
+ Current AC adapter state:
+
+ * ``0x00``: AC adapter offline (unplugged)
+ * ``0x01``: AC adapter online (plugged in)
+
+QMP Commands
+------------
+
+The following QMP commands control the AC adapter state:
+
+``ac-adapter-set-state``
+ Set the AC adapter connection state.
+
+ * ``connected``: Whether the AC adapter is connected (boolean)
+
+ Example::
+
+ -> { "execute": "ac-adapter-set-state",
+ "arguments": { "connected": true }}
+ <- { "return": {} }
+
+``query-ac-adapter``
+ Query the current AC adapter state.
+
+ Example::
+
+ -> { "execute": "query-ac-adapter" }
+ <- { "return": { "connected": true }}
+
+Examples
+--------
+
+Basic usage::
+
+ # Start VM with AC adapter
+ qemu-system-x86_64 -device acad -qmp tcp:localhost:4444,server,wait=off
+
+ # From another terminal, set AC adapter state via QMP:
+ echo '{"execute":"qmp_capabilities"}
+ {"execute":"ac-adapter-set-state",
+ "arguments":{"connected":true}}' | \
+ nc -N localhost 4444
+
+Simulate unplugging AC adapter::
+
+ # Start with AC adapter connected
+ echo '{"execute":"ac-adapter-set-state",
+ "arguments":{"connected":true}}' | nc -N localhost 4444
+
+ # Later, disconnect AC adapter
+ echo '{"execute":"ac-adapter-set-state",
+ "arguments":{"connected":false}}' | nc -N localhost 4444
+
+ # Reconnect AC adapter
+ echo '{"execute":"ac-adapter-set-state",
+ "arguments":{"connected":true}}' | nc -N localhost 4444
+
+Combined with battery device::
+
+ # Create a complete laptop power environment
+ qemu-system-x86_64 -device battery -device acad
+
+ # Simulate unplugging AC while on battery
+ echo '{"execute":"battery-set-state",
+ "arguments":{"state":{"present":true,"charging":false,
+ "discharging":true,"charge-percent":75}}}
+ {"execute":"ac-adapter-set-state",
+ "arguments":{"connected":false}}' | nc -N localhost 4444
diff --git a/docs/specs/index.rst b/docs/specs/index.rst
index 3442b76da6..a19c75384c 100644
--- a/docs/specs/index.rst
+++ b/docs/specs/index.rst
@@ -22,6 +22,7 @@ guest hardware that is specific to QEMU.
acpi_pci_hotplug
acpi_nvdimm
acpi_erst
+ acad
battery
sev-guest-firmware
fw_cfg
--
2.54.0
^ permalink raw reply related [flat|nested] 11+ messages in thread
* [PATCH v4 5/8] hw/acpi: Introduce the QEMU AC adapter
2026-05-26 4:29 [PATCH v4 0/8] Introduce a battery, AC adapter, and lid button Leonid Bloch
` (3 preceding siblings ...)
2026-05-26 4:29 ` [PATCH v4 4/8] docs/specs: Introduce the QEMU AC adapter documentation Leonid Bloch
@ 2026-05-26 4:29 ` Leonid Bloch
2026-06-02 5:51 ` Markus Armbruster
2026-05-26 4:29 ` [PATCH v4 6/8] docs/specs: Introduce the QEMU lid button documentation Leonid Bloch
` (2 subsequent siblings)
7 siblings, 1 reply; 11+ messages in thread
From: Leonid Bloch @ 2026-05-26 4:29 UTC (permalink / raw)
To: Michael S . Tsirkin, Igor Mammedov, Ani Sinha, Paolo Bonzini,
Richard Henderson, Eduardo Habkost, Eric Blake, Markus Armbruster,
Marcel Apfelbaum, Dmitry Fleytman
Cc: Leonid Bloch, qemu-devel
The AC adapter device communicates AC power state to the guest via ACPI.
AC adapter state is controlled programmatically via QMP commands,
ensuring deterministic behavior.
Properties:
- 'ioport': I/O port base address (default: 0x53c)
The device implements the ACPI_DEV_AML_IF interface to generate its
own AML code, placing the ADP0 device directly under \_SB scope.
QMP commands:
- ac-adapter-set-state: Set AC adapter connection state
- query-ac-adapter: Query current AC adapter state
Signed-off-by: Leonid Bloch <lb.workbox@gmail.com>
---
MAINTAINERS | 2 +
hw/acpi/Kconfig | 4 +
hw/acpi/acad-stub.c | 20 +++
hw/acpi/acad.c | 251 +++++++++++++++++++++++++++
hw/acpi/meson.build | 2 +
hw/acpi/trace-events | 4 +
hw/i386/Kconfig | 1 +
include/hw/acpi/acad.h | 25 +++
include/hw/acpi/acpi_dev_interface.h | 1 +
qapi/acpi.json | 47 +++++
10 files changed, 357 insertions(+)
create mode 100644 hw/acpi/acad-stub.c
create mode 100644 hw/acpi/acad.c
create mode 100644 include/hw/acpi/acad.h
diff --git a/MAINTAINERS b/MAINTAINERS
index 47436ec878..90941519e3 100644
--- a/MAINTAINERS
+++ b/MAINTAINERS
@@ -3047,6 +3047,8 @@ AC Adapter
M: Leonid Bloch <lb.workbox@gmail.com>
S: Maintained
F: docs/specs/acad.rst
+F: hw/acpi/acad*
+F: include/hw/acpi/acad.h
Subsystems
----------
diff --git a/hw/acpi/Kconfig b/hw/acpi/Kconfig
index 6b2c46d37a..889ace2dfa 100644
--- a/hw/acpi/Kconfig
+++ b/hw/acpi/Kconfig
@@ -74,6 +74,10 @@ config ACPI_VIOT
bool
depends on ACPI
+config AC_ADAPTER
+ bool
+ depends on ACPI
+
config ACPI_HW_REDUCED
bool
select ACPI
diff --git a/hw/acpi/acad-stub.c b/hw/acpi/acad-stub.c
new file mode 100644
index 0000000000..92093b7682
--- /dev/null
+++ b/hw/acpi/acad-stub.c
@@ -0,0 +1,20 @@
+/*
+ * QEMU emulated AC adapter device - QMP stubs.
+ *
+ * SPDX-License-Identifier: GPL-2.0-or-later
+ */
+
+#include "qemu/osdep.h"
+#include "qapi/error.h"
+#include "qapi/qapi-commands-acpi.h"
+
+void qmp_ac_adapter_set_state(bool connected, Error **errp)
+{
+ error_setg(errp, "No AC adapter device found");
+}
+
+AcAdapterInfo *qmp_query_ac_adapter(Error **errp)
+{
+ error_setg(errp, "No AC adapter device found");
+ return NULL;
+}
diff --git a/hw/acpi/acad.c b/hw/acpi/acad.c
new file mode 100644
index 0000000000..11777fbb3c
--- /dev/null
+++ b/hw/acpi/acad.c
@@ -0,0 +1,251 @@
+/*
+ * QEMU emulated AC adapter device.
+ *
+ * Copyright (c) 2019-2026 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 "qapi/error.h"
+#include "hw/core/qdev-properties.h"
+#include "migration/vmstate.h"
+#include "hw/acpi/acpi_aml_interface.h"
+#include "qapi/qapi-commands-acpi.h"
+
+#include "hw/acpi/acad.h"
+
+#define AC_ADAPTER_DEVICE(obj) OBJECT_CHECK(ACADState, (obj), \
+ TYPE_AC_ADAPTER)
+
+#define AC_STA_ADDR 0
+
+enum {
+ AC_ADAPTER_OFFLINE = 0,
+ AC_ADAPTER_ONLINE = 1,
+};
+
+typedef struct ACADState {
+ ISADevice dev;
+ MemoryRegion io;
+ uint16_t ioport;
+ uint8_t state;
+ bool qmp_connected;
+} ACADState;
+
+static void acad_get_dynamic_status(ACADState *s)
+{
+ s->state = s->qmp_connected ? AC_ADAPTER_ONLINE : AC_ADAPTER_OFFLINE;
+
+ trace_acad_get_dynamic_status(s->state);
+}
+
+static void acad_realize(DeviceState *dev, Error **errp)
+{
+ ISADevice *d = ISA_DEVICE(dev);
+ ACADState *s = AC_ADAPTER_DEVICE(dev);
+ bool ambiguous;
+
+ trace_acad_realize();
+
+ object_resolve_path_type("", TYPE_AC_ADAPTER, &ambiguous);
+ if (ambiguous) {
+ error_setg(errp, "at most one %s device is permitted",
+ TYPE_AC_ADAPTER);
+ return;
+ }
+
+ /* Initialize to disconnected by default */
+ s->qmp_connected = false;
+
+ isa_register_ioport(d, &s->io, s->ioport);
+}
+
+static const Property acad_device_properties[] = {
+ DEFINE_PROP_UINT16(AC_ADAPTER_IOPORT_PROP, ACADState, ioport, 0x53c),
+};
+
+static const VMStateDescription acad_vmstate = {
+ .name = "acad",
+ .version_id = 1,
+ .minimum_version_id = 1,
+ .fields = (VMStateField[]) {
+ VMSTATE_BOOL(qmp_connected, ACADState),
+ VMSTATE_END_OF_LIST()
+ }
+};
+
+static void build_acad_aml(AcpiDevAmlIf *adev, Aml *scope)
+{
+ Aml *dev, *field, *method, *pkg;
+ Aml *acad_state;
+ Aml *sb_scope;
+ ACADState *s = AC_ADAPTER_DEVICE(adev);
+
+ acad_state = aml_local(0);
+
+ sb_scope = aml_scope("\\_SB");
+ dev = aml_device("ADP0");
+ aml_append(dev, aml_name_decl("_HID", aml_string("ACPI0003")));
+
+ aml_append(dev, aml_operation_region("ACST", AML_SYSTEM_IO,
+ aml_int(s->ioport),
+ AC_ADAPTER_LEN));
+ field = aml_field("ACST", AML_BYTE_ACC, AML_NOLOCK, AML_PRESERVE);
+ aml_append(field, aml_named_field("PWRS", 8));
+ aml_append(dev, field);
+
+ method = aml_method("_PSR", 0, AML_NOTSERIALIZED);
+ aml_append(method, aml_store(aml_name("PWRS"), acad_state));
+ aml_append(method, aml_return(acad_state));
+ aml_append(dev, method);
+
+ method = aml_method("_PCL", 0, AML_NOTSERIALIZED);
+ pkg = aml_package(1);
+ aml_append(pkg, aml_name("_SB"));
+ aml_append(method, aml_return(pkg));
+ aml_append(dev, method);
+
+ method = aml_method("_PIF", 0, AML_NOTSERIALIZED);
+ pkg = aml_package(6);
+ /* Power Source State */
+ aml_append(pkg, aml_int(0)); /* Non-redundant, non-shared */
+ /* Maximum Output Power */
+ aml_append(pkg, aml_int(AC_ADAPTER_VAL_UNKNOWN));
+ /* Maximum Input Power */
+ aml_append(pkg, aml_int(AC_ADAPTER_VAL_UNKNOWN));
+ /* Model Number */
+ aml_append(pkg, aml_string("QADP001"));
+ /* Serial Number */
+ aml_append(pkg, aml_string("SN00000"));
+ /* OEM Information */
+ aml_append(pkg, aml_string("QEMU"));
+ aml_append(method, aml_return(pkg));
+ 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.ADP0"), aml_int(0x80)));
+ aml_append(scope, method);
+}
+
+static void acad_class_init(ObjectClass *class, const void *data)
+{
+ DeviceClass *dc = DEVICE_CLASS(class);
+ AcpiDevAmlIfClass *adevc = ACPI_DEV_AML_IF_CLASS(class);
+
+ dc->realize = acad_realize;
+ dc->hotpluggable = false;
+ device_class_set_props(dc, acad_device_properties);
+ dc->vmsd = &acad_vmstate;
+ adevc->build_dev_aml = build_acad_aml;
+}
+
+static uint64_t acad_ioport_read(void *opaque, hwaddr addr, unsigned size)
+{
+ ACADState *s = opaque;
+
+ acad_get_dynamic_status(s);
+
+ switch (addr) {
+ case AC_STA_ADDR:
+ return s->state;
+ default:
+ g_assert_not_reached();
+ }
+}
+
+static const MemoryRegionOps acad_ops = {
+ .read = acad_ioport_read,
+ .impl = {
+ .min_access_size = 1,
+ .max_access_size = 1,
+ },
+};
+
+static void acad_instance_init(Object *obj)
+{
+ ACADState *s = AC_ADAPTER_DEVICE(obj);
+
+ memory_region_init_io(&s->io, obj, &acad_ops, s, "acad",
+ AC_ADAPTER_LEN);
+}
+
+static const TypeInfo acad_info = {
+ .name = TYPE_AC_ADAPTER,
+ .parent = TYPE_ISA_DEVICE,
+ .instance_size = sizeof(ACADState),
+ .class_init = acad_class_init,
+ .instance_init = acad_instance_init,
+ .interfaces = (InterfaceInfo[]) {
+ { TYPE_ACPI_DEV_AML_IF },
+ { },
+ },
+};
+
+static ACADState *find_acad_device(Error **errp)
+{
+ bool ambiguous;
+ Object *o = object_resolve_path_type("", TYPE_AC_ADAPTER, &ambiguous);
+
+ if (!o) {
+ error_setg(errp, "No AC adapter device found");
+ return NULL;
+ }
+ if (ambiguous) {
+ error_setg(errp, "More than one AC adapter device present");
+ return NULL;
+ }
+ return AC_ADAPTER_DEVICE(o);
+}
+
+void qmp_ac_adapter_set_state(bool connected, Error **errp)
+{
+ ACADState *s = find_acad_device(errp);
+ Object *obj;
+
+ if (!s) {
+ return;
+ }
+
+ s->qmp_connected = connected;
+
+ obj = object_resolve_path_type("", TYPE_ACPI_DEVICE_IF, NULL);
+ if (obj) {
+ acpi_send_event(DEVICE(obj), ACPI_AC_ADAPTER_CHANGE_STATUS);
+ }
+}
+
+AcAdapterInfo *qmp_query_ac_adapter(Error **errp)
+{
+ ACADState *s = find_acad_device(errp);
+ AcAdapterInfo *ret;
+
+ if (!s) {
+ return NULL;
+ }
+
+ ret = g_new0(AcAdapterInfo, 1);
+ ret->connected = s->qmp_connected;
+
+ return ret;
+}
+
+static void acad_register_types(void)
+{
+ type_register_static(&acad_info);
+}
+
+type_init(acad_register_types)
diff --git a/hw/acpi/meson.build b/hw/acpi/meson.build
index e6bc78274e..731e9477e3 100644
--- a/hw/acpi/meson.build
+++ b/hw/acpi/meson.build
@@ -37,6 +37,8 @@ stub_ss.add(files('acpi-stub.c', 'aml-build-stub.c', 'ghes-stub.c'))
stub_ss.add(files('pci-bridge-stub.c'))
acpi_ss.add(when: 'CONFIG_BATTERY', if_true: files('battery.c'))
stub_ss.add(files('battery-stub.c'))
+acpi_ss.add(when: 'CONFIG_AC_ADAPTER', if_true: files('acad.c'))
+stub_ss.add(files('acad-stub.c'))
system_ss.add_all(when: 'CONFIG_ACPI', if_true: acpi_ss)
system_ss.add(when: 'CONFIG_GHES_CPER', if_true: files('ghes_cper.c'))
stub_ss.add(files('ghes_cper_stub.c'))
diff --git a/hw/acpi/trace-events b/hw/acpi/trace-events
index 8a6ab91a13..67602000f3 100644
--- a/hw/acpi/trace-events
+++ b/hw/acpi/trace-events
@@ -91,3 +91,7 @@ acpi_nvdimm_invalid_revision(uint32_t revision) "Revision 0x%" PRIx32 " is not s
# battery.c
battery_realize(void) "Battery device realize entry"
battery_get_dynamic_status(uint32_t state, uint32_t rate, uint32_t charge) "Battery read state: 0x%"PRIx32", rate: %"PRIu32", charge: %"PRIu32
+
+# acad.c
+acad_realize(void) "AC adapter device realize entry"
+acad_get_dynamic_status(uint8_t state) "AC adapter read state: %"PRIu8
diff --git a/hw/i386/Kconfig b/hw/i386/Kconfig
index 94004ffeb2..06f21cadb7 100644
--- a/hw/i386/Kconfig
+++ b/hw/i386/Kconfig
@@ -40,6 +40,7 @@ config PC
imply NVDIMM
imply FDC_ISA
imply BATTERY
+ imply AC_ADAPTER
select I8259
select I8254
select PCKBD
diff --git a/include/hw/acpi/acad.h b/include/hw/acpi/acad.h
new file mode 100644
index 0000000000..f163158f35
--- /dev/null
+++ b/include/hw/acpi/acad.h
@@ -0,0 +1,25 @@
+/*
+ * QEMU emulated AC adapter device.
+ *
+ * Copyright (c) 2019-2026 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_AC_ADAPTER_H
+#define HW_ACPI_AC_ADAPTER_H
+
+#define TYPE_AC_ADAPTER "acad"
+#define AC_ADAPTER_IOPORT_PROP "ioport"
+
+#define AC_ADAPTER_VAL_UNKNOWN 0xFFFFFFFF
+
+#define AC_ADAPTER_LEN 1
+
+#endif
diff --git a/include/hw/acpi/acpi_dev_interface.h b/include/hw/acpi/acpi_dev_interface.h
index a6f9022c0b..00566c56a7 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_GENERIC_ERROR = 128,
ACPI_BATTERY_CHANGE_STATUS = 256,
+ ACPI_AC_ADAPTER_CHANGE_STATUS = 2048,
} AcpiEventStatusBits;
#define TYPE_ACPI_DEVICE_IF "acpi-device-interface"
diff --git a/qapi/acpi.json b/qapi/acpi.json
index 4711a05614..025b5d8eaa 100644
--- a/qapi/acpi.json
+++ b/qapi/acpi.json
@@ -213,3 +213,50 @@
##
{ 'command': 'query-battery',
'returns': 'BatteryInfo' }
+
+##
+# @ac-adapter-set-state:
+#
+# Set the state of the emulated AC adapter device
+#
+# @connected: whether the AC adapter is connected
+#
+# Since: 11.1
+#
+# .. qmp-example::
+#
+# -> { "execute": "ac-adapter-set-state",
+# "arguments": { "connected": true } }
+# <- { "return": {} }
+##
+{ 'command': 'ac-adapter-set-state',
+ 'data': { 'connected': 'bool' } }
+
+##
+# @AcAdapterInfo:
+#
+# AC adapter state information
+#
+# @connected: whether the AC adapter is connected
+#
+# Since: 11.1
+##
+{ 'struct': 'AcAdapterInfo',
+ 'data': { 'connected': 'bool' } }
+
+##
+# @query-ac-adapter:
+#
+# Query the current state of the emulated AC adapter device
+#
+# Returns: AC adapter connection state
+#
+# Since: 11.1
+#
+# .. qmp-example::
+#
+# -> { "execute": "query-ac-adapter" }
+# <- { "return": { "connected": true } }
+##
+{ 'command': 'query-ac-adapter',
+ 'returns': 'AcAdapterInfo' }
--
2.54.0
^ permalink raw reply related [flat|nested] 11+ messages in thread
* [PATCH v4 6/8] docs/specs: Introduce the QEMU lid button documentation
2026-05-26 4:29 [PATCH v4 0/8] Introduce a battery, AC adapter, and lid button Leonid Bloch
` (4 preceding siblings ...)
2026-05-26 4:29 ` [PATCH v4 5/8] hw/acpi: Introduce the QEMU AC adapter Leonid Bloch
@ 2026-05-26 4:29 ` Leonid Bloch
2026-05-26 4:29 ` [PATCH v4 7/8] hw/acpi: Introduce the QEMU lid button Leonid Bloch
2026-05-26 4:29 ` [PATCH v4 8/8] scripts: Add laptop-mirror reference script Leonid Bloch
7 siblings, 0 replies; 11+ messages in thread
From: Leonid Bloch @ 2026-05-26 4:29 UTC (permalink / raw)
To: Michael S . Tsirkin, Igor Mammedov, Ani Sinha, Paolo Bonzini,
Richard Henderson, Eduardo Habkost, Eric Blake, Markus Armbruster,
Marcel Apfelbaum, Dmitry Fleytman
Cc: Leonid Bloch, qemu-devel
Signed-off-by: Leonid Bloch <lb.workbox@gmail.com>
---
MAINTAINERS | 5 ++
docs/specs/button.rst | 131 ++++++++++++++++++++++++++++++++++++++++++
docs/specs/index.rst | 1 +
3 files changed, 137 insertions(+)
create mode 100644 docs/specs/button.rst
diff --git a/MAINTAINERS b/MAINTAINERS
index 90941519e3..42179aba95 100644
--- a/MAINTAINERS
+++ b/MAINTAINERS
@@ -3050,6 +3050,11 @@ F: docs/specs/acad.rst
F: hw/acpi/acad*
F: include/hw/acpi/acad.h
+Button
+M: Leonid Bloch <lb.workbox@gmail.com>
+S: Maintained
+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..10e57a380e
--- /dev/null
+++ b/docs/specs/button.rst
@@ -0,0 +1,131 @@
+.. SPDX-License-Identifier: GPL-2.0-or-later
+
+========================
+Laptop Lid Button Device
+========================
+
+The button device provides laptop lid button state information to the guest via
+ACPI. Lid state is controlled via QMP commands, providing deterministic control
+for testing and migration safety.
+
+Configuration
+-------------
+
+The lid button device is created as an ISA device using ``-device button``.
+
+Properties
+~~~~~~~~~~
+
+``ioport`` (default: 0x53d)
+ I/O port base address for the lid button device register.
+
+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
+------------
+
+The following QMP commands control the lid button state:
+
+``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
+--------
+
+Basic usage::
+
+ # Start VM with lid button
+ 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
+
+Simulate closing lid::
+
+ # Start with lid open (default)
+ # Close the lid
+ echo '{"execute":"lid-button-set-state",
+ "arguments":{"open":false}}' | nc -N localhost 4444
+
+ # Later, open the lid
+ echo '{"execute":"lid-button-set-state",
+ "arguments":{"open":true}}' | nc -N localhost 4444
+
+Test suspend on lid close::
+
+ # Start VM with ACPI support
+ qemu-system-x86_64 -device button
+
+ # Close lid - guest OS should detect this and may suspend
+ echo '{"execute":"lid-button-set-state",
+ "arguments":{"open":false}}' | nc -N localhost 4444
+
+ # Open lid - guest OS should wake or detect lid open
+ echo '{"execute":"lid-button-set-state",
+ "arguments":{"open":true}}' | nc -N localhost 4444
+
+Combined with other laptop devices::
+
+ # Create a complete laptop environment
+ qemu-system-x86_64 -device battery -device acad -device button
+
+ # Simulate closing lid while on battery power
+ echo '{"execute":"battery-set-state",
+ "arguments":{"state":{"present":true,"charging":false,
+ "discharging":true,"charge-percent":60}}}
+ {"execute":"ac-adapter-set-state",
+ "arguments":{"connected":false}}
+ {"execute":"lid-button-set-state",
+ "arguments":{"open":false}}' | nc -N localhost 4444
diff --git a/docs/specs/index.rst b/docs/specs/index.rst
index a19c75384c..54d8f5f7c5 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
--
2.54.0
^ permalink raw reply related [flat|nested] 11+ messages in thread
* [PATCH v4 7/8] hw/acpi: Introduce the QEMU lid button
2026-05-26 4:29 [PATCH v4 0/8] Introduce a battery, AC adapter, and lid button Leonid Bloch
` (5 preceding siblings ...)
2026-05-26 4:29 ` [PATCH v4 6/8] docs/specs: Introduce the QEMU lid button documentation Leonid Bloch
@ 2026-05-26 4:29 ` Leonid Bloch
2026-05-26 4:29 ` [PATCH v4 8/8] scripts: Add laptop-mirror reference script Leonid Bloch
7 siblings, 0 replies; 11+ messages in thread
From: Leonid Bloch @ 2026-05-26 4:29 UTC (permalink / raw)
To: Michael S . Tsirkin, Igor Mammedov, Ani Sinha, Paolo Bonzini,
Richard Henderson, Eduardo Habkost, Eric Blake, Markus Armbruster,
Marcel Apfelbaum, Dmitry Fleytman
Cc: Leonid Bloch, qemu-devel
The lid button device communicates laptop lid state to the guest via ACPI.
Lid state is controlled programmatically via QMP commands for consistent
behavior across environments.
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 | 2 +
hw/acpi/Kconfig | 4 +
hw/acpi/button-stub.c | 20 +++
hw/acpi/button.c | 227 +++++++++++++++++++++++++++
hw/acpi/meson.build | 2 +
hw/acpi/trace-events | 4 +
hw/i386/Kconfig | 1 +
include/hw/acpi/acpi_dev_interface.h | 1 +
include/hw/acpi/button.h | 23 +++
qapi/acpi.json | 47 ++++++
10 files changed, 331 insertions(+)
create mode 100644 hw/acpi/button-stub.c
create mode 100644 hw/acpi/button.c
create mode 100644 include/hw/acpi/button.h
diff --git a/MAINTAINERS b/MAINTAINERS
index 42179aba95..1f8f3e247e 100644
--- a/MAINTAINERS
+++ b/MAINTAINERS
@@ -3054,6 +3054,8 @@ Button
M: Leonid Bloch <lb.workbox@gmail.com>
S: Maintained
F: docs/specs/button.rst
+F: hw/acpi/button*
+F: include/hw/acpi/button.h
Subsystems
----------
diff --git a/hw/acpi/Kconfig b/hw/acpi/Kconfig
index 889ace2dfa..0d5f885095 100644
--- a/hw/acpi/Kconfig
+++ b/hw/acpi/Kconfig
@@ -78,6 +78,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-stub.c b/hw/acpi/button-stub.c
new file mode 100644
index 0000000000..0ae478055b
--- /dev/null
+++ b/hw/acpi/button-stub.c
@@ -0,0 +1,20 @@
+/*
+ * QEMU emulated lid button device - QMP stubs.
+ *
+ * SPDX-License-Identifier: GPL-2.0-or-later
+ */
+
+#include "qemu/osdep.h"
+#include "qapi/error.h"
+#include "qapi/qapi-commands-acpi.h"
+
+void qmp_lid_button_set_state(bool open, Error **errp)
+{
+ error_setg(errp, "No lid button device found");
+}
+
+LidButtonInfo *qmp_query_lid_button(Error **errp)
+{
+ error_setg(errp, "No lid button device found");
+ return NULL;
+}
diff --git a/hw/acpi/button.c b/hw/acpi/button.c
new file mode 100644
index 0000000000..126969a5e7
--- /dev/null
+++ b/hw/acpi/button.c
@@ -0,0 +1,227 @@
+/*
+ * QEMU emulated lid button device
+ *
+ * Copyright (c) 2019-2026 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 "qapi/error.h"
+#include "hw/core/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
+
+enum {
+ LID_CLOSED = 0,
+ LID_OPEN = 1,
+};
+
+typedef struct ButtonState {
+ ISADevice dev;
+ MemoryRegion io;
+ uint16_t ioport;
+ uint8_t lid_state;
+ bool qmp_lid_open;
+} ButtonState;
+
+static void button_get_dynamic_status(ButtonState *s)
+{
+ trace_button_get_dynamic_status();
+
+ s->lid_state = s->qmp_lid_open ? LID_OPEN : LID_CLOSED;
+}
+
+static void button_realize(DeviceState *dev, Error **errp)
+{
+ ISADevice *d = ISA_DEVICE(dev);
+ ButtonState *s = BUTTON_DEVICE(dev);
+ bool ambiguous;
+
+ trace_button_realize();
+
+ object_resolve_path_type("", TYPE_BUTTON, &ambiguous);
+ if (ambiguous) {
+ error_setg(errp, "at most one %s device is permitted", TYPE_BUTTON);
+ return;
+ }
+
+ /* Initialize lid to open by default */
+ s->qmp_lid_open = true;
+
+ isa_register_ioport(d, &s->io, s->ioport);
+}
+
+static const Property button_device_properties[] = {
+ DEFINE_PROP_UINT16(BUTTON_IOPORT_PROP, ButtonState, ioport, 0x53d),
+};
+
+static const VMStateDescription button_vmstate = {
+ .name = "button",
+ .version_id = 1,
+ .minimum_version_id = 1,
+ .fields = (VMStateField[]) {
+ VMSTATE_BOOL(qmp_lid_open, 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._E0C", 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;
+ dc->hotpluggable = false;
+ 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:
+ g_assert_not_reached();
+ }
+}
+
+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(Error **errp)
+{
+ bool ambiguous;
+ Object *o = object_resolve_path_type("", TYPE_BUTTON, &ambiguous);
+
+ if (!o) {
+ error_setg(errp, "No lid button device found");
+ return NULL;
+ }
+ if (ambiguous) {
+ error_setg(errp, "More than one lid button device present");
+ return NULL;
+ }
+ return BUTTON_DEVICE(o);
+}
+
+void qmp_lid_button_set_state(bool open, Error **errp)
+{
+ ButtonState *s = find_button_device(errp);
+ Object *obj;
+
+ if (!s) {
+ return;
+ }
+
+ s->qmp_lid_open = open;
+
+ 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(errp);
+ LidButtonInfo *ret;
+
+ if (!s) {
+ return NULL;
+ }
+
+ ret = g_new0(LidButtonInfo, 1);
+ ret->open = s->qmp_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 731e9477e3..ab0acd2521 100644
--- a/hw/acpi/meson.build
+++ b/hw/acpi/meson.build
@@ -39,6 +39,8 @@ acpi_ss.add(when: 'CONFIG_BATTERY', if_true: files('battery.c'))
stub_ss.add(files('battery-stub.c'))
acpi_ss.add(when: 'CONFIG_AC_ADAPTER', if_true: files('acad.c'))
stub_ss.add(files('acad-stub.c'))
+acpi_ss.add(when: 'CONFIG_BUTTON', if_true: files('button.c'))
+stub_ss.add(files('button-stub.c'))
system_ss.add_all(when: 'CONFIG_ACPI', if_true: acpi_ss)
system_ss.add(when: 'CONFIG_GHES_CPER', if_true: files('ghes_cper.c'))
stub_ss.add(files('ghes_cper_stub.c'))
diff --git a/hw/acpi/trace-events b/hw/acpi/trace-events
index 67602000f3..13728637ce 100644
--- a/hw/acpi/trace-events
+++ b/hw/acpi/trace-events
@@ -95,3 +95,7 @@ battery_get_dynamic_status(uint32_t state, uint32_t rate, uint32_t charge) "Batt
# acad.c
acad_realize(void) "AC adapter device realize entry"
acad_get_dynamic_status(uint8_t state) "AC adapter read state: %"PRIu8
+
+# button.c
+button_realize(void) "Button device realize entry"
+button_get_dynamic_status(void) "Button read dynamic status entry"
diff --git a/hw/i386/Kconfig b/hw/i386/Kconfig
index 06f21cadb7..35c65d3f37 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 00566c56a7..35005d7ad0 100644
--- a/include/hw/acpi/acpi_dev_interface.h
+++ b/include/hw/acpi/acpi_dev_interface.h
@@ -16,6 +16,7 @@ typedef enum {
ACPI_GENERIC_ERROR = 128,
ACPI_BATTERY_CHANGE_STATUS = 256,
ACPI_AC_ADAPTER_CHANGE_STATUS = 2048,
+ ACPI_BUTTON_CHANGE_STATUS = 4096,
} 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..d0e2d33231
--- /dev/null
+++ b/include/hw/acpi/button.h
@@ -0,0 +1,23 @@
+/*
+ * QEMU emulated button device.
+ *
+ * Copyright (c) 2019-2026 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_LEN 1
+
+#endif
diff --git a/qapi/acpi.json b/qapi/acpi.json
index 025b5d8eaa..e0534e3657 100644
--- a/qapi/acpi.json
+++ b/qapi/acpi.json
@@ -260,3 +260,50 @@
##
{ '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: 11.1
+#
+# .. 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: 11.1
+##
+{ 'struct': 'LidButtonInfo',
+ 'data': { 'open': 'bool' } }
+
+##
+# @query-lid-button:
+#
+# Query the current state of the emulated laptop lid button device
+#
+# Returns: lid button state
+#
+# Since: 11.1
+#
+# .. qmp-example::
+#
+# -> { "execute": "query-lid-button" }
+# <- { "return": { "open": true } }
+##
+{ 'command': 'query-lid-button',
+ 'returns': 'LidButtonInfo' }
--
2.54.0
^ permalink raw reply related [flat|nested] 11+ messages in thread
* [PATCH v4 8/8] scripts: Add laptop-mirror reference script
2026-05-26 4:29 [PATCH v4 0/8] Introduce a battery, AC adapter, and lid button Leonid Bloch
` (6 preceding siblings ...)
2026-05-26 4:29 ` [PATCH v4 7/8] hw/acpi: Introduce the QEMU lid button Leonid Bloch
@ 2026-05-26 4:29 ` Leonid Bloch
7 siblings, 0 replies; 11+ messages in thread
From: Leonid Bloch @ 2026-05-26 4:29 UTC (permalink / raw)
To: Michael S . Tsirkin, Igor Mammedov, Ani Sinha, Paolo Bonzini,
Richard Henderson, Eduardo Habkost, Eric Blake, Markus Armbruster,
Marcel Apfelbaum, Dmitry Fleytman
Cc: Leonid Bloch, qemu-devel
Add an in-tree Python reference implementation that reads the host's
battery, AC adapter and lid state from sysfs/procfs and forwards changes
to a running QEMU guest through the battery-set-state,
ac-adapter-set-state and lid-button-set-state QMP commands.
This script is intended as an example for management layers
(such as libvirt) to follow when wiring host hardware to those devices,
not as an end-user deployment tool.
Signed-off-by: Leonid Bloch <lb.workbox@gmail.com>
---
MAINTAINERS | 6 +
docs/tools/index.rst | 1 +
docs/tools/laptop-mirror.rst | 82 +++++++++++++
scripts/laptop-mirror.py | 219 +++++++++++++++++++++++++++++++++++
4 files changed, 308 insertions(+)
create mode 100644 docs/tools/laptop-mirror.rst
create mode 100755 scripts/laptop-mirror.py
diff --git a/MAINTAINERS b/MAINTAINERS
index 1f8f3e247e..9616544d29 100644
--- a/MAINTAINERS
+++ b/MAINTAINERS
@@ -3057,6 +3057,12 @@ F: docs/specs/button.rst
F: hw/acpi/button*
F: include/hw/acpi/button.h
+Laptop mirror
+M: Leonid Bloch <lb.workbox@gmail.com>
+S: Maintained
+F: docs/tools/laptop-mirror.rst
+F: scripts/laptop-mirror.py
+
Subsystems
----------
Overall Audio backends
diff --git a/docs/tools/index.rst b/docs/tools/index.rst
index 868c3c4d9d..0c8fa6010f 100644
--- a/docs/tools/index.rst
+++ b/docs/tools/index.rst
@@ -17,3 +17,4 @@ command line utilities and other standalone programs.
qemu-trace-stap
qemu-vmsr-helper
qemu-vnc
+ laptop-mirror
diff --git a/docs/tools/laptop-mirror.rst b/docs/tools/laptop-mirror.rst
new file mode 100644
index 0000000000..62fc2f92c3
--- /dev/null
+++ b/docs/tools/laptop-mirror.rst
@@ -0,0 +1,82 @@
+=======================
+QEMU laptop mirror tool
+=======================
+
+Synopsis
+--------
+
+**laptop-mirror.py** [*OPTIONS*]
+
+Description
+-----------
+
+``laptop-mirror.py`` polls the host's battery, AC adapter and lid state
+from sysfs/procfs and forwards every change to a running QEMU guest using
+the ``battery-set-state``, ``ac-adapter-set-state`` and
+``lid-button-set-state`` QMP commands. This script is a reference for how
+a management layer (libvirt or similar) can wire host hardware to them,
+and isn't meant for production use as-is.
+
+Options
+-------
+
+.. program:: laptop-mirror.py
+
+.. option:: -s SOCKET, --socket SOCKET
+
+ QMP socket: a Unix path or ``host:port``. Falls back to ``$QMP_SOCKET``.
+
+.. option:: -i SECONDS, --interval SECONDS
+
+ Polling interval, in seconds. Default ``2.0``.
+
+.. option:: --battery, --no-battery
+
+ Mirror the battery (default: on).
+
+.. option:: --ac-adapter, --no-ac-adapter
+
+ Mirror the AC adapter (default: on).
+
+.. option:: --lid, --no-lid
+
+ Mirror the lid button (default: on). A device that is enabled but not
+ present on the host is silently skipped.
+
+.. option:: -v, --verbose
+
+ ``-v`` logs every state change; ``-vv`` adds debug output.
+
+Example
+-------
+
+Start QEMU with the laptop devices and a QMP socket::
+
+ qemu-system-x86_64 \
+ -device battery -device acad -device button \
+ -qmp unix:/tmp/qmp.sock,server=on,wait=off \
+ ...
+
+Then mirror your host state::
+
+ export QMP_SOCKET=/tmp/qmp.sock
+ $builddir/run scripts/laptop-mirror.py -v
+
+The script depends on the in-tree ``qemu.qmp`` package; ``$builddir/run``
+puts it on ``PYTHONPATH``.
+
+Caveats
+-------
+
+* QMP allows one client at a time. If ``qmp-shell``, libvirt or another
+ script is already connected, the mirror times out after ten seconds and
+ exits with an error.
+* When QEMU runs as root, its Unix QMP socket is root-owned. Run the
+ mirror as root too, ``chmod`` the socket after QEMU is up, or expose
+ QMP over TCP.
+
+See also
+--------
+
+:doc:`/specs/battery`, :doc:`/specs/acad`, :doc:`/specs/button`,
+:manpage:`qemu-qmp-ref(7)`
diff --git a/scripts/laptop-mirror.py b/scripts/laptop-mirror.py
new file mode 100755
index 0000000000..8db76e4ff9
--- /dev/null
+++ b/scripts/laptop-mirror.py
@@ -0,0 +1,219 @@
+#!/usr/bin/env python3
+#
+# SPDX-License-Identifier: GPL-2.0-or-later
+#
+# Copyright (c) 2025-2026 Leonid Bloch <lb.workbox@gmail.com>
+
+"""Reference: mirror host laptop power state into a QEMU guest via QMP.
+
+The C devices (battery/acad/button) are pure QMP-controlled; this script
+shows how a management layer (libvirt etc.) might wire host sysfs/procfs
+state to the QMP commands. See docs/tools/laptop-mirror.rst.
+"""
+from __future__ import annotations
+
+import argparse
+import logging
+import os
+import signal
+import socket
+import sys
+import time
+from pathlib import Path
+from typing import Any
+
+try:
+ from qemu.qmp import QMPError
+ from qemu.qmp.legacy import QEMUMonitorProtocol
+except ModuleNotFoundError as exc:
+ print(f"Module '{exc.name}' not found.", file=sys.stderr)
+ print(f"Try $builddir/run {' '.join(sys.argv)}", file=sys.stderr)
+ sys.exit(1)
+
+
+log = logging.getLogger("laptop-mirror")
+
+POWER_SUPPLY = Path("/sys/class/power_supply")
+ACPI_BUTTON = Path("/proc/acpi/button")
+
+
+def read_str(p: Path) -> str | None:
+ try:
+ return p.read_text().strip()
+ except OSError:
+ return None
+
+
+def read_int(p: Path) -> int | None:
+ s = read_str(p)
+ try:
+ return int(s) if s is not None else None
+ except ValueError:
+ return None
+
+
+def find_supply(kind: str) -> Path | None:
+ if not POWER_SUPPLY.is_dir():
+ return None
+ for d in sorted(POWER_SUPPLY.iterdir()):
+ if read_str(d / "type") == kind:
+ return d
+ return None
+
+
+def find_lid() -> Path | None:
+ lid_dir = ACPI_BUTTON / "lid"
+ if not lid_dir.is_dir():
+ return None
+ for sub in sorted(lid_dir.iterdir()):
+ if (state := sub / "state").is_file():
+ return state
+ return None
+
+
+def battery_state(path: Path) -> dict[str, Any] | None:
+ status = read_str(path / "status") or ""
+ cap = read_int(path / "capacity")
+ if cap is None:
+ en, ef = read_int(path / "energy_now"), read_int(path / "energy_full")
+ if en is None or not ef:
+ return None
+ cap = en * 100 // ef
+
+ state: dict[str, Any] = {
+ "present": True,
+ "charging": status == "Charging",
+ "discharging": status == "Discharging",
+ "charge-percent": max(0, min(100, cap)),
+ }
+ pw = read_int(path / "power_now")
+ if pw is not None:
+ state["rate"] = abs(pw) // 1000
+ return state
+
+
+def ac_online(path: Path) -> bool | None:
+ v = read_int(path / "online")
+ return None if v is None else bool(v)
+
+
+def lid_open(path: Path) -> bool | None:
+ s = read_str(path)
+ return None if s is None else "open" in s.lower()
+
+
+def qmp_connect(address, timeout):
+ if isinstance(address, tuple):
+ sock = socket.create_connection(address, timeout=timeout)
+ else:
+ sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
+ sock.settimeout(timeout)
+ sock.connect(address)
+ sock.settimeout(timeout)
+ if not sock.recv(1, socket.MSG_PEEK):
+ sock.close()
+ raise TimeoutError
+ sock.settimeout(None)
+ return sock
+
+
+def parse_args() -> argparse.Namespace:
+ p = argparse.ArgumentParser(
+ description="Mirror host laptop hardware state to a QEMU guest "
+ "via QMP.")
+ p.add_argument("-s", "--socket", default=os.environ.get("QMP_SOCKET"),
+ help="QMP socket: unix path or addr:port "
+ "(default: $QMP_SOCKET)")
+ p.add_argument("-i", "--interval", type=float, default=2.0,
+ metavar="SECONDS",
+ help="polling interval in seconds (default: 2.0)")
+ p.add_argument("-v", "--verbose", action="count", default=0,
+ help="increase verbosity (-v info, -vv debug)")
+ p.add_argument("--battery", action=argparse.BooleanOptionalAction,
+ default=True, help="monitor the battery")
+ p.add_argument("--ac-adapter", action=argparse.BooleanOptionalAction,
+ default=True, help="monitor the AC adapter")
+ p.add_argument("--lid", action=argparse.BooleanOptionalAction,
+ default=True, help="monitor the lid button")
+ args = p.parse_args()
+ if not args.socket:
+ p.error("--socket is required (or set $QMP_SOCKET)")
+ if args.interval <= 0:
+ p.error("--interval must be positive")
+ if not (args.battery or args.ac_adapter or args.lid):
+ p.error("at least one device must be enabled")
+ return args
+
+
+def main() -> int:
+ args = parse_args()
+ levels = [logging.WARNING, logging.INFO, logging.DEBUG]
+ logging.basicConfig(level=levels[min(args.verbose, 2)],
+ format="%(message)s", stream=sys.stderr)
+ logging.getLogger("qemu.qmp").setLevel(logging.CRITICAL)
+
+ bat = find_supply("Battery") if args.battery else None
+ ac = find_supply("Mains") if args.ac_adapter else None
+ lid = find_lid() if args.lid else None
+ if not (bat or ac or lid):
+ log.error("No host laptop devices found to mirror")
+ return 1
+ for name, path in (("battery", bat), ("ac-adapter", ac), ("lid", lid)):
+ if path is not None:
+ log.info("Mirroring %s from %s", name, path)
+
+ try:
+ sock = qmp_connect(QEMUMonitorProtocol.parse_address(args.socket), 10)
+ except TimeoutError:
+ log.error("Timed out negotiating QMP with %s. Is another QMP "
+ "client (e.g. qmp-shell) holding the socket?", args.socket)
+ return 1
+ except OSError as exc:
+ log.error("Could not connect to %s: %s", args.socket, exc)
+ return 1
+
+ qmp = QEMUMonitorProtocol(sock)
+ try:
+ qmp.connect()
+ except QMPError as exc:
+ log.error("QMP error: %s", exc)
+ return 1
+
+ prev: dict[str, dict[str, Any]] = {}
+
+ def push(command: str, payload: dict[str, Any]) -> None:
+ if prev.get(command) == payload:
+ return
+ try:
+ qmp.cmd(command, **payload)
+ except QMPError as exc:
+ log.warning("%s failed: %s", command, exc)
+ return
+ prev[command] = payload
+ log.info("%s -> %s", command, payload)
+
+ running = True
+
+ def stop(_signum, _frame):
+ nonlocal running
+ running = False
+
+ signal.signal(signal.SIGINT, stop)
+ signal.signal(signal.SIGTERM, stop)
+
+ try:
+ while running:
+ if bat and (s := battery_state(bat)) is not None:
+ push("battery-set-state", {"state": s})
+ if ac and (c := ac_online(ac)) is not None:
+ push("ac-adapter-set-state", {"connected": c})
+ if lid and (o := lid_open(lid)) is not None:
+ push("lid-button-set-state", {"open": o})
+ time.sleep(args.interval)
+ return 0
+ finally:
+ qmp.close()
+
+
+if __name__ == "__main__":
+ sys.exit(main())
--
2.54.0
^ permalink raw reply related [flat|nested] 11+ messages in thread
* Re: [PATCH v4 3/8] hw/acpi: Introduce the QEMU Battery
2026-05-26 4:29 ` [PATCH v4 3/8] hw/acpi: Introduce the QEMU Battery Leonid Bloch
@ 2026-06-02 5:49 ` Markus Armbruster
0 siblings, 0 replies; 11+ messages in thread
From: Markus Armbruster @ 2026-06-02 5:49 UTC (permalink / raw)
To: Leonid Bloch
Cc: Michael S . Tsirkin, Igor Mammedov, Ani Sinha, Paolo Bonzini,
Richard Henderson, Eduardo Habkost, Eric Blake, Markus Armbruster,
Marcel Apfelbaum, Dmitry Fleytman, qemu-devel
Leonid Bloch <lb.workbox@gmail.com> writes:
> The battery device communicates battery state to the guest via ACPI.
> Battery state is controlled programmatically via QMP commands, making
> the device deterministic and migration-safe.
>
> Properties:
> - 'ioport': I/O port base address (default: 0x530)
>
> The device implements the ACPI_DEV_AML_IF interface to generate its
> own AML code, placing the BAT0 device directly under \_SB scope as
> per ACPI specification.
>
> QMP commands:
> - battery-set-state: Set battery state (present, charging, capacity, rate)
> - query-battery: Query current battery state
Could we have one patch adding the device, and a second one adding the
QMP commands?
>
> This provides a stable interface for virtualization management systems.
>
> Signed-off-by: Leonid Bloch <lb.workbox@gmail.com>
> Signed-off-by: Marcel Apfelbaum <marcel.apfelbaum@gmail.com>
> ---
> MAINTAINERS | 2 +
> hw/acpi/Kconfig | 4 +
> hw/acpi/battery-stub.c | 20 ++
> hw/acpi/battery.c | 364 +++++++++++++++++++++++++++
> hw/acpi/meson.build | 2 +
> hw/acpi/trace-events | 4 +
> hw/i386/Kconfig | 1 +
> include/hw/acpi/acpi_dev_interface.h | 1 +
> include/hw/acpi/battery.h | 32 +++
> qapi/acpi.json | 71 ++++++
> 10 files changed, 501 insertions(+)
> create mode 100644 hw/acpi/battery-stub.c
> create mode 100644 hw/acpi/battery.c
> create mode 100644 include/hw/acpi/battery.h
>
> diff --git a/MAINTAINERS b/MAINTAINERS
> index e356f46a58..ce50329f48 100644
> --- a/MAINTAINERS
> +++ b/MAINTAINERS
> @@ -3040,6 +3040,8 @@ Battery
> M: Leonid Bloch <lb.workbox@gmail.com>
> S: Maintained
> F: docs/specs/battery.rst
> +F: hw/acpi/battery*
> +F: include/hw/acpi/battery.h
>
> Subsystems
> ----------
> diff --git a/hw/acpi/Kconfig b/hw/acpi/Kconfig
> index daabbe6cd1..6b2c46d37a 100644
> --- a/hw/acpi/Kconfig
> +++ b/hw/acpi/Kconfig
> @@ -88,3 +88,7 @@ config ACPI_ERST
> config ACPI_CXL
> bool
> depends on ACPI
> +
> +config BATTERY
> + bool
> + depends on ACPI
> diff --git a/hw/acpi/battery-stub.c b/hw/acpi/battery-stub.c
> new file mode 100644
> index 0000000000..d2f13b51c1
> --- /dev/null
> +++ b/hw/acpi/battery-stub.c
> @@ -0,0 +1,20 @@
> +/*
> + * QEMU emulated battery device - QMP stubs.
> + *
> + * SPDX-License-Identifier: GPL-2.0-or-later
> + */
> +
> +#include "qemu/osdep.h"
> +#include "qapi/error.h"
> +#include "qapi/qapi-commands-acpi.h"
> +
> +void qmp_battery_set_state(BatteryInfo *state, Error **errp)
> +{
> + error_setg(errp, "No battery device found");
> +}
> +
> +BatteryInfo *qmp_query_battery(Error **errp)
> +{
> + error_setg(errp, "No battery device found");
> + return NULL;
> +}
> diff --git a/hw/acpi/battery.c b/hw/acpi/battery.c
> new file mode 100644
> index 0000000000..35b81ad486
> --- /dev/null
> +++ b/hw/acpi/battery.c
> @@ -0,0 +1,364 @@
> +/*
> + * QEMU emulated battery device.
> + *
> + * Copyright (c) 2019-2026 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 "qapi/error.h"
> +#include "hw/core/qdev-properties.h"
> +#include "migration/vmstate.h"
> +#include "hw/acpi/acpi_aml_interface.h"
> +#include "qapi/qapi-commands-acpi.h"
> +
> +#include "hw/acpi/battery.h"
> +
> +#define BATTERY_DEVICE(obj) OBJECT_CHECK(BatteryState, (obj), TYPE_BATTERY)
> +
> +#define BATTERY_DISCHARGING 0x01 /* ACPI _BST bit 0 */
> +#define BATTERY_CHARGING 0x02 /* ACPI _BST bit 1 */
> +#define BATTERY_CRITICAL 0x04 /* ACPI _BST bit 2 */
> +#define BATTERY_PRESENT 0x10 /* ACPI _STA bit 4 */
> +
> +typedef struct BatteryState {
> + ISADevice dev;
> + MemoryRegion io;
> + uint16_t ioport;
> + uint32_t state;
> + uint32_t rate;
> + uint32_t charge;
> + bool qmp_present;
> + bool qmp_charging;
> + bool qmp_discharging;
> + int32_t qmp_charge_percent;
> + int32_t qmp_rate;
> +} BatteryState;
> +
> +enum {
> + BSTA_ADDR = 0,
> + BRTE_ADDR = 4,
> + BCRG_ADDR = 8,
> +};
> +
> +static void battery_get_dynamic_status(BatteryState *s)
> +{
> + s->state = 0;
> + if (s->qmp_present) {
> + s->state |= BATTERY_PRESENT;
> + if (s->qmp_charging) {
> + s->state |= BATTERY_CHARGING;
> + }
> + if (s->qmp_discharging) {
> + s->state |= BATTERY_DISCHARGING;
> + }
> + }
> + s->rate = s->qmp_rate;
> + s->charge = (s->qmp_charge_percent * BATTERY_FULL_CAP) / 100;
> +
> + trace_battery_get_dynamic_status(s->state, s->rate, s->charge);
> +}
> +
> +static void battery_realize(DeviceState *dev, Error **errp)
> +{
> + ISADevice *d = ISA_DEVICE(dev);
> + BatteryState *s = BATTERY_DEVICE(dev);
> + bool ambiguous;
> +
> + trace_battery_realize();
> +
> + object_resolve_path_type("", TYPE_BATTERY, &ambiguous);
> + if (ambiguous) {
> + error_setg(errp, "at most one %s device is permitted", TYPE_BATTERY);
> + return;
> + }
> +
> + /* Initialize QMP state to sensible defaults */
> + s->qmp_present = true;
> + s->qmp_charging = false;
> + s->qmp_discharging = true;
> + s->qmp_charge_percent = 50;
> + s->qmp_rate = 1000; /* 1000 mW discharge rate */
> +
> + isa_register_ioport(d, &s->io, s->ioport);
> +}
> +
> +static const Property battery_device_properties[] = {
> + DEFINE_PROP_UINT16(BATTERY_IOPORT_PROP, BatteryState, ioport, 0x530),
> +};
> +
> +static const VMStateDescription battery_vmstate = {
> + .name = "battery",
> + .version_id = 1,
> + .minimum_version_id = 1,
> + .fields = (VMStateField[]) {
> + VMSTATE_BOOL(qmp_present, BatteryState),
> + VMSTATE_BOOL(qmp_charging, BatteryState),
> + VMSTATE_BOOL(qmp_discharging, BatteryState),
> + VMSTATE_INT32(qmp_charge_percent, BatteryState),
> + VMSTATE_INT32(qmp_rate, BatteryState),
> + VMSTATE_END_OF_LIST()
> + }
> +};
> +
> +static void build_battery_aml(AcpiDevAmlIf *adev, Aml *scope)
> +{
> + Aml *dev, *field, *method, *pkg;
> + Aml *bat_state, *bat_rate, *bat_charge;
> + Aml *sb_scope;
> + BatteryState *s = BATTERY_DEVICE(adev);
> +
> + bat_state = aml_local(0);
> + bat_rate = aml_local(1);
> + bat_charge = aml_local(2);
> +
> + sb_scope = aml_scope("\\_SB");
> + dev = aml_device("BAT0");
> + aml_append(dev, aml_name_decl("_HID", aml_eisaid("PNP0C0A")));
> +
> + aml_append(dev, aml_operation_region("DBST", AML_SYSTEM_IO,
> + aml_int(s->ioport),
> + BATTERY_LEN));
> + field = aml_field("DBST", AML_DWORD_ACC, AML_NOLOCK, AML_PRESERVE);
> + aml_append(field, aml_named_field("BSTA", 32));
> + aml_append(field, aml_named_field("BRTE", 32));
> + aml_append(field, aml_named_field("BCRG", 32));
> + aml_append(dev, field);
> +
> + method = aml_method("_STA", 0, AML_NOTSERIALIZED);
> + aml_append(method, aml_return(aml_or(aml_int(0x0F),
> + aml_and(aml_name("BSTA"),
> + aml_int(0x10), NULL),
> + NULL)));
> + aml_append(dev, method);
> +
> + method = aml_method("_BIF", 0, AML_NOTSERIALIZED);
> + pkg = aml_package(13);
> + /* Power Unit */
> + aml_append(pkg, aml_int(0)); /* mW */
> + /* Design Capacity */
> + aml_append(pkg, aml_int(BATTERY_FULL_CAP));
> + /* Last Full Charge Capacity */
> + aml_append(pkg, aml_int(BATTERY_FULL_CAP));
> + /* Battery Technology */
> + aml_append(pkg, aml_int(1)); /* Secondary */
> + /* Design Voltage */
> + aml_append(pkg, aml_int(BATTERY_DESIGN_VOLTAGE));
> + /* Design Capacity of Warning */
> + aml_append(pkg, aml_int(BATTERY_CAPACITY_OF_WARNING));
> + /* Design Capacity of Low */
> + aml_append(pkg, aml_int(BATTERY_CAPACITY_OF_LOW));
> + /* Battery Capacity Granularity 1 */
> + aml_append(pkg, aml_int(BATTERY_CAPACITY_GRANULARITY));
> + /* Battery Capacity Granularity 2 */
> + aml_append(pkg, aml_int(BATTERY_CAPACITY_GRANULARITY));
> + /* Model Number */
> + aml_append(pkg, aml_string("QBAT001")); /* Model Number */
> + /* Serial Number */
> + aml_append(pkg, aml_string("SN00000")); /* Serial Number */
> + /* Battery Type */
> + aml_append(pkg, aml_string("Virtual")); /* Battery Type */
> + /* OEM Information */
> + aml_append(pkg, aml_string("QEMU")); /* OEM Information */
> + aml_append(method, aml_return(pkg));
> + aml_append(dev, method);
> +
> + pkg = aml_package(4);
> + /* Battery State */
> + aml_append(pkg, aml_int(0));
> + /* Battery Present Rate */
> + aml_append(pkg, aml_int(BATTERY_VAL_UNKNOWN));
> + /* Battery Remaining Capacity */
> + aml_append(pkg, aml_int(BATTERY_VAL_UNKNOWN));
> + /* Battery Present Voltage */
> + aml_append(pkg, aml_int(BATTERY_DESIGN_VOLTAGE));
> + aml_append(dev, aml_name_decl("DBPR", pkg));
> +
> + method = aml_method("_BST", 0, AML_NOTSERIALIZED);
> + aml_append(method, aml_store(aml_and(aml_name("BSTA"), aml_int(0x0F),
> + NULL),
> + bat_state));
> + aml_append(method, aml_store(aml_name("BRTE"), bat_rate));
> + aml_append(method, aml_store(aml_name("BCRG"), bat_charge));
> + aml_append(method, aml_store(bat_state,
> + aml_index(aml_name("DBPR"), aml_int(0))));
> + aml_append(method, aml_store(bat_rate,
> + aml_index(aml_name("DBPR"), aml_int(1))));
> + aml_append(method, aml_store(bat_charge,
> + aml_index(aml_name("DBPR"), aml_int(2))));
> + aml_append(method, aml_return(aml_name("DBPR")));
> + aml_append(dev, method);
> +
> + aml_append(sb_scope, dev);
> + aml_append(scope, sb_scope);
> +
> + /* Status Change */
> + method = aml_method("\\_GPE._E08", 0, AML_NOTSERIALIZED);
> + aml_append(method, aml_notify(aml_name("\\_SB.BAT0"), aml_int(0x80)));
> + aml_append(scope, method);
> +}
> +
> +static void battery_class_init(ObjectClass *class, const void *data)
> +{
> + DeviceClass *dc = DEVICE_CLASS(class);
> + AcpiDevAmlIfClass *adevc = ACPI_DEV_AML_IF_CLASS(class);
> +
> + dc->realize = battery_realize;
> + dc->hotpluggable = false;
> + device_class_set_props(dc, battery_device_properties);
> + dc->vmsd = &battery_vmstate;
> + adevc->build_dev_aml = build_battery_aml;
> +}
> +
> +static uint64_t battery_ioport_read(void *opaque, hwaddr addr, unsigned size)
> +{
> + BatteryState *s = opaque;
> +
> + battery_get_dynamic_status(s);
> +
> + switch (addr) {
> + case BSTA_ADDR:
> + return s->state;
> + case BRTE_ADDR:
> + return s->rate;
> + case BCRG_ADDR:
> + return s->charge;
> + default:
> + g_assert_not_reached();
> + }
> +}
> +
> +static const MemoryRegionOps battery_ops = {
> + .read = battery_ioport_read,
> + .endianness = DEVICE_LITTLE_ENDIAN,
> + .valid = {
> + .min_access_size = 4,
> + .max_access_size = 4,
> + },
> + .impl = {
> + .min_access_size = 4,
> + .max_access_size = 4,
> + },
> +};
> +
> +static void battery_instance_init(Object *obj)
> +{
> + BatteryState *s = BATTERY_DEVICE(obj);
> +
> + memory_region_init_io(&s->io, obj, &battery_ops, s, "battery",
> + BATTERY_LEN);
> +}
> +
> +static const TypeInfo battery_info = {
> + .name = TYPE_BATTERY,
> + .parent = TYPE_ISA_DEVICE,
> + .instance_size = sizeof(BatteryState),
> + .class_init = battery_class_init,
> + .instance_init = battery_instance_init,
> + .interfaces = (InterfaceInfo[]) {
> + { TYPE_ACPI_DEV_AML_IF },
> + { },
> + },
> +};
> +
> +static BatteryState *find_battery_device(Error **errp)
> +{
> + bool ambiguous;
> + Object *o = object_resolve_path_type("", TYPE_BATTERY, &ambiguous);
> +
> + if (!o) {
> + error_setg(errp, "No battery device found");
> + return NULL;
> + }
> + if (ambiguous) {
> + error_setg(errp, "More than one battery device present");
How can we reach this error? battery_realize() refuses to realize more
than one.
> + return NULL;
> + }
> + return BATTERY_DEVICE(o);
> +}
> +
> +void qmp_battery_set_state(BatteryInfo *state, Error **errp)
> +{
> + BatteryState *s = find_battery_device(errp);
> + Object *obj;
> +
> + if (!s) {
> + return;
> + }
> +
> + if (state->charging && state->discharging) {
> + error_setg(errp,
> + "'charging' and 'discharging' are mutually exclusive");
> + return;
> + }
> + if (!state->present && (state->charging || state->discharging)) {
> + error_setg(errp,
> + "'charging'/'discharging' require 'present' to be true");
> + return;
> + }
> + if (state->charge_percent < 0 || state->charge_percent > 100) {
> + error_setg(errp, "'charge-percent' must be in the range 0..100");
> + return;
> + }
> + if (state->has_rate && (state->rate < 0 || state->rate > INT32_MAX)) {
> + error_setg(errp, "'rate' must be in the range 0..0x%" PRIX32,
> + (uint32_t)INT32_MAX);
> + return;
> + }
I feel some of these error messages need work, but let's nail down the
interface first.
> +
> + s->qmp_present = state->present;
> + s->qmp_charging = state->charging;
> + s->qmp_discharging = state->discharging;
> + s->qmp_charge_percent = state->charge_percent;
> +
> + if (state->has_rate) {
> + s->qmp_rate = state->rate;
> + }
> +
> + obj = object_resolve_path_type("", TYPE_ACPI_DEVICE_IF, NULL);
> + if (obj) {
> + acpi_send_event(DEVICE(obj), ACPI_BATTERY_CHANGE_STATUS);
> + }
> +}
> +
> +BatteryInfo *qmp_query_battery(Error **errp)
> +{
> + BatteryState *s = find_battery_device(errp);
> + BatteryInfo *ret;
> +
> + if (!s) {
> + return NULL;
> + }
> +
> + ret = g_new0(BatteryInfo, 1);
> +
> + ret->present = s->qmp_present;
> + ret->charging = s->qmp_charging;
> + ret->discharging = s->qmp_discharging;
> + ret->charge_percent = s->qmp_charge_percent;
> + ret->has_rate = true;
> + ret->rate = s->qmp_rate;
> +
> + ret->has_remaining_capacity = false;
> + ret->has_design_capacity = true;
> + ret->design_capacity = BATTERY_FULL_CAP;
> +
> + return ret;
> +}
> +
> +static void battery_register_types(void)
> +{
> + type_register_static(&battery_info);
> +}
> +
> +type_init(battery_register_types)
> diff --git a/hw/acpi/meson.build b/hw/acpi/meson.build
> index 1c5251909b..e6bc78274e 100644
> --- a/hw/acpi/meson.build
> +++ b/hw/acpi/meson.build
> @@ -35,6 +35,8 @@ if have_tpm
> endif
> stub_ss.add(files('acpi-stub.c', 'aml-build-stub.c', 'ghes-stub.c'))
> stub_ss.add(files('pci-bridge-stub.c'))
> +acpi_ss.add(when: 'CONFIG_BATTERY', if_true: files('battery.c'))
> +stub_ss.add(files('battery-stub.c'))
> system_ss.add_all(when: 'CONFIG_ACPI', if_true: acpi_ss)
> system_ss.add(when: 'CONFIG_GHES_CPER', if_true: files('ghes_cper.c'))
> stub_ss.add(files('ghes_cper_stub.c'))
> diff --git a/hw/acpi/trace-events b/hw/acpi/trace-events
> index edc93e703c..8a6ab91a13 100644
> --- a/hw/acpi/trace-events
> +++ b/hw/acpi/trace-events
> @@ -87,3 +87,7 @@ acpi_nvdimm_read_io_port(void) "Alert: we never read _DSM IO Port"
> acpi_nvdimm_dsm_mem_addr(uint64_t dsm_mem_addr) "dsm memory address 0x%" PRIx64
> acpi_nvdimm_dsm_info(uint32_t revision, uint32_t handle, uint32_t function) "Revision 0x%" PRIx32 " Handle 0x%" PRIx32 " Function 0x%" PRIx32
> acpi_nvdimm_invalid_revision(uint32_t revision) "Revision 0x%" PRIx32 " is not supported, expect 0x1"
> +
> +# battery.c
> +battery_realize(void) "Battery device realize entry"
> +battery_get_dynamic_status(uint32_t state, uint32_t rate, uint32_t charge) "Battery read state: 0x%"PRIx32", rate: %"PRIu32", charge: %"PRIu32
> diff --git a/hw/i386/Kconfig b/hw/i386/Kconfig
> index 12473acaa7..94004ffeb2 100644
> --- a/hw/i386/Kconfig
> +++ b/hw/i386/Kconfig
> @@ -39,6 +39,7 @@ config PC
> imply VIRTIO_VGA
> imply NVDIMM
> imply FDC_ISA
> + imply BATTERY
> 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 65debb90a8..a6f9022c0b 100644
> --- a/include/hw/acpi/acpi_dev_interface.h
> +++ b/include/hw/acpi/acpi_dev_interface.h
> @@ -14,6 +14,7 @@ typedef enum {
> ACPI_VMGENID_CHANGE_STATUS = 32,
> ACPI_POWER_DOWN_STATUS = 64,
> ACPI_GENERIC_ERROR = 128,
> + ACPI_BATTERY_CHANGE_STATUS = 256,
> } AcpiEventStatusBits;
>
> #define TYPE_ACPI_DEVICE_IF "acpi-device-interface"
> diff --git a/include/hw/acpi/battery.h b/include/hw/acpi/battery.h
> new file mode 100644
> index 0000000000..eaff760db9
> --- /dev/null
> +++ b/include/hw/acpi/battery.h
> @@ -0,0 +1,32 @@
> +/*
> + * QEMU emulated battery device.
> + *
> + * Copyright (c) 2019-2026 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_BATTERY_H
> +#define HW_ACPI_BATTERY_H
> +
> +#define TYPE_BATTERY "battery"
> +#define BATTERY_IOPORT_PROP "ioport"
> +
> +#define BATTERY_FULL_CAP 10000 /* mWh */
> +#define BATTERY_DESIGN_VOLTAGE 12000 /* mV */
> +
> +#define BATTERY_CAPACITY_OF_WARNING (BATTERY_FULL_CAP / 10) /* 10% */
> +#define BATTERY_CAPACITY_OF_LOW (BATTERY_FULL_CAP / 25) /* 4% */
> +#define BATTERY_CAPACITY_GRANULARITY (BATTERY_FULL_CAP / 100) /* 1% */
> +
> +#define BATTERY_VAL_UNKNOWN 0xFFFFFFFF
> +
> +#define BATTERY_LEN 0x0C
> +
> +#endif
> diff --git a/qapi/acpi.json b/qapi/acpi.json
> index 906b3687a5..4711a05614 100644
> --- a/qapi/acpi.json
> +++ b/qapi/acpi.json
> @@ -142,3 +142,74 @@
> ##
> { 'event': 'ACPI_DEVICE_OST',
> 'data': { 'info': 'ACPIOSTInfo' } }
> +
> +##
> +# @BatteryInfo:
> +#
> +# Battery state information
> +#
> +# @present: whether the battery is present
Peeking at the C code... there is a difference between "have no
TYPE_BATTERY device" and "have one, but not present". What does the
latter mean? The machine has a battery device, but the actual battery
pack is not connected?
> +#
> +# @charging: whether the battery is charging
> +#
> +# @discharging: whether the battery is discharging
Peeking at the C code... looks like @charging is initialized to false,
@discharging to true, battery-set-state doesn't let you set both to
true, and there is no other way to change them. Correct?
If yes, you're encoding a tri-state (charging, discharging, neither) as
two bools. Hmm.
Peeking at the C code some more... looks like there's more
entanglement: @charging and @discharging must be false when @present is
false.
Let's go through the cases:
present charging discharging meaning
false false false battery pack missing
true false true battery charging
true true false battery discharging
true false false ???
Please complete / correct column meaning.
Are there missing rows?
> +#
> +# @charge-percent: battery charge percentage (0-100)
> +#
> +# @rate: charge/discharge rate in mW (optional)
Drop the (optional), please. As is, the description gets rendered like
• rate (int, optional) – charge/discharge rate in mW (optional)
What does absent mean? Hmm, the type is both ther argument of
battery-set-state and the return value of query-battery. Let's discuss
there.
> +#
> +# @remaining-capacity: remaining capacity in mWh (optional)
Likewise.
> +#
> +# @design-capacity: design capacity in mWh (optional)
Likewise.
> +#
> +# Since: 11.1
> +##
> +{ 'struct': 'BatteryInfo',
> + 'data': { 'present': 'bool',
> + 'charging': 'bool',
> + 'discharging': 'bool',
> + 'charge-percent': 'int',
> + '*rate': 'int',
> + '*remaining-capacity': 'int',
> + '*design-capacity': 'int' } }
> +
> +##
> +# @battery-set-state:
> +#
> +# Set the state of the emulated battery device
> +#
> +# @state: new battery state
> +#
> +# Since: 11.1
> +#
> +# .. qmp-example::
> +#
> +# -> { "execute": "battery-set-state",
> +# "arguments": { "state": { "present": true,
> +# "charging": true,
> +# "discharging": false,
> +# "charge-percent": 85 } } }
> +# <- { "return": {} }
> +##
> +{ 'command': 'battery-set-state',
> + 'data': { 'state': 'BatteryInfo' } }
This interface fundamentally assumes there is at most one battery. Is
that what we want? Forever and ever?
Let's avoid the silly "state": { ... }:
{ 'command': 'battery-set-state', 'boxed': true,
'data': 'BatteryInfo' }
Peeking at the command handler code... the command fails when there is
no TYPE_BATTERY device or more than one.
Members @charging, @discharging, @charge-percent are mandatory. They
must be present even with "present": false. Do they make any sense
then?
Should we document the command fails when there is no battery device?
> +
> +##
> +# @query-battery:
> +#
> +# Query the current state of the emulated battery device
> +#
> +# Returns: current battery state
This gets rendered like
Command query-battery (Since: 11.1)
Query the current state of the emulated battery device
Return:
BatteryInfo – current battery state
If you simply omit the Returns: line, it'll be
Command query-battery (Since: 11.1)
Query the current state of the emulated battery device
Return:
BatteryInfo
Choice is up to you.
Should we document the command fails when there is no battery device?
> +#
> +# Since: 11.1
> +#
> +# .. qmp-example::
> +#
> +# -> { "execute": "query-battery" }
> +# <- { "return": { "present": true,
> +# "charging": true,
> +# "discharging": false,
> +# "charge-percent": 85 } }
> +##
> +{ 'command': 'query-battery',
> + 'returns': 'BatteryInfo' }
^ permalink raw reply [flat|nested] 11+ messages in thread
* Re: [PATCH v4 5/8] hw/acpi: Introduce the QEMU AC adapter
2026-05-26 4:29 ` [PATCH v4 5/8] hw/acpi: Introduce the QEMU AC adapter Leonid Bloch
@ 2026-06-02 5:51 ` Markus Armbruster
0 siblings, 0 replies; 11+ messages in thread
From: Markus Armbruster @ 2026-06-02 5:51 UTC (permalink / raw)
To: Leonid Bloch
Cc: Michael S . Tsirkin, Igor Mammedov, Ani Sinha, Paolo Bonzini,
Richard Henderson, Eduardo Habkost, Eric Blake, Marcel Apfelbaum,
Dmitry Fleytman, qemu-devel
Let's discuss PATCH 3 before I review this one and PATCH 7.
^ permalink raw reply [flat|nested] 11+ messages in thread
end of thread, other threads:[~2026-06-02 5:51 UTC | newest]
Thread overview: 11+ messages (download: mbox.gz follow: Atom feed
-- links below jump to the message on this page --
2026-05-26 4:29 [PATCH v4 0/8] Introduce a battery, AC adapter, and lid button Leonid Bloch
2026-05-26 4:29 ` [PATCH v4 1/8] hw/acpi: Support extended GPE handling for additional ACPI devices Leonid Bloch
2026-05-26 4:29 ` [PATCH v4 2/8] docs/specs: Introduce the QEMU Battery documentation Leonid Bloch
2026-05-26 4:29 ` [PATCH v4 3/8] hw/acpi: Introduce the QEMU Battery Leonid Bloch
2026-06-02 5:49 ` Markus Armbruster
2026-05-26 4:29 ` [PATCH v4 4/8] docs/specs: Introduce the QEMU AC adapter documentation Leonid Bloch
2026-05-26 4:29 ` [PATCH v4 5/8] hw/acpi: Introduce the QEMU AC adapter Leonid Bloch
2026-06-02 5:51 ` Markus Armbruster
2026-05-26 4:29 ` [PATCH v4 6/8] docs/specs: Introduce the QEMU lid button documentation Leonid Bloch
2026-05-26 4:29 ` [PATCH v4 7/8] hw/acpi: Introduce the QEMU lid button Leonid Bloch
2026-05-26 4:29 ` [PATCH v4 8/8] scripts: Add laptop-mirror reference script Leonid Bloch
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.