How to correctly retrieve battery serial number?

3.1k views Asked by At

In Delphi 7 I'm working on a library implementing an object encapsulating information about the batteries attached to a system. It's working well, except for retrieving the serial number for the battery.

The code I am using for this call is as follows:

function TBattery.GetSerialNumber(hbat: THandle): boolean;
var
  bqi:          TBatteryQueryInformation;
  Serial:       PWideChar;
  SerialSize,
  dwOut:        DWORD;
begin
  Result := False;

  if hbat <> INVALID_HANDLE_VALUE then
  begin
    ZeroMemory(@bqi, SizeOf(bqi));
    dwOut := 0;

    bqi.BatteryTag := FBatteryTag;
    bqi.InformationLevel := BatterySerialNumber;

    SerialSize := 2048;
    GetMem(Serial, SerialSize);
    try
      ZeroMemory(Serial, SerialSize);

      Result := DeviceIoControl(hbat, IOCTL_BATTERY_QUERY_INFORMATION, @bqi,
                                SizeOf(bqi), Serial, SerialSize, @dwOut, nil);

      if Result then
        FSerialNumber := Serial;
    finally
      FreeMem(Serial, SerialSize);
    end;
  end;
end;

Unfortunately, DeviceIoControl() always returns False and if I check GetLastError() afterwards then it comes back with error 87, "the parameter is incorrect."

This doesn't make much sense, because the code works perfectly well if I simply change the InformationLevel from BatterySerialNumber to BatteryUniqueID, say. Also, I've used the handle to the battery (hbat) in other calls in the code before GetSerialNumber and they all work fine, and I can call others after this one fails as well, so that's not the issue.

Any ideas? I'm really at a loss.

2

There are 2 answers

3
RRUZ On BEST ANSWER

The issue it seems related to the dwOut variable which is passed as @dwOut, this variable represents the var lpBytesReturned parameter of the DeviceIoControl which is defined as

function DeviceIoControl(hDevice: THandle; dwIoControlCode: DWORD; lpInBuffer: Pointer;
  nInBufferSize: DWORD; lpOutBuffer: Pointer; nOutBufferSize: DWORD;
  var lpBytesReturned: DWORD; lpOverlapped: POverlapped): BOOL; stdcall;

So replacing your code by

  Result := DeviceIoControl(hbat, IOCTL_BATTERY_QUERY_INFORMATION, @bqi,
                            SizeOf(bqi), Serial, SerialSize, dwOut, nil);

Must fix the problem.

WinAPI

Also check this code translated to delphi from this msdn entry Enumerating Battery Devices which can help you to detect any additional issues with your code.

uses
  SetupApi,
  Windows,
  SysUtils;

type

  BATTERY_QUERY_INFORMATION_LEVEL = (
    BatteryInformation,
    BatteryGranularityInformation,
    BatteryTemperature,
    BatteryEstimatedTime,
    BatteryDeviceName,
    BatteryManufactureDate,
    BatteryManufactureName,
    BatteryUniqueID,
    BatterySerialNumber);
  TBatteryQueryInformationLevel = BATTERY_QUERY_INFORMATION_LEVEL;

  _BATTERY_QUERY_INFORMATION = record
    BatteryTag: ULONG;
    InformationLevel: BATTERY_QUERY_INFORMATION_LEVEL;
    AtRate: Longint;
  end;
  BATTERY_QUERY_INFORMATION = _BATTERY_QUERY_INFORMATION;
  PBATTERY_QUERY_INFORMATION = ^BATTERY_QUERY_INFORMATION;
  TBatteryQueryInformation = BATTERY_QUERY_INFORMATION;


const
  GUID_DEVCLASS_BATTERY:TGUID='{72631E54-78A4-11D0-BCF7-00AA00B7B32A}';
  //DEFINE_GUID( GUID_DEVCLASS_BATTERY, 0x72631E54, 0x78A4, 0x11D0, 0xBC, 0xF7, 0x00, 0xAA, 0x00, 0xB7, 0xB3, 0x2A );
  METHOD_BUFFERED     = 0;
  FILE_DEVICE_BATTERY = $00000029;
  FILE_READ_ACCESS    = $0001;    // for files and pipes

  IOCTL_BATTERY_QUERY_TAG =
    (FILE_DEVICE_BATTERY shl 16) or (FILE_READ_ACCESS shl 14) or ($10 shl 2) or (METHOD_BUFFERED);
  IOCTL_BATTERY_QUERY_INFORMATION =
    (FILE_DEVICE_BATTERY shl 16) or (FILE_READ_ACCESS shl 14) or ($11 shl 2) or (METHOD_BUFFERED);

function GetBatteryInfo(InformationLevel : BATTERY_QUERY_INFORMATION_LEVEL) : string;
var
   cbRequired : DWORD;
   hdev     : HDEVINFO;
   idev     : Integer;
   did      : TSPDeviceInterfaceData;
   pdidd    : PSPDeviceInterfaceDetailData;
   hBattery : THandle;
   bqi      : TBatteryQueryInformation;
   dwWait, dwOut : DWORD;
   lpOutBuffer: PWideChar;
begin
  // enumerate the batteries
  hdev :=  SetupDiGetClassDevs(@GUID_DEVCLASS_BATTERY, nil, 0,  DIGCF_PRESENT OR DIGCF_DEVICEINTERFACE);
  if ( INVALID_HANDLE_VALUE <>  THandle(hdev) ) then
  begin
      idev:=0;//first battery
      ZeroMemory(@did, SizeOf(did));
      did.cbSize := SizeOf(did);
      if (SetupDiEnumDeviceInterfaces(hdev, nil, GUID_DEVCLASS_BATTERY, idev, did)) then
      begin
        try
          cbRequired := 0;
          SetupDiGetDeviceInterfaceDetail(hdev, @did, nil, 0, cbRequired, nil);
         if (ERROR_INSUFFICIENT_BUFFER= GetLastError()) then
         begin
            pdidd:=AllocMem(cbRequired);
            try
              pdidd.cbSize := SizeOf(TSPDeviceInterfaceDetailData);
              if (SetupDiGetDeviceInterfaceDetail(hdev, @did, pdidd, cbRequired, cbRequired, nil)) then
              begin
                 hBattery :=CreateFile(pdidd.DevicePath, GENERIC_READ OR GENERIC_WRITE, FILE_SHARE_READ OR FILE_SHARE_WRITE, nil, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, 0);
                 if (INVALID_HANDLE_VALUE <> hBattery) then
                 begin
                  try
                    ZeroMemory(@bqi, SizeOf(bqi));
                     // With the tag, you can query the battery info.
                    dwWait := 0;
                      if (DeviceIoControl(hBattery, IOCTL_BATTERY_QUERY_TAG,  @dwWait, sizeof(dwWait), @bqi.BatteryTag, sizeof(bqi.BatteryTag), dwOut, nil)) then
                      begin
                        lpOutBuffer:=AllocMem(MAX_PATH);
                        try
                          ZeroMemory(lpOutBuffer,MAX_PATH);
                          bqi.InformationLevel:=InformationLevel;
                          if DeviceIoControl(hBattery, IOCTL_BATTERY_QUERY_INFORMATION, @bqi, SizeOf(BATTERY_QUERY_INFORMATION), lpOutBuffer, 255, dwOut,nil) then
                            Result:= WideCharToString(lpOutBuffer);
                        finally
                          FreeMem(lpOutBuffer);
                        end;
                      end;
                  finally
                    CloseHandle(hBattery)
                  end;
                 end;
              end;
            finally
              FreeMem(pdidd);
            end;
         end;
        finally
          SetupDiDestroyDeviceInfoList(hdev);
        end;
      end;
  end;
end;

begin
  try
    if not LoadsetupAPI then exit;
     Writeln(GetBatteryInfo(BatterySerialNumber));
  except
    on E: Exception do
      Writeln(E.ClassName, ': ', E.Message);
  end;
  readln;
end.

WMI

Finally as aside note, you can use the WMI to retrieve the same info, in this case using the BatteryStaticData WMI class

    {$APPTYPE CONSOLE}

    uses
      SysUtils,
      ActiveX,
      ComObj,
      Variants;

    // Battery Static Data

    procedure  GetBatteryStaticDataInfo;
    const
      WbemUser            ='';
      WbemPassword        ='';
      WbemComputer        ='localhost';
      wbemFlagForwardOnly = $00000020;
    var
      FSWbemLocator : OLEVariant;
      FWMIService   : OLEVariant;
      FWbemObjectSet: OLEVariant;
      FWbemObject   : OLEVariant;
      oEnum         : IEnumvariant;
      iValue        : LongWord;
    begin;
      FSWbemLocator := CreateOleObject('WbemScripting.SWbemLocator');
      FWMIService   := FSWbemLocator.ConnectServer(WbemComputer, 'root\WMI', WbemUser, WbemPassword);
      FWbemObjectSet:= FWMIService.ExecQuery('SELECT SerialNumber FROM BatteryStaticData','WQL',wbemFlagForwardOnly);
      oEnum         := IUnknown(FWbemObjectSet._NewEnum) as IEnumVariant;
      while oEnum.Next(1, FWbemObject, iValue) = 0 do
      begin
        Writeln(Format('SerialNumber    %s',[String(FWbemObject.SerialNumber)]));// String

        Writeln('');
        FWbemObject:=Unassigned;
      end;
    end;


    begin
     try
        CoInitialize(nil);
        try
          GetBatteryStaticDataInfo;
        finally
          CoUninitialize;
        end;
     except
        on E:EOleException do
            Writeln(Format('EOleException %s %x', [E.Message,E.ErrorCode])); 
        on E:Exception do
            Writeln(E.Classname, ':', E.Message);
     end;
     Writeln('Press Enter to exit');
     Readln;      
    end.
0
Restless On

In summary, the code @RRUZ and I posted work fine under Windows 7, as well as other third-party applications. They do not work for retrieving the serial number under Windows XP. I've also tested under WinXP and 7 with base installs of the OS on the exact same hardware, with identical results (success under Windows 7, not under windows XP).

It appears that under WinXP the value BatterySerialNumber for IOCTL_BATTERY_QUERY_INFORMATION's InformationLevel member is not supported, but this is not documented directly in the Windows SDK docs. It is documented that invalid entries should return error 1 (ERROR_INVALID_FUNCTION) for GetLastError(), but in this case is returning 87 (for an invalid parameter) instead. I posit that this is because that value in the enumeration is not valid, so it makes the parameter invalid, but I'm not exactly sure.

Thanks to all for their help, especially @RRUZ for going way above and beyond!

(As an aside, it appears that one can extract the serial number from the battery's Unique ID (Using BatteryUniqueID as the InformationLevel member) and removing the manufacturer name and device name from the unique ID. That's a terrible hack, but it's a semi-viable workaround for Windows XP.)