Thursday, September 02, 2021

Command Line Battery Information Finally Returns!

Well, I'm on a roll. Ever since I wrote that short program that auto-resets my typematic rates via Win32 API, I felt this urge to do something similar for a long-standing problem. But before I get into that, let me go back a few steps.

If you'd look carefully at this ancient entry from 2007, you would find that there's a weird cyan-coloured line in the console box in the lower left corner. That cyan-coloured line is the command-line output of the battery status of the laptop that I am running on. I find it necessary to have something like this for the simple reason that my taskbar is always hidden up in the top, which means that the notification area's battery information hardly ever shows up.

Combine that with me using console screens in near-full screen most of the time, its clear that I don't really get to see the battery status (or the time, for that matter, which explains the whole showing of the local time in rule aspect of my .vimrc file).

In the old days [in Cygwin], I could easily access battery information through the /proc/acpi/battery path, and parsing the information there using Python2 to generate what I wanted. This had two big advantages:
  1. Ease of access of information without doing strange things; and
  2. General portabiliity across both Cygwin and regular Linux.
Those were important back in the days where I dual-booted between Windows XP and Linux (Slackware).

The portability aspect diminished over time as I reached into the era of Windows 7 due to the use of virtual machines to run the Linux environments that I needed to get work done. It also coincided eventually with the removal of the /proc/acpi/battery interface in Cygwin, which gave me the problem of how to get the battery information to generate that information bar.

The answer was to use the wmic command, specifically the incantation:
wmic Path Win32_Battery
This incantation is a Windows-only way of obtaining all battery information within the system to stdout, after which it was just a case of parsing the returned text to generate what I needed. Since it still involved parsing of text, I used Python2 (and after that, Python3 since Python2 was no longer supported past 2020-01-01), with the difference that I would use a system fork() to execute the wmic incantation to receive its stdout process to operate on.

It worked. Sort of, in the correctness kind of way.

Truth is, it was slow as hell.

Since I was doing this every time the command prompt shows up, the noticeable delay was getting annoying. To be fair, it wasn't showing a multi-second delay, but it was definitely of the right order of magnitude to feel sluggish. Recall from yesterday's entry that 250 ms is sluggish in my book, that was definitely a no-go.

For the longest time then I had just turned off that functionality. It annoyed me of course. There was only one way to solve this, and it was to write an actual program using the Win32 API to do the query and output it all out without having to undergo all these crazy overheads of fork()-ing and then parsing [unnecessary] text when everything can be obtained in the compact binary way.

To me, using Win32 API used to mean ``install the gargantuan nonsense that is Microsoft Visual Studio, and go through the messy-ass process of defining an entire project just to compile one file''. But after yesterday's experience, I found that I could get away with it by just using g++ on Cygwin, with the Win32 API headers/libraries installed of course.

Unlike the FilterKeys code yesterday, this one is not that straightforward enough that I could just dump the source here. Briefly, I will describe what was needed:
  1. For general battery information (needed for the overall battery status), the GetSystemPowerStatus() API was all that was needed, and this one is easy.
  2. To get the individual battery information, it requires a messy-ass enumeration of the battery-class of devices with a lot of associated dereferencing. The general skeleton can be found in this MSDN documentation.
  3. The documentation is incomplete though, since it doesn't specify what headers are needed. On Cygwin, it turns out that one would need the following incantation to work:
    #include <windows.h>
    #include <winbase.h>
    #include <Setupapi.h>
    
    #define INITGUID
    #include <guiddef.h>
    #include <devguid.h>
    #include <BatClass.h>
    Don't question it---I spent time figuring this nonsense out from source-delving and what-not.
  4. Finally, the compilation process requires the linking of an additional library, the incantation being:
    g++ pbwarwin.cpp -lsetupapi
    Compare this to the FilterKeys code yesterday that didn't need anything extra.
And with that, I finished writing the program and did a quick test.
That's a one-sample estimate here, 220 ms for the wmic fork version in Python3 versus the 47 ms in this C++ program. That's nearly 5× faster, and even lower in latency than the 188 ms delay for my typematic rate.

Needless to say, I am pleased as punch at the success. In terms of stupid line count (I don't have sloccount set up heere), they are roughly the same at around 260+ lines, but the overheads are completely different, which result in this much improvement. There is also a subtle improvement here in the new C++ code: it generates detailed battery information for exactly the number of batteries that are present in the system, unlike the old version where I had to make some assumptions and hope for the best since there was no indication on the number of available batteries based only on the wmic output.

And so, I can now merrily have my battery information back. Till the next update.

No comments: