From: bugzilla-daemon@kernel.org
To: linux-bluetooth@vger.kernel.org
Subject: [Bug 221263] New: HID: Apple Magic Mouse 2 (BT 0x004C:0x0323) reports wrong battery percentage -- hidinput_query_battery_capacity() reads buf[1] (status byte) instead of buf[2] (AbsoluteStateOfCharge)
Date: Thu, 19 Mar 2026 23:50:48 +0000 [thread overview]
Message-ID: <bug-221263-62941@https.bugzilla.kernel.org/> (raw)
https://bugzilla.kernel.org/show_bug.cgi?id=221263
Bug ID: 221263
Summary: HID: Apple Magic Mouse 2 (BT 0x004C:0x0323) reports
wrong battery percentage --
hidinput_query_battery_capacity() reads buf[1]
(status byte) instead of buf[2]
(AbsoluteStateOfCharge)
Product: Drivers
Version: 2.5
Hardware: ARM
OS: Linux
Status: NEW
Severity: normal
Priority: P3
Component: Bluetooth
Assignee: linux-bluetooth@vger.kernel.org
Reporter: wh6cyy@gmail.com
Regression: No
Hi,
I am reporting a confirmed bug in the HID battery subsystem that causes the
Apple Magic Mouse 2 to permanently show 4% battery at boot when connected via
Bluetooth. The root cause has been isolated to a hardcoded byte-index
assumption in hidinput_query_battery_capacity() (drivers/hid/hid-input.c)
that is incompatible with the Magic Mouse 2's multi-field report 0x90
structure.
--- System information ---
Kernel: 6.18.15-400.asahi.fc43.aarch64+16k (Asahi Linux on Apple Silicon)
Device: Apple Magic Mouse 2, "William's Magic Mouse #2"
BT MAC: D0:C0:50:CF:DF:F1
HID IDs: Bus=0x0005 (Bluetooth), Vendor=0x004C (Apple BT),
Product=0x0323
Driver: hid-magicmouse (BT path)
--- Observed vs. expected behaviour ---
/sys/class/power_supply/hid-d0:c0:50:cf:df:f1-battery/capacity
reports: 4
macOS reports 94% for the same mouse simultaneously (confirmed ground
truth).
Occasional spontaneous correct readings (~95%) are observed at boot before
the first sysfs read (see Race Condition section below), confirming the
device transmits the correct value.
UPower sysfs snapshot:
POWER_SUPPLY_NAME=hid-d0:c0:50:cf:df:f1-battery
POWER_SUPPLY_TYPE=Battery
POWER_SUPPLY_STATUS=Discharging
POWER_SUPPLY_CAPACITY=4
POWER_SUPPLY_SCOPE=Device
POWER_SUPPLY_MODEL_NAME=William's Magic Mouse #2
--- HID Report Descriptor (battery collection) ---
Full raw descriptor (from hid-recorder):
R: 135 05 01 09 02 a1 01 85 12 05 09 19 01 29 02 15 00 25 01 95 02 75 01
81 02 95 01 75 06 81 03 05 01 09 01 a1 00 16 01 f8 26 ff 07 36 01 fb 46
ff 04 65 13 55 0d 09 30 09 31 75 10 95 02 81 06 75 08 95 02 81 01 c0 06
02 ff 09 55 85 55 15 00 26 ff 00 75 08 95 40 b1 a2 c0 06 00 ff 09 14 a1
01 85 90 05 84 75 01 95 03 15 00 25 01 09 61 05 85 09 44 09 46 81 02 95
05 81 01 75 08 95 01 15 00 26 ff 00 09 65 81 02 c0
Battery Application Collection (report 0x90):
85 90 -- Report ID 0x90
05 84 -- Usage Page: Power Device (0x84)
75 01 95 03 -- Report Size 1, Count 3
15 00 25 01 -- Logical Min 0, Max 1
09 61 -- Usage: PresentStatus (0x61) [bit 0]
05 85 -- Usage Page: Battery System (0x85)
09 44 -- Usage: Charging (0x44) [bit 1]
09 46 -- Usage: Discharging (0x46) [bit 2]
81 02 -- Input (FIELD 0: 3 x 1-bit status)
95 05 81 01 -- 5-bit padding
75 08 95 01 -- Report Size 8, Count 1
15 00 26 ff 00 -- Logical Min 0, Max 255
09 65 -- Usage: AbsoluteStateOfCharge (0x65)
81 02 -- Input (FIELD 1: 8-bit charge)
c0 -- End Collection
Raw buffer layout for report 0x90 (as seen by the kernel):
buf[0] = 0x90 -- report ID
buf[1] = 0x04 -- FIELD 0: status byte (bit2=Discharging=1, rest=0)
buf[2] = 0x5F -- FIELD 1: AbsoluteStateOfCharge = 95 decimal
--- Bluetooth traffic capture (btmon) ---
The kernel queries: 41 90 (GET_REPORT, INPUT type, report ID 0x90)
The mouse consistently responds: a1 90 04 5f
a1 = BT HID DATA INPUT header
90 = report ID
04 = status byte = 0b00000100 (bit2=Discharging)
5f = AbsoluteStateOfCharge = 95 decimal ← matches macOS 94%
The value 0x5f=95 is stable across every captured report.
--- Root cause ---
hidinput_query_battery_capacity() in drivers/hid/hid-input.c:
ret = hidinput_scale_battery_capacity(dev, buf[1]); /* BUG */
It hardcodes buf[1] as the battery byte. For reports where the battery
field is the first (and only) data field, buf[1] is correct. For report
0x90 on the Magic Mouse 2, buf[1] is the status byte (0x04 = 4) and the
AbsoluteStateOfCharge is at buf[2] (0x5F = 95).
Arithmetic confirming the bug:
Observed: buf[1] = 0x04 = 4 → scale(4, 0, 100) = 4% ✓ matches sysfs
Correct: buf[2] = 0x5F = 95 → scale(95, 0, 100) = 95% ✓ matches macOS
The correct byte offset is deterministic from the descriptor:
1 + (field->report_offset / 8) = 1 + (8/8) = 2 → buf[2]
The leading 1 accounts for the report-ID byte prepended by
hid_hw_raw_request().
--- Race condition / intermittent correct readings ---
hidinput_get_battery_property() has two paths:
A. Query path (buggy): when battery_status != HID_BATTERY_REPORTED, calls
hidinput_query_battery_capacity() → reads buf[1] = 4 → returns 4%.
B. Event-driven path (correct): after the first spontaneous HID INPUT
event for report 0x90 processes the AbsoluteStateOfCharge usage via
hid_input_var_field() (which correctly bit-extracts each usage's value
per the descriptor), hidinput_update_battery() caches 95 in
dev->battery_capacity and sets battery_status = HID_BATTERY_REPORTED.
Subsequent sysfs reads return 95%.
The race: if UPower reads sysfs before the first mouse input event (which is
typical at boot), path A fires → 4%. If the user moves the mouse before
UPower reads → path B fires first → 95%.
This explains the "sometimes correct at boot" behaviour: it is a genuine
race between UPower's enumeration and the first spontaneous HID report.
--- Why magicmouse_fetch_battery() does not apply ---
magicmouse_fetch_battery() checks is_usb_magicmouse2() which tests:
vendor == USB_VENDOR_ID_APPLE /* 0x05AC */
A BT-connected Magic Mouse 2 presents vendor=0x004C (Apple BT vendor ID),
not 0x05AC. is_usb_magicmouse2() returns false; the battery timer is never
armed; all battery handling falls through to hid-input.c where the bug lives.
--- Proposed fix direction ---
Option A (general fix): Store the AbsoluteStateOfCharge field's byte offset
at hidinput_configure_usage() time (e.g., in a new hdev->battery_field_offset
member) and use it in hidinput_query_battery_capacity() instead of the
hardcoded 1.
Option B (device-specific): Extend is_usb_magicmouse2() to also accept
BT_VENDOR_ID_APPLE (0x004C), arming the existing battery timer for BT-path
devices so magicmouse_fetch_battery() runs and primes battery_status before
the first sysfs read, bypassing the hid-input.c query path entirely.
I do not have a tested patch yet. I am happy to test patches.
Regards,
Will
--
Kernel: 6.18.15-400.asahi.fc43.aarch64+16k
Platform: Apple Silicon (Asahi Linux / Fedora 43)
--
You may reply to this email to add a comment.
You are receiving this mail because:
You are the assignee for the bug.
next reply other threads:[~2026-03-19 23:50 UTC|newest]
Thread overview: 3+ messages / expand[flat|nested] mbox.gz Atom feed top
2026-03-19 23:50 bugzilla-daemon [this message]
2026-03-19 23:52 ` [Bug 221263] HID: Apple Magic Mouse 2 (BT 0x004C:0x0323) reports wrong battery percentage -- hidinput_query_battery_capacity() reads buf[1] (status byte) instead of buf[2] (AbsoluteStateOfCharge) bugzilla-daemon
2026-03-20 9:06 ` bugzilla-daemon
Reply instructions:
You may reply publicly to this message via plain-text email
using any one of the following methods:
* Save the following mbox file, import it into your mail client,
and reply-to-all from there: mbox
Avoid top-posting and favor interleaved quoting:
https://en.wikipedia.org/wiki/Posting_style#Interleaved_style
* Reply using the --to, --cc, and --in-reply-to
switches of git-send-email(1):
git send-email \
--in-reply-to=bug-221263-62941@https.bugzilla.kernel.org/ \
--to=bugzilla-daemon@kernel.org \
--cc=linux-bluetooth@vger.kernel.org \
/path/to/YOUR_REPLY
https://kernel.org/pub/software/scm/git/docs/git-send-email.html
* If your mail client supports setting the In-Reply-To header
via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line
before the message body.
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox