A clean macOS menu bar battery monitor with desktop widgets for a Razer DeathAdder V3 Pro and a ROG Falchion RX Low Profile.
Peripheral Battery keeps the two numbers you actually care about visible all the time: your mouse battery and your keyboard battery. The host app lives in the macOS menu bar, handles device polling and notifications, and feeds a lightweight WidgetKit extension for desktop widgets.
This project is based on uffiulf/razer-battery-status-macos for the original Razer battery
monitoring direction, then narrowed down into a cleaner menu bar app plus WidgetKit desktop
widget focused on just a few daily-use peripherals.
The current Razer battery path is reimplemented in native macOS IOKit code and keeps only the battery and charging pieces the app needs. The ASUS keyboard path is a separate integration built around the ROG Omni Receiver HID report flow used by the Falchion RX Low Profile in 2.4GHz mode.
The widget is designed to stay minimal on the desktop while still making battery state
glanceable in the actual menu bar and desktop setup. The current build supports both
small and medium widget families.
- Live battery reading for the Razer DeathAdder V3 Pro in the macOS menu bar
- Live battery reading for the ROG Falchion RX Low Profile in 2.4GHz mode
- Small and medium desktop widgets backed by the same shared snapshot data
- Low-battery notifications when a device drops to 20% or below
- Charging detection for the mouse when USB-C power is connected
- Manual refresh from the menu bar plus automatic periodic refresh
- Reconnect handling for wake-from-sleep and device hotplug events
- Clean desktop-focused widget styling with custom device icons
Razer DeathAdder V3 ProWireless dongle mode is supported for battery reads, and wired mode is used to detect charging.ROG Falchion RX Low ProfileBattery reporting is supported in 2.4GHz mode through the ROG Omni Receiver path currently implemented in the app.ROG Omni Receiverwith product ID0x1ACEThis is the receiver path the keyboard integration is built against.
Not currently implemented:
- Other Razer mice, even if they are close relatives, still need their own verified product IDs and protocol confirmation.
- Other ASUS or ROG keyboards still need their own captured HID traffic and response parsing.
- Bluetooth-specific battery paths are not implemented in this repo.
Adding a new device is mostly protocol reverse-engineering work rather than UI work.
- Identify the transport first. Confirm whether the device reports battery over USB control transfers, HID output/input reports, a receiver dongle, or a wired-only path.
- Capture the vendor traffic on Windows.
In practice this is often the fastest route because the official vendor software usually
exposes the proprietary battery query path there first. A common setup is
WiresharkwithUSBPcapfor USB traffic, then triggering battery refreshes, charge-state changes, or wireless-mode changes while recording. - Extract the protocol constants. You need the vendor and product IDs, interface number, report ID, request bytes, expected response shape, checksum or framing rules, and the exact byte that maps to battery percent.
- Reproduce the query on macOS.
For Razer-style USB control transfers, extend
src/RazerDevice.cppandsrc/RazerDevice.hpp. For receiver-driven HID devices similar to the Falchion path, start fromsrc/main.mm. - Use the local probe tools before wiring it into the app.
tools/rog_probe.mmandtools/rog_usb_probe.mmare useful for checking report sizes, interfaces, and raw replies on macOS before you add app UI logic. - Finish the app-facing integration. After the protocol is stable, add the new device name, widget presentation, and any custom icon or snapshot handling needed by the menu bar app and widget extension.
- The menu bar app polls supported devices using native macOS APIs and USB/HID access.
- The latest battery snapshot is written into the shared App Group container.
- The WidgetKit extension reads the same snapshot and renders the desktop widget.
The shared App Group identifier used by the project is group.com.young.peripheralbattery.
The Razer mouse battery path uses direct USB control transfers.
The ROG Falchion battery path depends on the Omni Receiver HID interrupt report, so macOS
requires Input Monitoring permission before the keyboard battery can be read reliably in
2.4GHz mode.
Enable it here after the first launch:
System Settings -> Privacy & Security -> Input Monitoring
If you change the permission, quit and reopen the app once.
makeThis builds the standalone menu bar binary.
./build_app.shThis is the recommended path when you want the full macOS app bundle with the embedded widget extension.
Running the script produces two copies:
./PeripheralBattery.appThis is the Xcode-built bundle copied out of DerivedData. Use this copy when installing to/Applicationsand when validating widget behavior./tmp/PeripheralBattery.appThis is a convenience copy that gets re-signed ad-hoc by the script. It is useful for quick local launches, but it should not be your default source bundle for/Applicationswhen you are debugging WidgetKit refreshes, extension identity, or App Group behavior.
Open PeripheralBattery.xcodeproj in Xcode and run the PeripheralBattery scheme.
The project contains two targets:
PeripheralBatteryfor the menu bar host appPeripheralBatteryWidgetExtensionfor the desktop widget
Before the first successful widget run in Xcode:
- Set your Apple team for both targets.
- Enable the App Group
group.com.young.peripheralbatteryfor both targets. - Run the app once and grant
Input Monitoringif macOS prompts for it. - Add the widget from the macOS desktop widget picker.
For command-line compile verification without signing:
xcodebuild -project PeripheralBattery.xcodeproj \
-scheme PeripheralBattery \
-configuration Debug \
-derivedDataPath build/XcodeDerived \
CODE_SIGNING_ALLOWED=NO \
build- Build the app bundle with
./build_app.shor from Xcode. - Use the freshly built
./PeripheralBattery.appas the install source. - Quit the running app if it is already open.
- Replace
/Applications/PeripheralBattery.appwith the new bundle. - Open the app from
/Applications. - Grant
Input Monitoringif you want keyboard battery reporting. - Add the desktop widget from the widget picker.
If you are iterating on the widget design, removing the old widget instance and re-adding it is often the fastest way to force macOS to pick up a fresh extension build.
When the menu bar app and the desktop widget both matter, the safe update chain is:
- Run
./build_app.shor rebuild from Xcode. - Treat the newly built
./PeripheralBattery.appas the canonical bundle. - Quit the old app if it is running.
- Replace
/Applications/PeripheralBattery.appwith the new bundle. - Launch the app from
/Applications. - If the keyboard path is involved, confirm
Input Monitoringis still enabled after launch. - If the widget still shows old data or old art, remove the existing desktop widget and add it again.
This matters because the widget is not just reading app files directly. The host app, the embedded
PeripheralBatteryWidgetExtension.appex, WidgetKit's cached extension identity, and the shared
App Group container all need to stay aligned on the same installed build.
The Falchion path depends on Input Monitoring. If keyboard battery suddenly disappears:
- Check
System Settings -> Privacy & Security -> Input Monitoring. - Confirm
PeripheralBatteryis still enabled. - If you changed the toggle, quit and reopen the app once.
If you want to confirm that the low-level ASUS receiver path is still alive, the local probe can check the raw HID reply path directly:
clang++ -x objective-c++ -std=c++17 -framework IOKit -framework CoreFoundation \
tools/rog_probe.mm -o /tmp/rog_probe && /tmp/rog_probeThis usually means the host app is fine, but the widget instance or installed extension is stale.
Quick checks:
- Verify the host app is still writing fresh shared snapshot data:
defaults read group.com.young.peripheralbattery batterySnapshot- If the snapshot is current, rebuild and reinstall the app using the
Update Flowabove. - Remove the old desktop widget instance and add it again.
- If needed, confirm the installed widget extension still carries the shared App Group entitlement:
codesign -d --entitlements :- \
/Applications/PeripheralBattery.app/Contents/PlugIns/PeripheralBatteryWidgetExtension.appexThe installed widget extension should expose group.com.young.peripheralbattery. If the menu bar
is correct but the widget is stale, the failure is usually in installation state, extension
identity, or widget instance caching rather than in the battery polling logic itself.
src/ Native menu bar app and device polling
Widget/ WidgetKit views, timeline store, icons, and widget assets
build_app.sh
Makefile
- Original Razer battery-monitoring direction from
uffiulf/razer-battery-status-macos - This repo's macOS menu bar app, widget integration, and multi-device adaptation are specific to Peripheral Battery
